DEV Community

Implementing the Third Principle of the 12-Factor App in Spring Boot and Kubernetes

Introduction

In this article, we will implement the third principle of the 12-Factor Application methodology in a Spring Boot and Kubernetes environment. Doing so allows us to avoid common problems such as:

  • Applications running with incorrect configuration for a specific environment
  • Delegating the responsibility for correct configuration to the DevOps role rather than developers
  • Minimizing risk related to misconfiguration

When working across multiple environments with microservices, a common anti-pattern causes frequent issues — particularly in production. This pattern consists of assuming that everything has been compiled correctly with the right configuration. Instead, this tutorial demonstrates how to properly implement the third principle of the 12-Factor App methodology.

As stated in the 12-Factor App documentation:

"The twelve-factor app stores config in environment variables (often shortened to env vars or env). Env vars are easy to change between deploys without changing any code; unlike config files, there is little chance of them being checked into the code repo accidentally; and unlike custom config files, or other config mechanisms such as Java System Properties, they are a language- and OS-agnostic standard."


Prerequisites

  • A running Kubernetes cluster. If you do not have one, you can run a local cluster using tools such as kind or minikube.
  • One or more Spring Boot applications deployed to Kubernetes.

Problem

Consider a business with two applications: a users service and a transactions service. Suppose there is a specific configuration for a development (dev) environment and sensitive configuration for a production (prod) environment.

Common Workaround

A typical workaround is to define multiple application.properties profiles in Spring Boot, one per environment, as shown below:

.
├── application.properties
├── dev-application.properties
└── prod-application.properties
Enter fullscreen mode Exit fullscreen mode

A variable is then defined in application.properties and its value is changed manually each time a build is required for a specific environment:

spring.profiles.active=dev
Enter fullscreen mode Exit fullscreen mode

Pitfalls of this approach:

This solution requires a separate compilation per environment. If, for example, there are four environments (DEV, QA, TEST, and PROD) and five microservices, the team must perform twenty compilations instead of five. More critically, this approach is error-prone: a service could be compiled for the dev environment and accidentally deployed to production, resulting in poor customer experience or financial loss.


Best Practice

The recommended approach is to store environment-specific configuration within the environment itself and have the application consume it at runtime. While Spring Boot provides a Config Server, this introduces additional infrastructure costs — particularly in cloud architectures where CPU and memory resources are at a premium. For Kubernetes-native microservices, there is a simpler and more cost-effective solution.

Overview of the solution:

  1. Define an application.properties file with the values for the target environment (e.g., dev).
  2. Transform this file into a Kubernetes ConfigMap.
  3. Modify the Deployment manifest to mount the application.properties file at the path /config/.
  4. Configure Spring Boot to load its configuration from this mounted path instead of the compiled artifact.

Detailed Steps

Step 1: Define the ConfigMap

Create a properties file with the desired configuration for your environment:

spring.profiles.active=dev
server.port=8083

# JPA configuration
spring.jpa.database=POSTGRESQL
spring.jpa.hibernate.ddl-auto=auto
spring.jpa.properties.hibernate.default_schema=public
spring.jpa.generate-ddl=false

# SQL initialization configuration
spring.sql.init.platform=postgres
Enter fullscreen mode Exit fullscreen mode

Then create a Kubernetes ConfigMap from the file:

kubectl create configmap application-properties --from-file=application.properties
Enter fullscreen mode Exit fullscreen mode

Step 2: Configure the Deployment Manifest

According to the official Spring Boot documentation, it is possible to define an external configuration location using the SPRING_CONFIG_LOCATION environment variable. When this variable is set, Spring Boot will ignore the compiled artifact application.properties and instead load configuration from the specified path.

Additionally, the application.properties file from the ConfigMap must be mounted into the container using a Kubernetes volume. The following Deployment manifest illustrates this configuration:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-transactions-app
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: api-transactions-app
  template:
    metadata:
      labels:
        app: api-transactions-app
    spec:
      containers:
      - name: api-transactions-app
        image: examplebusiness/api-transactions:latest
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8080
          protocol: TCP
        env:
        - name: SPRING_CONFIG_LOCATION
          value: file:/app/conf/transactions-additional.properties
        volumeMounts:
        - mountPath: /app/conf/
          name: application-properties
      volumes:
      - name: application-properties
        configMap:
          name: application-properties
Enter fullscreen mode Exit fullscreen mode

With this configuration in place, every time the application starts — whether in development or production — it will load the environment-specific configuration that was injected at the infrastructure level.


Pros and Cons

This approach migrates the configuration pipeline from an error-prone process to a safer, more reliable workflow. However, there are trade-offs to consider.

Advantages:

  • A single compiled artifact is used across all environments, eliminating redundant builds.
  • Configuration is decoupled from the application code, reducing the risk of environment-specific values being committed to the repository.
  • Responsibility for environment configuration is clearly delegated to the infrastructure layer.

Disadvantages:

  • Configuration is mounted at container startup. Therefore, any change to the ConfigMap requires a pod restart, which may result in brief downtime depending on the application's restart time.

Fortunately, this limitation can be addressed by using Spring Cloud Kubernetes PropertySource Reload, which enables dynamic configuration refresh without requiring a full pod restart.

Top comments (0)