The 12-Factor App Methodology

Mikaeel KhalidMikaeel Khalid
6 min read

Introduction

In today's fast-paced world, building robust, scalable, and maintainable applications is crucial for success. The 12-Factor App methodology, developed by engineers at Heroku, provides a set of best practices for building cloud-native applications. By following these principles, developers can create applications that are portable, scalable, and resilient. In this blog post, we will explore each of the 12 factors in detail, accompanied by code examples to illustrate their implementation.

The Twelve Factors

  1. Codebase: Maintain a single codebase for the application, stored in a version control system such as Git. This ensures consistency and allows for easy collaboration among developers. Having a single codebase reduces complexity and makes it easier to track changes and rollbacks.

Example: When starting a new project or joining an existing one, clone the repository to your local machine using the following command:

$ git clone <repository-url>
  1. Dependencies: Explicitly declare and isolate dependencies. Utilize a dependency management system appropriate for your programming language (e.g., npm for JavaScript, pip for Python). This ensures that all required libraries and packages are consistently installed across different environments.

Example (using npm): To install a package and add it to the project's dependencies, run the following command:

$ npm install <package-name> --save
  1. Config: Store configuration settings in the environment and not within the application code. This approach allows for easy configuration changes across different environments and avoids hardcoding sensitive information such as database credentials or API keys. Use environment variables to access these settings within the application.

Example (Node.js): To access a configuration setting for the application's port, use the following code:

const port = process.env.PORT || 3000;

In this example, the application will use the port specified in the environment variable "PORT" if available; otherwise, it will default to port 3000.

  1. Backing Services: Treat backing services (databases, caches, message queues, etc.) as attached resources. Access them via a URL or connection string provided by the environment. This approach promotes loose coupling and allows for easy substitution of different service providers or configurations.

Example (MongoDB connection in Node.js):

const mongoose = require('mongoose');

mongoose.connect(process.env.MONGODB_URI);

In this example, the application connects to a MongoDB database using the connection string specified in the environment variable "MONGODB_URI".

  1. Build, Release, Run: Strictly separate the build, release, and run stages of the application. The build stage compiles source code, compiles assets, and performs any necessary transformations. The release stage takes the build output and combines it with the current configuration to create a release that can be deployed. The run stage executes the released application.

Example (deployment script using Docker):

#!/bin/bash
docker build -t myapp .
docker run -d -p 80:80 myapp

In this example, the script builds a Docker image for the application, tags it as "myapp", and runs it as a detached container, mapping port 80 on the host to port 80 inside the container.

  1. Processes: Design applications to run as stateless processes. Avoid storing the session state or any other form of local state within the application instance. Use external storage (e.g., databases or caches) to manage persistent data.

Example (stateless Express.js route handler):

app.get('/api/users', async (req, res) => {
  const users = await User.find();
  res.json(users);
});

In this example, an Express.js route handler retrieves a list of users from a database (which manages the state) and sends the response back to the client.

  1. Port Binding: Applications should be self-contained and explicitly bind to a port defined by the environment. This allows the application to be easily deployed to different environments without modification.

Example (binding to port in Node.js):

const port = process.env.PORT || 3000;
app.listen(port, () => {
  console.log(`Server running on port ${port}`);
});

In this example, the application binds to the port specified in the environment variable "PORT" or defaults to port 3000 if the variable is not set.

  1. Concurrency: Scale out the application through the process model rather than the thread model. This means that an application should be able to scale horizontally by adding more instances of the entire application, instead of relying on multithreading within a single instance.

Example (concurrency in Python using Gunicorn):

$ gunicorn --workers 4 app:app

In this example, Gunicorn is used to run a Python application with four worker processes.

  1. Disposability: Design applications to start up quickly and shut down gracefully. This allows for easy scaling, and rolling deployments, and minimizes the impact of failures. Use mechanisms provided by the hosting platform or runtime to handle process management.

Example (graceful shutdown in Node.js):

process.on('SIGTERM', () => {
  server.close(() => {
    console.log('Server gracefully shut down');
    process.exit(0);
  });
});

In this example, the application listens for the "SIGTERM" signal, typically sent by the hosting platform during a shutdown, and gracefully shuts down the server.

  1. Dev/Prod Parity: Ensure that the development, staging, and production environments are as similar as possible. Aim for consistency in configuration, dependencies, and other factors. Use tools such as containers or virtualization to achieve parity.

Example (Docker-based development environment): To achieve Dev/Prod Parity using Docker, you can define a Docker Compose file that specifies the development environment for your application. This allows developers to work in an environment that closely resembles the production setup.

Here's an example Docker Compose file for a Node.js application:

version: '3'
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - .:/app
    ports:
      - 3000:3000
    environment:
      - NODE_ENV=development
      - MONGODB_URI=mongodb://mongo:27017/myapp
  mongo:
    image: mongo:latest

In this example, the Docker Compose file defines two services: app and mongo. The app service represents your Node.js application, while the mongo service represents a MongoDB database.

  1. Logs: Treat logs as event streams. Applications should write their log events to stdout or stderr, allowing them to be captured and aggregated by a centralized logging system. Logging helps with troubleshooting, monitoring, and performance analysis.

Example (logging in Node.js using Winston):

const winston = require('winston');

winston.log('info', 'Application started');

In this example, the Winston logging library is used to log an information message indicating that the application has started.

  1. Admin Processes: Run administrative or management tasks as one-off processes, separate from the application code. This ensures that these tasks can be executed independently and avoids unnecessary complexity in the application itself.

Example (running a database migration script):

$ knex migrate:latest

In this example, the command is used to run a database migration script using the Knex.js library.

Conclusion

The 12-Factor App methodology provides a comprehensive set of best practices for building modern, scalable, and maintainable applications. By following these principles, developers can create applications that are portable, scalable, and resilient. The code examples provided here offer a starting point for implementation but keep in mind that the specific implementation may vary based on the programming language, framework, and tools you are using. Embracing the 12 factors will help you build robust and future-proof applications that can thrive in today's fast-paced digital landscape.

You can read more about it here: https://12factor.net

0
Subscribe to my newsletter

Read articles from Mikaeel Khalid directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mikaeel Khalid
Mikaeel Khalid

I am a Software and Certified AWS & GCP Engineer, I am passionate about solving complex technical problems. My goal here is to help others by sharing my learning journey.