The Twelve-Factor App

Twelve Rules for Cloud Services

The Twelve-Factor App is a well-known checklist (from Heroku) for building services that run well in the cloud. Spring Boot follows most of it out of the box.

#FactorIn a Spring Boot service
1CodebaseOne Git repo per service, many deploys (dev, test, prod)
2DependenciesDeclared in pom.xml; the fat JAR contains them
3ConfigIn the environment: SPRING_DATASOURCE_URL, profiles, Config Server
4Backing servicesDB, Kafka, Redis are attached resources selected by URL
5Build, release, runCI builds the JAR/image once; the same image goes to every environment
6ProcessesStateless: no user session in memory; store state in DB/Redis
7Port bindingEmbedded Tomcat exposes server.port; no external app server
8ConcurrencyScale out by running more instances
9DisposabilityFast startup, graceful shutdown (server.shutdown=graceful)
10Dev/prod paritySame DB type locally (Docker/Testcontainers) as in prod
11LogsWrite to stdout; the platform collects them
12Admin processesOne-off tasks (migrations with Flyway) run as part of the release

Config From the Environment

Example

Spring Boot maps environment variables onto properties automatically (relaxed binding): SPRING_DATASOURCE_URL → spring.datasource.url. The same JAR runs everywhere:

# same JAR, different environments
SERVER_PORT=8081 \
SPRING_DATASOURCE_URL=jdbc:mysql://prod-db:3306/product_db \
SPRING_DATASOURCE_USERNAME=product_svc \
SPRING_DATASOURCE_PASSWORD=$DB_PASSWORD \
java -jar product-service-1.0.0.jar

Example output

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
...
INFO  c.s.product.ProductServiceApplication : Starting ProductServiceApplication v1.0.0
INFO  o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8081 (http) with context path '/'
INFO  c.s.product.ProductServiceApplication : Started ProductServiceApplication in 3.214 seconds

Example: Stateless and Configurable

Example

A service reads its settings from an environment map (a stand-in for System.getenv()) with safe defaults, and keeps no per-user state in memory, so any instance can serve any request:

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Map<String, String> devEnv = Map.of();
        Map<String, String> prodEnv = Map.of(
                "SERVER_PORT", "8081",
                "PRODUCT_DB_URL", "jdbc:mysql://prod-db:3306/product_db",
                "LOG_LEVEL", "WARN");

        System.out.println("dev : " + ServiceConfig.from(devEnv));
        System.out.println("prod: " + ServiceConfig.from(prodEnv));

        // Stateless: the cart lives in a shared store, not in an instance field
        Map<String, List<String>> sharedCartStore = new TreeMap<>(); // stand-in for Redis
        Instance a = new Instance("instance-A", sharedCartStore);
        Instance b = new Instance("instance-B", sharedCartStore);
        a.addToCart("user7", "Kettle");      // first request hits A
        b.addToCart("user7", "Headphones");  // next request hits B
        System.out.println("Cart seen by A: " + a.cart("user7"));
    }
}

record ServiceConfig(int port, String dbUrl, String logLevel) {
    static ServiceConfig from(Map<String, String> env) {
        return new ServiceConfig(
                Integer.parseInt(env.getOrDefault("SERVER_PORT", "8080")),
                env.getOrDefault("PRODUCT_DB_URL", "jdbc:h2:mem:product_db"),
                env.getOrDefault("LOG_LEVEL", "INFO"));
    }
}

record Instance(String name, Map<String, List<String>> store) {
    void addToCart(String user, String item) {
        store.computeIfAbsent(user, k -> new ArrayList<>()).add(item);
        System.out.println(name + " added " + item + " for " + user);
    }
    List<String> cart(String user) { return store.getOrDefault(user, List.of()); }
}

Output

dev : ServiceConfig[port=8080, dbUrl=jdbc:h2:mem:product_db, logLevel=INFO]
prod: ServiceConfig[port=8081, dbUrl=jdbc:mysql://prod-db:3306/product_db, logLevel=WARN]
instance-A added Kettle for user7
instance-B added Headphones for user7
Cart seen by A: [Kettle, Headphones]

Tip: Never hard-code passwords or hostnames in application.yml that goes into Git. Use ${DB_PASSWORD} placeholders and set the value in the environment or a secret store.