Hire Me
← All Writing Architecture

The Twelve-Factor App Applied to Spring Boot

Applying the Twelve-Factor App methodology to Spring Boot microservices — what each factor means in practice, how Spring Boot supports it, and where developers commonly go wrong.

The Twelve-Factor App methodology was written in 2011 by the engineers at Heroku. Over fifteen years later it still reads as a near-perfect description of how a cloud-native service should be built. What’s changed is that Spring Boot now makes it significantly easier to comply — most factors are supported idiomatically out of the box. What hasn’t changed is the ways developers accidentally violate them. This post goes through all twelve, with the practical Spring Boot implementation and the failure modes I’ve seen in production, including patterns I’ve encountered at Mosaic Smart Data and DWP Digital.

I. Codebase — One repo, many deploys

One codebase tracked in version control. Multiple deploys (staging, production) from the same codebase, differentiated by configuration.

Spring Boot support: Natural. One Git repository, one pom.xml.

Common violation: Maintaining separate branches for each environment (staging, production) and manually cherry-picking changes. This creates divergence and makes it impossible to reason about what’s actually deployed. Use one main branch, tags for releases, and configuration profiles for environment differences.

II. Dependencies — Explicitly declared and isolated

All dependencies declared in pom.xml. Nothing assumed from the environment (no “I know the server has Oracle drivers installed”).

Spring Boot support: Maven and Gradle handle this. The uber-JAR (java -jar app.jar) is self-contained.

Common violation: Relying on a JNDI datasource provided by the application server, or assuming a JDBC driver is available on the classpath without declaring it. At one legacy DWP system I reviewed, the Oracle JDBC driver was dropped into the Tomcat lib/ folder on each server by hand and not declared anywhere in the build. The first containerisation attempt failed because the container had no Oracle driver and nobody knew why.

III. Config — Stored in the environment

All configuration that varies between deploys (database URLs, credentials, API keys, feature flags) stored in environment variables, not in the codebase.

Spring Boot support: @Value, @ConfigurationProperties, and the Environment abstraction all map environment variables to properties cleanly. Spring Boot’s externalized configuration hierarchy (application.yml < environment variables < command-line arguments) gives you precise control.

@ConfigurationProperties(prefix = "betfair")
@Validated
public record BetfairProperties(
    @NotBlank String appKey,
    @NotBlank String username,
    @NotBlank String sessionToken,
    @Min(1) @Max(60) int streamingTimeoutSeconds
) {}

Common violation: Hard-coding environment-specific values in application-prod.yml and committing it to Git. If application-prod.yml contains passwords or API keys, your secrets are in version control history permanently, even if you later remove the file. Use AWS Parameter Store, Secrets Manager, or Kubernetes Secrets for production credentials — never YAML files in Git.

IV. Backing Services — Treat as attached resources

Databases, caches, message queues, and external APIs are all “attached resources” accessed via URL/credentials from config. Swap a backing service (swap RDS for Aurora, or point at a different database) by changing config, not code.

Spring Boot support: spring.datasource.url, spring.kafka.bootstrap-servers, spring.data.redis.host — all injectable from the environment.

Common violation: Hardwiring the notion that “production uses RDS” into application code. If your service builds a datasource programmatically using a flag like if (env.equals("prod")) { ... }, you’ve violated this factor. Configuration, not branching logic, should determine which resource you connect to.

V. Build, Release, Run — Strictly separate stages

Build: convert source to an executable. Release: combine executable with config. Run: execute in the environment.

Spring Boot support: Maven/Gradle produces the build artifact (JAR). GitHub Actions (or your CI pipeline) combines it with environment-specific config to produce a versioned release. The container runs it.

Common violation: Running mvn package on the production server during deployment. This conflates build and run, makes the build dependent on the production environment’s Maven installation, and means your deployment requires network access to Maven Central. Build once, deploy the artefact.

VI. Processes — Execute as one or more stateless processes

Application instances are stateless. Any state that needs to persist between requests lives in a backing service (database, Redis, etc.).

Spring Boot support: Default Spring Boot is stateless — unless you add HttpSession or in-memory state.

Common violation: Storing session state in HttpSession and deploying multiple instances behind a load balancer without sticky sessions. Sessions are tied to a specific instance. When that instance scales down, sessions are lost. Use Spring Session with Redis for distributed session state, or better, design APIs to be stateless using JWTs. At DWP Digital, migrating legacy session-based authentication to JWT-based stateless auth was a prerequisite for deploying multiple replicas on Kubernetes.

VII. Port Binding — Export services via port binding

The service is self-contained and binds to a port itself. It doesn’t require an application server (Tomcat, JBoss) to be pre-installed.

Spring Boot support: Embedded Tomcat (or Undertow, Jetty) is included in the uber-JAR. The application binds to a configurable port via server.port. This is probably the factor Spring Boot implements most completely out of the box.

Common violation: Deploying to an external Tomcat installation as a WAR file. This is possible with Spring Boot but it bypasses the self-contained advantage. Containerisation of a WAR-based deployment is significantly more complex than containerising a JAR.

VIII. Concurrency — Scale out via the process model

Scale by running more instances of the application process, not by making single instances larger.

Spring Boot support: Stateless processes (Factor VI) are a prerequisite. Spring Boot services designed for containerisation scale horizontally cleanly.

Common violation: Spawning background threads inside the application for work that should be handled by a queue consumer. If your service processes tasks by spinning up ExecutorService threads, you’ve created internal concurrency that’s invisible to the orchestrator. Move that work to a Kafka consumer or SQS listener — the orchestrator can now scale consumers independently of request-handling workers.

IX. Disposability — Fast startup and graceful shutdown

Instances should start quickly (minimising time to serve traffic) and shut down gracefully (finishing in-flight requests before exiting).

Spring Boot support: Spring Boot 2.3+ added graceful shutdown support:

server:
  shutdown: graceful

spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

When the JVM receives SIGTERM, Spring Boot stops accepting new requests, waits up to 30 seconds for in-flight requests to complete, then shuts down cleanly. Without this, Fargate task replacements kill connections mid-request.

Common violation: Catching SIGTERM and ignoring it (common in legacy code), or using shutdown hooks that don’t coordinate with in-flight HTTP requests. Test your graceful shutdown explicitly — it’s the kind of thing that only breaks in production.

X. Dev/Prod Parity — Keep development, staging, and production as similar as possible

Spring Boot support: Testcontainers is the answer here. Start the same PostgreSQL version in tests that you run in production.

@SpringBootTest
@Testcontainers
class OrderRepositoryTest {

    @Container
    static PostgreSQLContainer<?> postgres =
        new PostgreSQLContainer<>("postgres:16-alpine");

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }
}

Common violation: Using H2 in tests and PostgreSQL in production. H2 SQL dialect differences (function names, type handling, constraint behaviour) mean tests pass but production fails. I’ve seen this cause silent data corruption in a DWP Digital integration — a query that H2 accepted silently and PostgreSQL rejected on a type mismatch. Use Testcontainers.

XI. Logs — Treat logs as event streams

Write logs to stdout/stderr. Don’t manage log files in the application.

Spring Boot support: The default Logback configuration writes to stdout. Don’t change this in production. Let your container orchestrator (ECS, Kubernetes) capture stdout and route it to CloudWatch, Elasticsearch, or your chosen log aggregation platform.

Common violation: Configuring Logback to write to a file on the filesystem in production containers. The file exists inside the container, is not persisted (ephemeral container storage), and disappears when the container is replaced. You lose your logs on every deployment.

XII. Admin Processes — Run admin tasks as one-off processes

Database migrations, one-time data fixes, and admin scripts should run as one-off processes from the same codebase, not baked into application startup.

Spring Boot support: Flyway or Liquibase for migrations. Run as a one-off task (java -jar app.jar --spring.flyway.enabled=true --server.port=-1) or as an ECS run-task triggered before each deployment.

Common violation: Running migrations inside ApplicationListener<ApplicationReadyEvent> during service startup. If you’re running three instances and they all start simultaneously, each attempts the migration. Flyway handles this with a lock, but it’s still a bad pattern — migration failures cause service startup failures, and the migration log is mixed into the application log. Separate the concerns.

Twelve factors, twenty minutes to read, years to fully internalise. The value isn’t in achieving a perfect score — it’s in having a shared vocabulary for the tradeoffs and a systematic way to audit what your services are and aren’t doing.

If you’re building Spring Boot microservices for cloud deployment and want an engineer who takes architectural hygiene seriously, get in touch.