Run Multiple Spring Boot Services in a Multi‑module Monorepo

Summary

The user is attempting to orchestrate a multi-module Spring Boot monorepo using the spring-boot-docker-compose dependency. While the project structure is logically sound, the user is encountering a fundamental limitation of the Maven Lifecycle. Running mvn spring-boot:run across a multi-module project executes the plugin sequentially per module. Because spring-boot:run is a blocking process, Maven waits for the first application to terminate before starting the second, making it impossible to run a distributed system of microservices using a single Maven command.

Root Cause

The root cause is the blocking nature of the Spring Boot Maven Plugin.

  • The spring-boot:run goal forks a JVM process and holds the execution thread until that process exits.
  • Maven’s default execution model for plugins is sequential.
  • Even if the -T (threaded) flag is used in Maven, the spring-boot:run goal is not designed to run as a background daemon; it is designed to run a single application for development.

Why This Happens in Real Systems

In complex monorepos, developers often try to treat their Build Tool (Maven/Gradle) as an Orchestrator (Kubernetes/Docker Compose). This leads to “Tooling Friction” where:

  • Lifecycle Mismatch: Build tools are designed to move from state A (source) to state B (artifact). They are not designed to maintain long-running stateful processes.
  • Resource Contention: Running 6+ Spring Boot applications via a single process manager often leads to Out-Of-Memory (OOM) errors on local machines if JVM heap sizes are not strictly capped.
  • Dependency Coupling: When the Docker Compose logic is embedded in the application runtime via spring-boot-docker-compose, each app tries to manage the same shared infrastructure, leading to race conditions during container startup/shutdown.

Real-World Impact

  • Developer Productivity Loss: Engineers spend significant time manually opening multiple IDE tabs or terminal windows to start services.
  • Inconsistent Environments: Some developers may run only a subset of services, leading to “works on my machine” bugs when integrated.
  • CI/CD Bottlenecks: Attempts to use the same “run all” logic in integration tests can cause CI runners to hang indefinitely.

Example or Code (if necessary and relevant)

To bypass the sequential blocking of Maven, a shell-based orchestration approach is required to launch processes in the background.

# Start all apps in the background using a loop
for app in app1 app2 app3 app4 app5 app6; do
  ./mvnw -pl $app -am spring-boot:run &
done

# To stop all background Java processes
pkill -f "spring-boot"

How Senior Engineers Fix It

Senior engineers decouple Build/Compilation from Runtime Orchestration. Instead of relying on Maven to “run” the apps, they implement one of the following:

  • Docker-Compose First: Move the spring-boot-docker-compose logic out of the Java code and into a standalone docker-compose.yml. Build the images once and run docker-compose up.
  • IDE Run Configurations: Create a Compound Run Configuration in IntelliJ IDEA or VS Code that launches all 6 Spring Boot Main classes in parallel.
  • Task Runners: Use a Makefile or a justfile to manage the lifecycle of the microservices, allowing for parallel execution and easy cleanup.
  • K3s/Kind: For larger monorepos, they deploy a local Lightweight Kubernetes cluster to mirror production orchestration exactly.

Why Juniors Miss It

  • Tooling Over-Reliance: Juniors often assume the build tool (Maven) is the primary interface for all application interactions, including runtime.
  • Lack of Process Knowledge: They may not distinguish between a build-time plugin and a runtime orchestrator.
  • Over-Engineering Local Dev: They try to find a “perfect” single command within the existing ecosystem rather than introducing a separate, simpler tool (like a shell script or Docker Compose) to handle process management.

Leave a Comment