12 Factor App

Md Faizan AlamMd Faizan Alam
11 min read

Imagine your app growing wildly popular — millions of users! But it crashes every time new users join. Not ideal, right? That’s where scalability comes in — building software that handles growth gracefully.

Now, maintaining a complex app can feel like untangling a massive knot. That’s where maintainability shines, keeping things organized and easy to fix.

The 12 Factor App methodology tackles both these challenges, along with a third — portability. Think of it as building an app that can run on any platform, like a phone app that works seamlessly on different phone models. Let’s explore these principles and see how they make your app development journey smoother and more efficient.

What is a 12 Factor App?

A 12 Factor App is a set of guidelines that helps developers build software like Lego sets. Imagine separate blocks for the code, database, and configurations. With 12 Factor, each part is clearly defined and independent. This makes your app:

  • Scalable: Easy to grow as your user base increases, like adding more Lego bricks to build a bigger structure.

  • Maintainable: Simple to fix and update, because each part is like a labeled Lego brick, easy to find and replace.

  • Portable: Able to run on different platforms, like your Lego creation working at home or a friend’s house.

By following these guidelines, you create well-structured, adaptable applications that are a breeze to develop and maintain in the long run.

The 12 Factors

Now that you have read so much about the benefits of the 12 Factor App, lets see what are the 12 Factors that make this methodology work.

Credits — Ashish Kumar Singal

We’ll go through all these 12 factors and learn them by implementing them in a simple nodejs server code below:

const http = require('http');

const hostname = '127.0.0.1';
const port = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

server.listen(port, hostname, () => {
  console.log(`Server running at http://${hostname}:${port}/`);
});

1. Codebase

Our adventure begins with the first factor: Codebase. Every great app starts with organized code. We’ll structure our Node.js server into a clear codebase, separating concerns and ensuring scalability.

// server.js
const http = require('http');

const hostname = '127.0.0.1';
const port = process.env.PORT || 3000;

const app = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

module.exports = app;

In this updated version, we’ve separated our server logic into a module, server.js, making it easier to manage and maintain our codebase.

2. Dependencies

Next up is Dependencies. Managing dependencies is crucial for reproducibility and stability. This factor emphasizes the importance of explicitly declaring and isolating your dependencies. This means that you should specify the exact versions of the libraries and packages that your application depends on. You should also isolate your dependencies so that they do not affect each other.

Keep a package.json file where all your dependencies and their respective versions are defined.

3. Config

Configurations play a vital role in flexibility and environment adaptation. Let’s enhance our server by externalizing configuration settings.

// server.js
const http = require('http');

const hostname = process.env.HOST || '127.0.0.1';
const port = process.env.PORT || 3000;

const app = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

module.exports = app;

By allowing environment variables to dictate our server’s configuration, we’ve made our code more adaptable across different environments.

4. Backing Services

Backing Services are the backbone of our application. The fourth factor asks us to treat these services as attached resources. This means that your application should connect to these services using configuration settings, rather than hardcoding them into the code.

You can easily swap out or scale these services without having to make changes to your application’s codebase. Let’s integrate a database service into our Node.js server.

// server.js
const http = require('http');
const mysql = require('mysql');

const connection = mysql.createConnection({
  host: process.env.DB_HOST,
  user: process.env.DB_USER,
  password: process.env.DB_PASS,
  database: process.env.DB_NAME
});

const hostname = process.env.HOST || '127.0.0.1';
const port = process.env.PORT || 3000;

const app = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

connection.connect((err) => {
  if (err) {
    console.error('Error connecting to database: ' + err.stack);
    return;
  }
  console.log('Connected to database as id ' + connection.threadId);
});

module.exports = app;

We’ve introduced a MySQL database as a backing service, allowing our server to interact with persistent data.

5. Build, Release and Run

The Build, Release, Run cycle ensures consistent and reliable deployments. Let’s automate this process using npm scripts. The build stage transforms your code into executable artifacts, the release stage combines these artifacts with configuration, and the run stage initiates the application.

This separation ensures that your application can be deployed and run in any environment, regardless of the underlying infrastructure. It also makes it easier to automate the deployment process, which can help to improve consistency and repeatability.

The following are particular instances of how you can incorporate the fifth factor into your application:

1. To automate the build process, utilise a build tool such as Jenkins. This will assist you in guaranteeing the consistency and repeatability of the build process.

2. To manage the configuration of your application, use a configuration management tool like OCP’s Config Map or AWS’s SSM. You can keep the configuration and the code apart by doing this.

3. Package your application and its dependencies into a container using a containerisation tool such as Docker. This will assist you in creating a scalable and portable application.

4. Lastly, remember to keep those Docker images in an Artifactory.

6. Processes

Processes are isolated, stateless, and share-nothing components of our application. In the simplest case, the code is a stand-alone script, the execution environment is a developer’s local laptop with an installed language runtime, and the process is launched via the command line (for example, python my_script.py). On the other end of the spectrum, a production deploy of a sophisticated app may use many process types, instantiated into zero or more running processes.

Twelve-factor processes are stateless and share-nothing. Any data that needs to persist must be stored in a stateful backing service, typically a database. Let’s enhance our server to utilize process management.

// server.js
const cluster = require('cluster');
const http = require('http');
const os = require('os');

const numCPUs = os.cpus().length;

if (cluster.isMaster) {
  console.log(`Master ${process.pid} is running`);

  // Fork workers.
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }

  cluster.on('exit', (worker, code, signal) => {
    console.log(`Worker ${worker.process.pid} died`);
  });
} else {
  // Workers can share any TCP connection
  // In this case, it is an HTTP server
  const app = require('./server');

  const server = http.createServer(app);

  const port = process.env.PORT || 3000;
  server.listen(port, () => {
    console.log(`Worker ${process.pid} started listening on port ${port}`);
  });
}

By utilizing the cluster module, our server can take advantage of multiple CPU cores, enhancing performance and scalability.

7. Port Binding

The twelve-factor app is completely self-contained and does not rely on runtime injection of a webserver into the execution environment to create a web-facing service. The web app exports HTTP as a service by binding to a port, and listening to requests coming in on that port.

In a local development environment, the developer visits a service URL like http://localhost:5000/ to access the service exported by their app. In deployment, a routing layer handles routing requests from a public-facing hostname to the port-bound web processes.

// server.js
const http = require('http');

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

const app = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

module.exports = app;

if (!module.parent) {
  app.listen(port, () => {
    console.log(`Server running at http://localhost:${port}/`);
  });
}

By checking if the module is the main module (module.parent), we ensure our server only binds to a port when executed directly, improving flexibility and encapsulation.

8. Concurrency

Once executed, a computer programme is represented by one or more processes. Web applications come in a range of forms for process execution. For instance, PHP processes are started and stopped as needed based on the volume of requests, and they operate as Apache’s children. In contrast, Java processes use the JVM to provide a single, massive uberprocess that reserves a significant amount of memory and CPU at startup. Threads are used internally to manage concurrency. In both scenarios, the app developers have very limited visibility into the running process or processes.

// server.js
const http = require('http');
const { Worker, isMainThread, parentPort } = require('worker_threads');

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

const app = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

module.exports = app;

if (isMainThread) {
  app.listen(port, () => {
    console.log(`Server running at http://localhost:${port}/`);
  });
} else {
  parentPort.postMessage('ready');
}

By leveraging Worker Threads, our server can handle concurrent operations efficiently, improving scalability and responsiveness.

9. Disposability

The twelve-factor app’s processes are disposable, meaning they can be started or stopped at a moment’s notice. This facilitates fast elastic scaling, rapid deployment of code or config changes, and robustness of production deploys. The goal of processes should be to reduce startup time. The launch command should ideally only take a few seconds to execute, after which the process is up and ready to accept requests or jobs.

When a process manager sends out a SIGTERM signal, the process gracefully ends. A web process can be gracefully terminated by stopping to listen on the service port, which will reject any incoming requests, letting any pending requests expire, and then closing down. This model implicitly states that HTTP requests should be brief — no more than a few seconds — or, if polling takes a long time, that the client should automatically try to reconnect in the event that the connection is lost.

// server.js
const http = require('http');

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

const app = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello World\n');
});

module.exports = app;

if (!module.parent) {
  const server = app.listen(port, () => {
    console.log(`Server running at http://localhost:${port}/`);
  });

  process.on('SIGTERM', () => {
    console.info('SIGTERM signal received.');
    console.log('Closing server gracefully.');
    server.close(() => {
      console.log('Server closed.');
    });
  });
}

10: Dev/Prod Parity

Dev/Prod Parity ensures consistency between development and production environments, minimizing surprises and issues during deployment.

Historically, there have been substantial gaps between development (a developer making live edits to a local deploy of the app) and production (a running deploy of the app accessed by end users). These gaps manifest in three areas:

  • The time gap: A developer may work on code that takes days, weeks, or even months to go into production.

  • The personnel gap: Developers write code, ops engineers deploy it.

  • The tools gap: Developers may be using a stack like Nginx, SQLite, and OS X, while the production deploy uses Apache, MySQL, and Linux.

The twelve-factor app is designed for continuous deployment by keeping the gap between development and production small. Looking at the three gaps described above:

  • Make the time gap small: a developer may write code and have it deployed hours or even just minutes later.

  • Make the personnel gap small: developers who wrote code are closely involved in deploying it and watching its behavior in production.

  • Make the tools gap small: keep development and production as similar as possible.

11. Logs

Logs are the stream of aggregated, time-ordered events collected from the output streams of all running processes and backing services. Logs in their raw form are typically a text format with one event per line (though backtraces from exceptions may span multiple lines). Logs have no fixed beginning or end, but flow continuously as long as the app is operating.

A twelve-factor app never concerns itself with routing or storage of its output stream. It should not attempt to write to or manage logfiles. Instead, each running process writes its event stream, unbuffered, to stdout. During local development, the developer will view this stream in the foreground of their terminal to observe the app’s behavior.

In staging or production deploys, each process’ stream will be captured by the execution environment, collated together with all other streams from the app, and routed to one or more final destinations for viewing and long-term archival. These archival destinations are not visible to or configurable by the app, and instead are completely managed by the execution environment. Open-source log routers (such as Logplex and Fluentd) are available for this purpose.

12. Admin Processes

Admin processes in the context of the 12 Factor App methodology refer to having separate functionalities or interfaces dedicated to administrative tasks related to your application.

Let’s say you have a Node.js server that runs your main application, handling user requests, processing data, and serving content. However, along with your main application, you might also need specific functions or tools for administrative purposes, like monitoring server health, managing user accounts, or analyzing usage statistics.

To implement admin processes, you would create a separate module or server specifically for these administrative tasks. This admin module would have its own routes, endpoints, or interfaces designed solely for administrative actions. For example, you might have routes like /admin/monitor for checking server health, /admin/users for managing user accounts, or /admin/analytics for viewing usage statistics.

Combining everything at once

The purpose of the 12 Factor App principles is to enable developers to design web-scale applications. At first, they may appear overwhelming, and in many respects, they are. Rethinking the fundamentals of software development can be a difficult undertaking.

Thankfully, applying The 12 Factor App’s principles is not a black-or-white decision. You can take them in small, easily absorbed portions, working your way through the first one first. Making the decision to abide by the principles and then making the initial move is the tricky part. Its just for you to take the first step.


Thank you for reading! If you have any feedback or notice any mistakes, please feel free to leave a comment below. I’m always looking to improve my writing and value any suggestions you may have. If you’re interested in working together or have any further questions, please don’t hesitate to reach out to me at fa1319673@gmail.com.

10
Subscribe to my newsletter

Read articles from Md Faizan Alam directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Md Faizan Alam
Md Faizan Alam

I am a Fullstack Developer from India and a Tech Geek. I try to learn excting new technologies and document my journey in this Blog of mine. I try to spread awareness about new and great technologies I come across or learn.