Building software is ultimately about three high-level goals. The first, and arguably most important, solve problems. Second, manage complexity. Third, enable change. If the work you are doing isn’t getting to the heart of one of these pillars, it’s possible you are focused on the wrong thing. Sometimes, we churn unnecessarily on the nonessential. At points like this, we need to take a step back and evaluate. Is this really the best use of our time? Which of the three goals am I really tackling and is there a better way? Starter projects can help with these goals in multiple ways.
What Is a Starter Project?
A starter project is a jumping-off point. It’s all the structure, configuration and tooling that your organization has established as a common baseline for development work. Startups and small groups might not have much use for them. When you have more than a handful of teams and multiple handfuls of work, you might want to consider having your own. If you are a polyglot technology organization, you’ll likely have multiple starter projects, one for each technology stack.
Why Do You Need One?
A starter project helps to establish some consistency in derived projects. If your teams have total code ownership of projects using whatever language and framework they choose, then this might be less useful to you. If however, you have hundreds of apps and microservices and only a handful of teams, you will get a boost in efficiency by having some commonality across projects. Where does the configuration live? What’s the module structure? How is the build configured? These are all questions that can be answered when using a starter project.
In addition to the lower cognitive load, you also get project acceleration. Deadlines are always too close. Having a full working skeleton of a project in 15 minutes is better than the days of fiddling, copying and pasting, and otherwise hacking together a project from other recent work (and finding out later that you still missed things).
How Is It Used In Practice?
Your starter project will provide a combination of capabilities and guidelines / guardrails. How it is set up and used is based on a few questions and answers.
What is the structure? This applies to both the overall project and the hierarchy within it. For projects written in Java, this would include usage patterns around multi-module projects as well as how the package namespaces are set up.
How is the build done? Whatever your tool of choice might be (make, gradle, gulp, maven, shell scripts, etc), there’s a lot of reuse potential in the way builds are set up.
What is the versioning strategy?
Are there common dependencies that should be used? If your organization has settled on known libraries for solving certain problems then they can be included in the starter.
How is the application configuration handled and are there any defaults?
How is logging done? This includes formatting, appender configuration, file locations, etc.
How is artifact management implemented? The final binaries, releases and images have to go somewhere. Repository configuration and interaction can be specified in the starter.
How are the tests setup, configured and run? You should have unit, integration, and functional tests for every system. Predefine where these tests are located and how they are executed in your starting baseline. Some simple versions of these tests can be created as a default across projects to make requests against common end points or invoke some sort of health check functionality.
What is the build pipeline? I am a strong proponent CI/CD practices. Whatever tooling you decide to use for continuous delivery, you should be able to define the pipeline in the project itself. The added benefit of similar build workflows across projects is that it allows for the definition of common logic that all of them can leverage.
What sort of project automation and tooling do you usually need? Some of the structure and logic for functions like database migrations, code generation, and support utilities can be defined in the tooling that your starter project defines.
What is the default way to run the program for local execution? If possible, I prefer to have an application or service run locally on my laptop. This allows for all manner of testing and troubleshooting before code is ever pushed to SCM. Have this capability in place from the start of the project.
Is there documentation that projects need to have? Consider the API, application design, troubleshooting and general information documents that you’d like to have for your application. A set of markdown files with placeholder headings in a ‘docs’ folder is a sufficient starting point.
There are a few ways to create a new project based on your starter. One is to fork the project from the starter source code (assuming you are using a modern SCM tool like git). I have found this to be less than ideal. Mostly because during project work, it can become very easy to target the starter by mistake in a pull request. What we have opted to do is maintain scripting that does the derivation for you. This is mostly about text replacement in various files and renaming folders and files inside the project. The script takes some parameters, downloads the starter and does the various replace / rename operations based on the supplied values. Once that is done, the result is a fully-functional, derived project that can be checked into git.
There are tools out there that will do some of this for you. For example, JHipster gives you the capability of generating a project and subsequent code from the command line. I’m personally leery of code generation for code that you’re going to be modifying. Also, it might not cover all the capabilities that your starter may need. Other tools like Spring Initializr can give you a starter baseline. Some organizations maintain their own initializr that was forked from Spring’s and subsequently enhanced to provide the starter capabilities they need. Pick the right approach that best suits your needs.
What are the downsides? One of the biggest challenges has been trying to keep starter-derived projects up to date with changes and enhancements in the starter itself. Due to the realities of schedules and priorities, it is not possible to update everything at a single go every time we make significant changes. We have to allow for a range of drift between the starter and the derived projects. Unfortunately, there’s no objective way to determine when a project has gone too far out of sync and needs a team to make some updates. Oftentimes we are better at writing software than maintaining it.
There are ways to mitigate those downsides. Especially around common functionality. Rather than packing a lot of code into files / classes within the starter, common functionality should be extracted into libraries. This reduces the code needed in the starter and duplication in derived projects. It also makes updates as simple as incrementing dependency versions. When it comes to build logic, common tasks and customizations can be extracted into plugins for your build tool of choice, or into shared libraries used by your CI server.
It can be difficult to establish consensus for what the official starter should look like. Opinions will abound. As they often do about minor things. Try to get past the bike-shedding. One of the good things about having a starter project is that it encodes the collective decision-making and established practices into a working project. Then you don’t have to have the pointless debates about where the curly braces go or whether indentation is done with spaces or tabs (because the answer is obviously spaces unless you’re working on a makefile). All of those considerations are already agreed upon and in place. As with many guiding principles of human behavior, we want our starter projects to make it easy to do the right thing.