Apply each of the twelve factors to Java services on Kubernetes, detect and fix violations, extend the methodology with modern concerns, assess services for compliance, and plan legacy modernisation steps
The Twelve-Factor App is a methodology (originally from Heroku, 2011) describing how to build software-as-a-service applications that are portable across environments, deployable continuously, and scalable horizontally without architectural change. Its twelve factors codify practices — config in the environment, stateless processes, disposability, logs as streams — that now underpin container and Kubernetes platforms.
Figure 1 — The twelve factors
Vocabulary
Meaning
Backing service
Any service consumed over the network (DB, cache, queue)
Release
Build artifact + environment config
Disposability
Fast startup and graceful shutdown
Dev/prod parity
Minimal differences between environments
Table 1 — Terms
2Why It Exists
Violation
Consequence
Config compiled into the artifact
Rebuild per environment, drift
Session state in memory
Sticky sessions, lost carts on scale-in
Writing logs to local files
Lost on pod deletion; can't aggregate
Different DB in dev (H2) and prod (PostgreSQL)
Bugs only in production
Slow startup (90 s)
Autoscaling useless in spikes
Table 2 — Why
Figure 2 — Twelve-factor compliance vs incident rate
3Core Concepts — The Factors Applied (1–6)
Factor
Rule
Java / Kubernetes practice
I Codebase
One codebase, many deploys
One repo per service (or monorepo module); same commit to all envs
II Dependencies
Explicitly declare and isolate
Maven with BOMs; no reliance on system packages; minimal base images
III Config
Store config in the environment
Env vars/ConfigMaps/Secrets; Spring profiles only for structure; no secrets in images
IV Backing services
Treat as attached resources
Aurora, Redis, Kafka addressed by URL/credentials from config; swap without code change
V Build, release, run
Strictly separate stages
CI builds image once; GitOps release = digest + config; run = pods
VI Processes
Stateless, share-nothing
No in-memory sessions; files to S3; caches rebuildable
Table 3 — Factors I–VI
4How It Works Internally — The Factors Applied (7–12)
Factor
Rule
Java / Kubernetes practice
VII Port binding
Self-contained, export via port
Embedded Tomcat/Netty on 8080; no app server deployment
VIII Concurrency
Scale out via process model
HPA adds pods; virtual threads within a process
IX Disposability
Fast start, graceful stop
AOT cache/CRaC; graceful shutdown; idempotent work
X Dev/prod parity
Keep environments similar
Testcontainers with real PostgreSQL/Kafka; same image through envs
XI Logs
Treat as event streams
JSON to stdout; platform ships logs
XII Admin processes
Run as one-off processes
Kubernetes Jobs for migrations (Flyway), same image and config
Table 4 — Factors VII–XII
4ADeep Dive — Config (Factor III) Done Right
Config type
Mechanism
Non-secret settings
ConfigMap → env vars / mounted application.yaml
Secrets
External Secrets → Kubernetes Secret → env/volume
Feature toggles
Flag service (runtime, not deploy-time)
Environment structure
Kustomize overlays per env
Validation
@ConfigurationProperties with @Validated — fail fast at startup
Table 5 — Config layers
4BDeep Dive — Processes and State (Factor VI)
State
Twelve-factor home
HTTP session
Stateless JWT, or Spring Session on Redis
Uploaded files
S3 via presigned URLs
Caches
Redis or local caches that can be lost
Scheduled job locks
ShedLock on DB / Kubernetes CronJob
In-flight work
Queues with at-least-once + idempotency
Table 6 — Where state goes
4CDeep Dive — Disposability (Factor IX)
Aspect
Target
Startup to ready
< 10 s (AOT cache), < 1 s (CRaC)
Graceful shutdown
Finish in-flight within 25 s
Crash safety
Work is idempotent; messages redelivered
Kafka consumers
Commit offsets after processing; cooperative rebalancing
Table 7 — Fast and safe to kill
4DDeep Dive — Beyond Twelve Factors
Figure 3 — Beyond twelve factors
Extension
Why added
API first
Contracts enable parallel teams
Telemetry
Metrics and traces, not only logs
Security (authN/Z, least privilege)
Not covered originally
Health probes
Orchestrators need readiness/liveness
Graceful degradation
Resilience in distributed systems
Table 8 — Modern additions (e.g. "Beyond the Twelve-Factor App")
4EDeep Dive — Modernising Legacy Java (NovaBank)
Legacy trait
Modern step
WAR on WebLogic with JNDI datasources
Spring Boot executable jar, config via env
Session replication in cluster
Stateless JWT
Log4j to local files
JSON stdout
Nightly batch on the same server
Kubernetes CronJob / AWS Batch
Property files per environment in repo
ConfigMaps + Secrets Manager
Table 9 — Incremental path, one factor at a time
5APIs / Features / Syntax
JAVA
// Factor III: typed, validated configuration from the environment@ConfigurationProperties(prefix = "payments")@Validatedpublicrecord PaymentsProperties(@NotBlank String partnerBaseUrl,@NotNull Duration timeout,@Min(1) @Max(5) int maxAttempts) { }// env: PAYMENTS_PARTNERBASEURL, PAYMENTS_TIMEOUT=2s, PAYMENTS_MAXATTEMPTS=2
YAML
# Factor XII: admin process as a Job using the same image and config as the appapiVersion: batch/v1kind: Jobmetadata: name: orders-migrate-2026-09-28 annotations: { argocd.argoproj.io/hook: PreSync }spec: backoffLimit: 0 template: spec: restartPolicy: Never containers: - name: flyway image: 111122223333.dkr.ecr.ap-south-1.amazonaws.com/orders@sha256:9f2c... args: ["--spring.main.web-application-type=none", "--app.mode=migrate"] envFrom: - configMapRef: { name: orders-config } - secretRef: { name: orders-db }
JAVA
// Factor X: dev/prod parity with Testcontainers (same PostgreSQL major version as prod)@SpringBootTest@Testcontainersclass OrderRepositoryIT {@Container@ServiceConnectionstatic PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:17");@Autowired OrderRepository repo;@Testvoid savesAndLoads() { /* ... */ }}
6Practical Examples
6.1 Config rebuilds removed
A service baked environment URLs into its jar and had three builds per release. Moving to env-based config gave one image through all environments and removed a class of "wrong endpoint in prod" incidents.
6.2 Lost carts on scale-in
In-memory carts vanished when HPA scaled pods down after a sale. Moving carts to DynamoDB made pods disposable.
6.3 H2 in tests hid a bug
A query worked on H2 but failed on PostgreSQL due to case sensitivity. Testcontainers with PostgreSQL 17 now runs in CI.
6.4 Worked example — compliance scorecard
Factor
checkout
legacy-reports
III Config
✔
✘ (property files)
VI Processes
✔
✘ (local temp files)
XI Logs
✔
✘ (file appender)
Table 10 — Assess per service
6.5 Worked example — backing service swap
Change
Code change?
Self-managed Redis → ElastiCache
No — URL and credentials only
Table 11 — Attached resources
6.6 Worked example — build/release/run
Stage
Artifact
Build
Image digest sha256:9f2c…
Release
Digest + prod-aps1 overlay (Git commit)
Run
Pods
Table 12 — Traceable releases
6.7 Worked example — interview whiteboard
Prompt
Answer outline
"How do the twelve factors apply to Spring Boot on Kubernetes?"
"One codebase, many deploys; Maven-declared dependencies in minimal images; config via env/ConfigMaps/Secrets with validated @ConfigurationProperties; backing services as attached resources by URL; build once, release as digest+config via GitOps, run as pods; stateless processes with state in Redis/DynamoDB/S3; embedded server port binding; scale out via HPA; disposability via AOT/CRaC and graceful shutdown; parity via Testcontainers and same image; JSON logs to stdout; migrations as Jobs. Extend with API-first, telemetry, security and health probes."
Table 13 — Structured answer
6.8 Worked example — PayGrid config for cells
Config
Source
Cell ID, merchant ranges
Environment per cell deployment
Table 14 — Same image, many cells
6.9 Worked example — anti-patterns
Anti-pattern
Effect
Secrets in application.yaml in Git
Leaks
Profiles per environment containing URLs
Rebuild/drift
Local file state
Data loss
Long startup
Poor elasticity
Table 15 — Common mistakes
6.10 Worked example — concurrency model
Level
Mechanism
Within process
Virtual threads
Across processes
Pods via HPA
Table 16 — Both levels
6.11 Worked example — logs as streams
App does
Platform does
Write JSON to stdout
Collect, route, store
Table 17 — Separation of concerns
6.12 Worked example — admin tasks
Task
Mechanism
Data backfill
Job with same image and config
REPL debugging
Ephemeral debug container (audited)
Table 18 — No SSH
6.13 Worked example — dependency isolation
Practice
Detail
Distroless/chiselled JRE base images
No system dependencies
Table 19 — Reproducible
6.14 Worked example — dev parity for AWS services
Service
Local substitute
S3, SQS, DynamoDB
LocalStack in Testcontainers
Table 20 — Closer to prod
6.15 Worked example — limitations
Limitation
Modern view
Env vars for large/complex config
Mounted files acceptable
Stateful workloads
Use managed stateful services or operators
Table 21 — Interpret, don't dogmatise
6.16 Worked example — interview follow-ups
Follow-up
Answer
"Are Spring profiles a violation?"
Only if they encode environment values; use them for structure
"Where do secrets go?"
Secret manager injected at runtime
Table 22 — Common probes
6.17 Worked example — checklist
Item
✔
Config external + validated
✔
Stateless
✔
Build once
✔
Fast, graceful lifecycle
✔
Parity tests
✔
Logs to stdout
✔
Table 23 — Twelve-factor baseline
6.18 Worked example — NovaBank modernisation roadmap
Quarter
Factors addressed
Q1
III, XI
Q2
VI, VII
Q3
IX, X
Table 24 — Incremental
6.19 Worked example — config change without deploy
Most twelve-factor violations creep in through convenience. Environment URLs end up in Spring profiles committed to the repository, so each environment needs its own build and configuration drifts. Secrets land in application.yaml in Git and leak. A service writes temporary files, sessions or job state to local disk, which disappears when a pod is rescheduled. Tests use H2 or mocks instead of the real database, so parity gaps reach production. Startup grows to a minute or more as dependencies accumulate, which quietly breaks autoscaling during traffic spikes. Catch these with automation rather than reviews: gitleaks for secrets, a lint rule against file appenders, startup time measured in CI and a compliance scorecard for every service.
8Performance & Production Considerations
Consideration
Practice
Startup
Measured in CI
Config reload
Rolling restarts
Log volume
Levels
Table 32 — Operating twelve-factor apps
Twelve-factor services behave well on a platform only if their lifecycle is fast and predictable. Measure time to readiness in CI and fail the build when it regresses beyond an agreed budget, because slow starts make scaling and rollouts slow. Changing environment configuration means restarting processes, so roll ConfigMap changes out with a rolling restart, and use a flag service for values that must change instantly, such as rate limits. Logging to stdout moves cost to the platform: control volume with levels and sampling, use structured JSON, and never log secrets or personal data. Size the termination grace period to cover the slowest in-flight request plus the load balancer's deregistration delay, so shutdowns never cut requests off.
9Architecture & Design Considerations
Guideline
Detail
Platform contract
Factors
Extend with security/telemetry
Modern needs
Modernise incrementally
Legacy
Table 33 — Design guidance
10Interview Questions & Answers
Q1BasicConcept
What is the Twelve-Factor App?
The Twelve-Factor App is a methodology, published by Heroku engineers in 2011, for building software-as-a-service applications that are portable across environments, deployable continuously and scalable horizontally without architectural change. It sets out twelve practices: one codebase, explicit dependencies, config in the environment, backing services as attached resources, separate build, release and run stages, stateless processes, port binding, scaling through the process model, disposability, dev/prod parity, logs as event streams and admin tasks as one-off processes. Its real value is as a contract between an application and its platform: if a Spring Boot service follows it, Kubernetes can build, configure, scale, replace and observe it automatically, with no special handling.
Q2BasicConcept
Where should configuration live?
Configuration that varies between environments, such as URLs, credentials, feature settings and resource sizes, should live in the environment, not in the code or the build artifact. Then the same image moves unchanged from test to production and only the injected configuration differs. On Kubernetes that means ConfigMaps exposed as environment variables or mounted application.yaml files, with secrets coming from a secret manager through External Secrets, never stored in Git or baked into images. In Spring Boot, bind it to @ConfigurationProperties records with @Validated, so a bad value fails at startup. Profiles are fine for structure but not for environment values; ShopNow ended three builds per release by moving URLs out of the jar.
Q3IntermediateInternals
What does build, release, run mean?
It means three strictly separate stages. Build turns a commit into an immutable artifact, here a container image identified by its digest, built once by CI. Release combines that exact artifact with environment-specific configuration, for example the digest plus a production overlay committed to the GitOps repository, and every release is versioned and traceable. Run executes the release as processes, pods on Kubernetes, without changing code or config. Because nothing is modified at run time or rebuilt per environment, what was tested is what runs, any release can be traced to its commit, and rolling back simply means deploying a previous release. Patching code inside a running container breaks the model.
Q4IntermediateConcept
Why stateless processes?
Because instances that hold no unique state can be added, removed or replaced at any time without losing data or breaking user sessions. Twelve-factor processes are stateless and share nothing; anything that must outlive a request goes to a backing service. HTTP sessions become stateless JWTs or Spring Session on Redis, uploaded files go to S3, carts and other data go to DynamoDB or a database, scheduled job locks use ShedLock or Kubernetes CronJobs, and in-flight work sits in queues with idempotent consumers. Local caches are fine only if losing them is harmless. ShopNow learned this when in-memory carts vanished as the autoscaler removed pods after a sale; moving carts to DynamoDB fixed it.
Q5Senior EngineerCode
How do you achieve dev/prod parity in Java?
Run the same image everywhere and test against the same backing services as production, not lightweight substitutes. In Spring Boot tests, Testcontainers starts real dependencies in Docker, for example PostgreSQL 17 to match the production major version, Kafka and Redis, and the @ServiceConnection annotation wires their connection details into the application context automatically. For AWS services, LocalStack in Testcontainers emulates S3, SQS and DynamoDB closely enough for integration tests. Avoid H2 as a stand-in for PostgreSQL; ShopNow had a query that passed on H2 but failed in production because of case sensitivity. Keep the time, people and tools gaps small as well: deploy often, let developers deploy, and use identical manifests across environments.
Q6Senior EngineerInternals
What is disposability?
Disposability means processes can be started or stopped at a moment's notice, which is what lets a platform scale, deploy and recover automatically. It has two halves. Fast startup keeps autoscaling and rollouts useful; for Java that means trimming classpath scanning and using the JDK AOT cache, or CRaC checkpoints, to reach readiness in seconds. Graceful shutdown means that on SIGTERM the service stops accepting new work, finishes in-flight requests within the termination grace period and releases its resources; Spring Boot supports this with server.shutdown=graceful. Because crashes still happen, work must also be safe to repeat: idempotent handlers, Kafka offsets committed after processing, and messages redelivered rather than lost.
Q7Technical ArchitectDesign
How are admin processes run on Kubernetes?
As Kubernetes Jobs that use the same image and the same configuration as the application, so a migration or backfill runs exactly the code and settings that production runs. The note's example runs a Flyway migration as an Argo CD PreSync hook Job pinned to the image digest, with envFrom pulling in the service's ConfigMap and Secret, backoffLimit 0 and restartPolicy Never, so a failed migration stops the sync instead of retrying silently. Recurring tasks become CronJobs. Interactive investigation uses audited ephemeral debug containers rather than SSH into a server. The anti-pattern is running admin scripts from a laptop or a special admin host, with different code, credentials and dependencies.
Q8Technical ArchitectTricky
What's missing from the original twelve factors?
The original factors were written for a single platform and a mostly HTTP world, so several concerns that matter today are missing. API first: contracts defined up front so teams can work in parallel. Telemetry: metrics and traces, not just logs, because distributed systems cannot be diagnosed without them. Security: authentication, authorisation, least-privilege identities and secret management, which the original text barely covers. Health: liveness and readiness probes, so orchestrators know when to route traffic to a process or restart it. Graceful degradation and resilience patterns for calls between services matter too. Kevin Hoffman's Beyond the Twelve-Factor App adds most of these, and modern platform checklists treat them as equally mandatory.
Q9Solution ArchitectScenario
How do you modernise a legacy app?
Assess the application against each factor, then fix violations incrementally in order of impact, not all at once. NovaBank's path is typical. Config and logs came first, moving per-environment properties files into ConfigMaps and Secrets Manager and replacing Log4j file appenders with JSON on stdout, because together they unblock containerisation. Then state and port binding: stateless JWTs instead of session replication, and a Spring Boot executable jar instead of a WebLogic WAR. Then disposability and parity, with faster startup, graceful shutdown and Testcontainers, while nightly batch moved to CronJobs or AWS Batch. Score each service on a compliance scorecard, deliver changes quarter by quarter, and keep the application running in production throughout.
Q10Solution ArchitectTrade-off
Is twelve-factor still relevant?
Yes, though it should be applied pragmatically rather than as dogma. The factors became the default assumptions of container platforms: Kubernetes expects configuration injected at run time, stateless replicas it can reschedule, a fast and graceful lifecycle, logs on stdout and one-off Jobs, and serverless platforms such as Lambda assume the same. A service that ignores them fights its platform. Some guidance has aged: large or complex configuration is better mounted as files than spread across environment variables, and stateful workloads run well on managed services or operators. The methodology also needs the later additions of telemetry, security and health. Used as a checklist for the application-platform contract, it remains highly relevant.
Newsletter
Get new tutorials every week
New ArchitectMind tutorials and interview questions, once a week. No spam, unsubscribe anytime.
Your subscription could not be saved. Please try again.
Almost done! Check your inbox and click the confirmation link to complete your subscription.
Keep going in Book 9
Cloud, Containers, DevOps & Production Architecture