Developer Tooling
I Built a CLI That Generates My Spring Boot Boilerplate
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
1# The usual way to start a new service:2# 1. Copy the last service's repo3# 2. Delete the domain-specific classes4# 3. Rename the package, the artifact id, the Docker image5# 4. Forget to update one of them6# 5. Spend an hour finding out which oneBoth 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:
1@Command(name = "create", description = "Scaffold a new Spring Boot service")2public class CreateCommand implements Callable<Integer> {34 @Parameters(index = "0", description = "Service name")5 String name;67 @Option(names = "--error-handling", defaultValue = "true")8 boolean errorHandling;910 @Option(names = "--observability")11 boolean observability;1213 @Option(names = "--resilience")14 boolean resilience;1516 @Override17 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}1public class ProjectGenerator {23 private final ProjectSpec spec;45 public ProjectGenerator(ProjectSpec spec) {6 this.spec = spec;7 }89 public void generate(Path targetDir) {10 TemplateEngine templates = TemplateEngine.fromClasspath("templates/spring-boot-base");1112 templates.renderAll(targetDir, Map.of(13 "serviceName", spec.name(),14 "packageName", spec.name().toLowerCase().replace("-", "")15 ));1617 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.
1$ springforge create payment-service \2 --error-handling \3 --observability \4 --resilience56Created 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 ├── Dockerfile11 ├── compose.yaml12 └── pom.xmlMaking 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.