Developers all around the world adopt one or more topics that together form the 12 Factor App principles. They already changed the way they build their software. Since this has great advantages in the cloud-native ecosystem, those companies they work for manage to push features faster and more reliable. The official 12 factor website explains what the factors mean in detail. It’s wise to read these web pages prior to this article so you have a good understanding of what 12 factor really is all about. In this article, we’ll explore them in more detail and highlight the main advantages. Benefits when applying the 12 Factor App principles (part 1).
Every software architect that builds modern software as well as (cloud-native) developers and Ops engineers should understand what 12 factor means. The purpose of 12 factor is to build software as a service using proven “ways of working” that focus on speed and reliability. This ensures the applications are highly portable, can be deployed on modern cloud platforms, run on environments that are as similar as possible, and can scale up without much hassle. There is no need to learn a specific programming language since the principles apply to most of them.
Factor 1: Codebase
Perhaps, the first factor is the most important: one codebase for each application. This single codebase is used to do many deployments. This means that every application (component) has its own version and thus life-cycle management. The most important benefit here is to avoid lengthy discussions between subject matter experts. Source code is stored in Subversion, Mercurial, or Git and is not shared with other applications. Everything that the application requires is stored in the codebase: the actual business logic as well as provisioning scripts and configuration settings. Nothing should be stored outside it.
Modern applications use distributed source code control systems like Git. You can use a variety of Git solutions for your applications like Github, Gitlab, Azure Git repositories, or CodeCommit in AWS. Every CI/CD tooling solution like Jenkins, Azure DevOps, or CodePipeline can pull your sources from these repositories to deploy your application multiple times. It’s possible to run a different version of your application in Development compared to Production. But all should come from a single codebase.
Clarity is one of the biggest benefits. Source code and applications can be linked to each other one by one. Developers are not confused about finding out which piece of source code belongs to which application. Every application (component) has its own specialization and there are no features that distract from this principle. It helps to keep applications clean, fast, and easy to refactor.
This principle helps to eliminate confusion about which version of which application runs in which environment. Even better: a running instance can be tracked down to the source code from which it stems. Auditors are happy since this brings organizations “in control” and less time is lost finding the actual source code which forms the heart of everything.
Factor 2: Dependencies
This factor is linked to the previous one. While everything that “makes up the application” is stored in the Source code control system, external dependencies should not. Only store source code that is unique and relevant for the (inner) workings of the application in source code control systems. This means that you should avoid storing duplicate code and (external) dependencies. Besides this, also separate dependencies that are unique for each environment to which the application is deployed.
The main benefits of storing your dependencies externally are:
- Keep the codebase small. Imagine storing a bunch of binaries of 100+ kb. Together they make a significant part of the total size of your repository. It gets even worse when you replace the current versions with new versions since every (new) version will be stored in history as well. Then every dependency eats away even more disk space. Your application becomes slower to checkout for every new build.
- Keep a history of the dependencies you need in a dependency manifest such as package.json, a NuGet file, or a Gradle properties file. It’s more difficult to keep a good history of the dependencies you need when you embed them directly in your source code repository since this is not in a structured format.
- Diffs are only possible based on structured files like plain text files, XML files, or other formats that allow a line-by-line comparison. Binary files like Jars, DLLs and other binary formats cannot be compared (easily). Therefore it’s impossible to track those changes down.
Developers do not need to take care of the dependencies in their applications. They can rely on the dependency manifest so they do not need to worry about cluttered applications which mix business rules and third-party solutions which can be seen as supportive to the application.
Another key aspect to keep in mind that contradicts the previous factor is to include any dependency which works on the Operating System level. For example Curl or Wget to fetch information from other hosts. If that feature is needed, you need to build it into the application to make sure it works (as expected) on every system.
Factor 3: Configuration
Application-related configuration should not be stored in the source code of the application. It should be strictly separated to make sure that the actual source code that makes up the application remains the same for every environment in which it is deployed. If you follow this principle, you can be sure that your application behaves the same in every environment (considering every environment is provisioned equally).
The following pieces of configuration are relevant here:
- Host names that dictate the target environment
- Database credentials or service principles for Cloud-native services
- Service tokens to connect to other external services
- Parameters that vary between environments
On the official 12 factor website, you can read that it should be possible to open source an application at any given time. If you won’t leak any credentials by then, you did your homework correctly.
Two more benefits are gained through this principle:
- Grouping of “environment-related” variables is a no-go. This method does not scale well enough to support the scaling of the application and the wide variety of deployment environments that need to be supported.
- Store configuration into Environment variables. These files can be Operating System agnostic so you can run your application on multiple systems without changing the configuration format.
Some examples of external configuration files are Kubernetes manifests or Docker compose files. Ruby uses Gem files to store its configuration. All of these formats are easy to read and change.
Factor 4: Backing services
Cloud-native applications tend to be as flexible as possible. Not only from a “business feature” perspective to react to external changes or customer demand, but also from a responsibility perspective. Every component (remember micro-services) and every piece of source code should be loosely coupled with each other. Tight integrations are not welcome here. The same is true for external services.
External services are also called “backing services”. This includes (connections to) databases, message queues, storage systems, and other services which are accessible through (standard) APIs. For 12 Factor Apps, it does not matter if these services operate in a local system or a remote system. Both types should be treated equally.
Most significant benefits
The main benefits of treating all of these services, in the same manner, are as follows:
- A uniform way to handle these services helps to ease the setup and re-usability of the various components which make up the application or system. This speeds up development time.
- Developers should be able to swap a (local) service for another one without changing the actual source code (of the business logic). Your application only faces a short period of downtime while the underlying backing service changes. For example, swap a self-hosted MySQL service with a MariaDB service in the Cloud (think of a drop-in replacement).
- It’s important that back-end services have their own lifecycle, independently of the application. A change in the back-end service does not have a (negative) impact on the application itself. Consider backing services as attached/independent resources to avoid tight coupling of them.
- Backing services are maintained by developers for developers, not by system operators. They do not know the inner workings of the application.
End of part 1
So far we’ve seen already 4 out of 12 principles of the 12 Factor app. In our next article, we’ll cover the other principles to conclude the list.
If you have questions related to this topic, feel free to book a meeting with one of our solutions experts, mail to firstname.lastname@example.org.