The 12-Factor App methodology defines a clean contract between an application and its running environment. By following these twelve principles, teams can build services that are portable, scalable, and continuously deployable without either the application or the infrastructure stepping on each other’s toes.
Originally published by Heroku, the methodology was designed specifically to support Software-as-a-Service offerings. In this article we walk through each of the 12 factors and show how Eclipse MicroProfile and Kubernetes provide concrete tools to satisfy them.
Why the 12-Factor App?
Modern cloud-native applications must handle frequent deployments, elastic scaling, and diverse runtime environments. The 12-Factor App methodology answers these challenges with a set of best practices that help teams:
- Use declarative formats for setup automation, reducing onboarding time for new developers.
- Maintain a clean contract with the OS, maximising portability across execution environments.
- Design for deployment on modern cloud platforms, removing the need for bespoke server administration.
- Keep development and production as similar as possible, enabling continuous deployment.
- Scale horizontally without major changes to tooling, architecture, or development practices.
Eclipse MicroProfile: A Brief Overview
Eclipse MicroProfile is an open-source project and community established by the Eclipse Foundation in 2016. Its members include IBM, Red Hat, Tomitribe, Payara, the London Java Community (LJC), and many others. MicroProfile targets lightweight, iterative development with a rapid release cadence—typically three releases per year.
The project has numerous runtime implementations, including Open Liberty, Quarkus, Payara, TomEE, WildFly, KumuluzEE, Piranha, and others. Each implementation provides the MicroProfile APIs on top of a Jakarta EE or compatible base.
The 12 Factors, One by One
1. Codebase
One codebase tracked in version control, many deploys.
Use a dedicated Git repository per microservice so that each service can evolve and be released independently. Version control enforces discipline: maintaining a single repository per application forces teams to analyse the boundaries of their system and identify parts of a potential monolith that should be extracted into separate microservices.
2. Dependencies
Explicitly declare and isolate dependencies.
Avoid bundling third-party libraries inside your packaged artifact. Instead, declare them explicitly in a build descriptor—pom.xml for Maven or settings.gradle / build.gradle for Gradle. This approach makes version upgrades predictable and keeps the artifact lean.
3. Config
Store configuration in the environment.
The golden rule is: write once, configure everywhere. Configuration that changes between deployments (database URLs, credentials, feature flags) must be externalised from application code so that you never need to repackage a service just to change a property.
MicroProfile Config lets you read properties from system properties, environment variables, or custom config sources such as a Kubernetes ConfigMap—without changing application code.
Two access patterns are supported:
// Programmatic lookup
Config config = ConfigProvider.getConfig();
String value = config.getValue("my.string.property", String.class);
// CDI injection
@Inject
@ConfigProperty(name = "my.string.property")
String myPropV;Code language: JavaScript (javascript)
Because the property can be backed by a Kubernetes ConfigMap or a plain environment variable, updating it does not require rebuilding or redeploying the container image.
4. Backing Services
Treat backing services as attached resources.
Databases, message brokers, and external APIs should be swappable by changing configuration alone. MicroProfile Rest Client lets you model a backing service as a typed Java interface annotated with @RegisterRestClient, and then bind its URL via a config property.
// 1. Declare the interface
@Dependent
@RegisterRestClient
@RegisterProvider(UnknownUrlExceptionMapper.class)
@Path("/properties")
public interface SystemClient {
@GET
@Produces(MediaType.APPLICATION_JSON)
Properties getProperties() throws UnknownUrlException, ProcessingException;
}
// 2. Inject it where needed
@Inject
@RestClient
private SystemClient defaultRestClient;
// 3. Bind the URL in config (e.g., microprofile-config.properties or ConfigMap)
// com.acme.SystemClient/mp-rest/url=http://localhost:9080/systemCode language: JavaScript (javascript)
Swapping the backing service only requires updating the binding property value, provided the replacement service implements the same interface.
5. Build, Release, Run
Strictly separate build and run stages.
- Build stage: Compile source code and produce an artifact — for example,
mvn packageproduces a JAR. - Release stage: Combine the build artifact with environment-specific configuration and produce a versioned container image — for example,
docker buildfollowed by pushing to a registry. - Run stage: Execute the immutable release image in the target environment — for example,
docker runlocally orkubectl apply -f deployment.yamlin Kubernetes.
Each release should be uniquely identified and immutable. You never modify a running release; you create a new one.
6. Processes
Execute the app as one or more stateless processes.
Microservices should be stateless so that the infrastructure can destroy and recreate instances without data loss. REST is a widely adopted stateless protocol. JAX-RS, included in MicroProfile, makes it straightforward to build RESTful services:
@ApplicationPath("system")
public class SystemApplication extends Application {}
@Path("properties")
public class PropertiesResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
public JsonObject getProperties() { /* ... */ }
}Code language: PHP (php)
This exposes the endpoint http://host:port/system/properties as a stateless RESTful service. Any persistent state belongs in a backing service (factor 4), not in process memory.
7. Port Binding
Export services via port binding.
In cloud environments, service ports change frequently. MicroProfile Config combined with MicroProfile Rest Client handles this cleanly. Store the service URL in a Kubernetes ConfigMap and reference it via the mp-rest/url config property. MicroProfile Config picks up the value automatically at runtime.
Note that Kubernetes environment variable names cannot contain dots (.) or forward slashes (/). Replace non-alphanumeric characters with underscores when mapping property names to environment variables. For example:
# Property name
com.acme.SystemClient/mp-rest/url=http://service-host:9080/system
# Equivalent environment variable name
com_acme_SystemClient_mp_rest_url=http://service-host:9080/systemCode language: PHP (php)
8. Concurrency
Scale out via the process model.
Stateless microservices can be scaled horizontally by running more instances. Kubernetes Horizontal Pod Autoscaler (HPA) and Knative autoscaling can scale deployments up or down automatically based on observed metrics such as CPU utilisation or incoming request rate.
9. Disposability
Maximise robustness with fast startup and graceful shutdown.
Microservices should start quickly and shut down gracefully. They also need to be resilient to transient failures in downstream services. MicroProfile Fault Tolerance provides a set of annotations that make services more robust:
@Retry— automatically retry on transient network failures.@Timeout— fail fast if a response takes too long.@CircuitBreaker— stop calling a failing service to give it time to recover.@Bulkhead— limit concurrent calls to prevent one slow service from exhausting all threads.@Fallback— execute an alternative method when the primary path fails.
The example below shows a JAX-RS endpoint that will respond within 100 ms regardless of back-end latency, retrying on failure and falling back to a default response if necessary:
@Path("/client")
@ApplicationScoped
public class ClientController {
@Inject @RestClient
private Service service;
@Inject @ConfigProperty(name = "conference")
String conf;
@GET
@Path("/test/{parameter}")
@Retry
@Timeout(100)
@Asynchronous
@Fallback(fallbackMethod = "fallback")
public CompletionStage<String> onClientSide(@PathParam("parameter") String parameter) {
return CompletableFuture.completedFuture(
"We are live @ " + conf + "! " + service.doSomething(parameter)
);
}
public CompletionStage<String> fallback(@PathParam("parameter") String parameter) {
return CompletableFuture.completedFuture("This is my fallback!");
}
}Code language: JavaScript (javascript)
10. Dev/Prod Parity
Keep development, staging, and production as similar as possible.
Differences between environments are a leading cause of subtle bugs. Container images built with Docker eliminate the classic “it works on my laptop” problem: the same image runs in a developer’s local environment, in CI, in staging, and in production. Kubernetes manifests further codify the expected runtime configuration so that every environment is defined as code.
11. Logs
Treat logs as event streams.
Applications should write log events to standard output (stdout) and let the execution environment handle routing and storage. In Kubernetes, logs written to stdout/stderr are captured by the container runtime and can be forwarded to centralised log aggregation platforms such as the ELK Stack (Elasticsearch, Logstash, Kibana) or Loki. Emitting logs in structured JSON format makes them easier to query and index.
12. Admin Processes
Run admin and management tasks as one-off processes.
Database migrations, cache warming, and similar administrative tasks should run as separate, short-lived processes rather than being embedded in the main application. Kubernetes Jobs are a natural fit: a Job runs a container to completion and then terminates, keeping administrative logic cleanly separated from service logic.
Summary
The table below maps each of the 12 factors to the primary MicroProfile or Kubernetes feature that addresses it:
| Factor | Tool / Feature |
|---|---|
| 1. Codebase | Git (one repo per microservice) |
| 2. Dependencies | Maven / Gradle |
| 3. Config | MicroProfile Config + Kubernetes ConfigMap |
| 4. Backing Services | MicroProfile Rest Client + MicroProfile Config |
| 5. Build, Release, Run | Maven + Docker + kubectl |
| 6. Processes | JAX-RS (stateless REST) |
| 7. Port Binding | MicroProfile Config + MicroProfile Rest Client |
| 8. Concurrency | Kubernetes HPA / Knative autoscaling |
| 9. Disposability | MicroProfile Fault Tolerance |
| 10. Dev/Prod Parity | Docker images + Kubernetes manifests |
| 11. Logs | stdout + ELK / Loki |
| 12. Admin Processes | Kubernetes Jobs |
Eclipse MicroProfile and Kubernetes together provide a comprehensive toolkit for implementing all 12 factors. MicroProfile handles application-level concerns—configuration, service clients, fault tolerance, and RESTful APIs—while Kubernetes provides the runtime infrastructure for scheduling, scaling, secrets management, and job execution.
Original article sourced from msdynamicsworld.com (2020). Code examples are illustrative and based on the MicroProfile API specifications.