Back to Articles

Developer Tooling

I Built a CLI That Generates My Spring Boot Boilerplate

2026-11-058 min read
CLIAutomation

Nine articles in this series each ended with "extract it into a reusable module." That only stays reusable if using it takes one command, not nine tabs of copy-pasting between repositories. The last piece of the toolkit is the thing that assembles the rest of it.

The problem

Starting a new Spring Boot service usually means one of two paths: start from an empty spring init and rebuild error handling, logging, correlation IDs, and resilience configuration from memory, or clone a previous service and manually strip out everything domain-specific.

The naive solution

the usual process
1# The usual way to start a new service:
2# 1. Copy the last service's repo
3# 2. Delete the domain-specific classes
4# 3. Rename the package, the artifact id, the Docker image
5# 4. Forget to update one of them
6# 5. Spend an hour finding out which one

Both paths work, and both cost real time on every single new service, time spent re-deriving decisions that were already made and already tested nine services ago.

Why it breaks

  • Copy-cloning a repo carries forward whatever domain logic and dependencies that repo happened to have, requiring careful manual deletion that is easy to get wrong under time pressure.
  • Starting from scratch means the base configuration for error handling, tracing, and resilience drifts slightly between services as memory of "how we did it last time" fades.
  • Neither path is scriptable, so it cannot run in a service-catalog self-service flow or a CI job that provisions a new microservice on request.

The production solution

A small Picocli-based CLI that scaffolds a base Spring Boot project and layers in the modules built across this series, selected per flag rather than baked in unconditionally:

CreateCommand.java
1@Command(name = "create", description = "Scaffold a new Spring Boot service")
2public class CreateCommand implements Callable<Integer> {
3
4 @Parameters(index = "0", description = "Service name")
5 String name;
6
7 @Option(names = "--error-handling", defaultValue = "true")
8 boolean errorHandling;
9
10 @Option(names = "--observability")
11 boolean observability;
12
13 @Option(names = "--resilience")
14 boolean resilience;
15
16 @Override
17 public Integer call() {
18 ProjectSpec spec = new ProjectSpec(name, errorHandling, observability, resilience);
19 new ProjectGenerator(spec).generate(Path.of(name));
20 System.out.println("Created " + name + "/");
21 return 0;
22 }
23}
ProjectGenerator.java
1public class ProjectGenerator {
2
3 private final ProjectSpec spec;
4
5 public ProjectGenerator(ProjectSpec spec) {
6 this.spec = spec;
7 }
8
9 public void generate(Path targetDir) {
10 TemplateEngine templates = TemplateEngine.fromClasspath("templates/spring-boot-base");
11
12 templates.renderAll(targetDir, Map.of(
13 "serviceName", spec.name(),
14 "packageName", spec.name().toLowerCase().replace("-", "")
15 ));
16
17 if (spec.errorHandling()) {
18 templates.copyModule("common/error", targetDir);
19 }
20 if (spec.observability()) {
21 templates.copyModule("common/observability", targetDir);
22 }
23 if (spec.resilience()) {
24 templates.copyModule("common/resilience", targetDir);
25 }
26 }
27}

Each common/* module is a directory of real, tested Java files, the exact ones from the earlier articles in this series, copied as-is into the generated project rather than templated string-by-string. Templating is reserved for the handful of values that genuinely vary: the service name, the package name, the artifact coordinates.

usage
1$ springforge create payment-service \
2 --error-handling \
3 --observability \
4 --resilience
5
6Created payment-service/
7 ├── src/main/java/.../common/error/ (from article 1)
8 ├── src/main/java/.../common/observability/ (from article 9)
9 ├── src/main/java/.../common/resilience/ (from article 6)
10 ├── Dockerfile
11 ├── compose.yaml
12 └── pom.xml

Making it reusable

The CLI itself becomes the artifact: a single JAR (or native image via GraalVM, for a CLI that needs to start in milliseconds) published once and run by anyone starting a new service, instead of a wiki page describing "how we usually set up a new repo" that goes stale the moment someone forgets to update it.

Testing it

The generator is the one component in this entire series that is genuinely simple to test end-to-end: run it against a temp directory with every flag combination, then assert the generated project actually compiles and its tests pass. That single check (does the output build) catches template drift immediately, long before someone downstream discovers a generated project that doesn't compile.

What I learned

The real value of this series was never the individual patterns, each one is a well-known technique on its own. It was collecting them into one place, tested and consistent, so that "start a new service the right way" stopped being a fact you had to remember and became a command you could just run.