Decoding the 12-Factor App: A Deep Dive into Modern Application Development
12-Factor App is a methodology for building software-as-a-service (SaaS) applications that are cloud-native, scalable, resilient, and portable.
Benefits
- creates applications that are compatible with modern cloud platforms, such as AWS, Azure, Google Cloud Platform
- avoids common pitfalls and errors that can affect the quality, performance, security, and maintainability of their applications
- adopts best practices that promote code readability, modularity, reusability, testability, and documentation
- collaborates more effectively with other developers working on the same codebase, by enforcing a clear contract between the application and the underlying operating system and backing services
- delivers value faster to their customers, by enabling faster feedback loops, shorter release cycles, and easier scaling
High Overview
- Codebase: There should be exactly one codebase for a deployed service with the codebase being used for many deployments
- Dependencies: All dependencies should be declared, with no implicit reliance on system tools or libraries
- Config: Configuration that varies between deployments should be stored in the environment
- Backing services: All backing services are treated as attached resources and attached and detached by the execution environment
- Build, release, run: The delivery pipeline should strictly consist of build, release, run
- Processes: Applications should be deployed as one or more stateless processes with persisted data stored on a backing service
- Port binding: Self-contained services should make themselves available to other services by specified ports
- Concurrency: Concurrency is advocated by scaling individual processes
- Disposability: Fast startup and shutdown are advocated for a more robust and resilient system
- Dev/Prod parity: All environments should be as similar as possible
- Logs: Applications should produce logs as event streams and leave the execution environment to aggregate
- Admin processes: Any needed admin tasks should be kept in source control and packaged with the application
Codebase
Codebase factor focuses on how the application’s code is organized and managed. The goal is to maintain a single codebase that can be deployed across multiple environments with minimal changes.
Best Practices
- Use a version control system (VCS) like Git to manage the codebase. This allows for easy tracking of changes, collaboration among team members, and rollback of changes if necessary
- Maintain a single codebase for the application that is used for deploying to all environments (development, staging, production). This ensures consistency and reduces the likelihood of environment-specific bugs
- Use branches in the VCS for developing new features or making significant changes. Once these are tested and reviewed, they can be merged into the main branch
- Keep the build, release, and run stages separate. The build stage involves compiling code and creating an executable file, the release stage involves combining the executable with the environment configuration, and the run stage involves executing the application in a specific environment
- Use tags in the VCS to mark different versions of the application. This allows for easy rollback to previous versions if necessary
- Document the codebase thoroughly, including its structure, functionality, and any important decisions made during development. This helps developers understand the codebase and contributes to its maintainability
Anti-Patterns
- Don’t maintain separate codebases for different environments. This can lead to inconsistencies and bugs that are hard to track down
- Avoid making changes directly on the production code. Instead, make changes in a separate branch and merge them into the main branch after testing and review
- Don’t ignore failing tests or build errors. These are indicators of problems in the codebase that need to be addressed
- Be cautious when using third-party libraries or frameworks. Ensure they are reliable and well-maintained, and keep them up-to-date
- Don’t hardcode environment-specific configurations into the application. Instead, use environment variables or configuration files that can be easily changed when deploying to different environments
Dependencies
Dependencies factor focuses on how the application relies on other libraries, frameworks, and services to function properly. The goal is to minimize coupling between the application and its dependencies, allowing for easy updates and maintenance.
Best Practices
- Use environment variables to store dependency configurations, rather than hardcoding them into the application. This allows for easy changes without requiring code modifications
- Treat dependencies as first-class citizens by tracking them in the same version control system (VCS) as the rest of the codebase. This ensures that all team members can easily access and update dependencies
- Avoid using global state management systems like Singleton patterns or static classes. Instead, use dependency injection to manage dependencies throughout the application
- Keep dependencies up-to-date and aligned across all environments. This means regularly updating dependencies and ensuring they match across development, staging, and production environments
- Minimize the number of dependencies used by the application. Each dependency adds complexity and increases the likelihood of conflicts or bugs
- Prefer vendoring dependencies over fetching them at runtime. Vendoring allows for easier management of dependencies and reduces the risk of unexpected changes affecting the application
- Document dependencies thoroughly, including their purpose, usage, and potential compatibility issues. This helps developers understand the role each dependency plays and avoid introducing breaking changes
Anti-Patterns
- Don’t rely on implicit dependencies, where the application assumes certain dependencies will always be present without explicitly declaring them. This leads to confusion and makes it difficult to maintain or scale the application.
- Avoid tightly coupling the application to specific versions of dependencies. Instead, define ranges of acceptable versions and use tools like semantic versioning to ensure compatible updates.
- Don’t hardcode API keys, credentials, or sensitive data related to dependencies within the application. Store these values securely outside of the codebase.
- Be cautious when using third-party dependencies, especially those with known vulnerabilities or poor maintenance. Regularly review and assess the security risks associated with external dependencies.
- Don’t rely on system-wide packages that are not included in the application’s dependency declaration. This can lead to inconsistencies between environments and makes it harder to manage and update dependencies
Config
Config factor focuses on how the application manages its configuration settings. The goal is to separate config from code, which is based on the principle that the same codebase should be deployable in multiple environments with different configurations.
Best Practices
- Store config in the environment. This is a model for config that scales up nicely as the app naturally expands into more deploys over its lifetime
- Strict separation of config from code. Config varies substantially across deploys, code does not
- A litmus test for whether an app has all config correctly factored out of the code is whether the codebase could be made open source at any moment, without compromising any credentials
- Use constants for internal settings such as algorithm parameters
- Never use config for mutable data. Config does not vary between deploys, while databases do
Anti-Patterns
- Don’t store config as constants in the code. This is a violation of twelve-factor and is a direct path to fragile/low-quality apps
- Don’t group config variables together as “environments”. Keep development, staging, and production as similar as possible
- Don’t use the same backing services for different deploys. For example, if the staging deploy makes use of a backing service, the production deploy should not point at the same backing service
- Don’t include real (production) credentials in the code or version control. It’s easy to accidentally push a commit that includes them. Instead, access these credentials through environment variables or use a secure identity provider
- Don’t rely on implicit existence of an environment-specific config setting. Instead, always declare all needed environment variables explicitly in a config file or setup script
Backing services
Backing services factor focuses on how the application interacts with services that are treated as attached resources. These can include datastores, messaging systems, SMTP services, and more. The goal is to ensure loose coupling between the application and its backing services, promoting portability across different environments.
Best Practices
- Treat backing services as attached resources that can be replaced without affecting the application’s codebase. This allows for easy changes and maintenance
- Use environment variables to store backing service configurations, rather than hardcoding them into the application. This allows for easy changes without requiring code modifications
- Ensure that the application can connect to backing services over a network, as if they were local to the application. This promotes portability and scalability
- Keep backing services up-to-date and aligned across all environments. This means regularly updating services and ensuring they match across development, staging, and production environments
- Document backing services thoroughly, including their purpose, usage, and potential compatibility issues. This helps developers understand the role each service plays and avoid introducing breaking changes
Anti-Patterns
- Don’t tightly couple the application to specific instances of backing services. Instead, allow for flexibility in connecting to different instances of the same type of service
- Avoid hardcoding credentials or sensitive data related to backing services within the application. Store these values securely outside of the codebase
- on’t assume that all backing services are local or file system-based. Be prepared for backing services to be network-based resources that require robust connection handling
- Be cautious when using third-party backing services, especially those with known vulnerabilities or poor maintenance. Regularly review and assess the security risks associated with external services
- Don’t rely on a single backing service for all functionality. Instead, use a combination of different services to achieve the desired functionality and ensure high availability
Build, release, run
Build, release, run factor focuses on how the application is packaged, deployed, and executed. The goal is to ensure consistency and reliability across different environments and stages of the application lifecycle.
Best Practices
- Separate the stages of building, releasing, and running the application. This allows for clear boundaries and responsibilities between development, testing, and deployment
- Use version control systems (VCS) to track each build and release. This ensures that all team members can easily access and update the application’s state
- Automate the build and release processes as much as possible. This reduces human error and increases efficiency
- Keep all environments (development, staging, production) as similar as possible. This ensures that the application behaves consistently across all stages
- Use environment variables to store configuration settings that change between environments
- Document the build, release, and run processes thoroughly. This helps developers understand how to package, deploy, and execute the application
Anti-Patterns
- Don’t manually intervene in the build or release processes. This can introduce errors and inconsistencies
- Avoid using different tools or systems for different stages of the application lifecycle. This can lead to confusion and inconsistencies
- Don’t hardcode configuration settings into the application. This makes it difficult to manage changes between environments
- Be cautious when using third-party build or deployment tools. Ensure they are reliable and well-maintained
- Don’t ignore failures in the build or release processes. Always investigate and resolve issues as soon as they occur
Processes
Processes factor focuses on how the application manages running tasks. The goal is to ensure that the application can scale out via the process model, meaning each process is self-contained and shares no state with any other process.
Best Practices
- Do not store data in-memory of the application
- The memory space or filesystem of the process/application can be used only as a temporary one
- The processes must be independent
- Do not keep track of or make assumptions about the existence of information produced by other processes such as session variables
- A stateless architecture facilitates horizontal scaling by adding multiple run processes to handle a huge workload
Anti-Patterns
- Multiple applications must be split up and placed in separate codebases if there are multiple applications from the same code base
- If several applications share a codebase, it would make sense to factor the code into a library (with its own codebase) that will be used by the other applications
Port binding
Port binding factor focuses on how the application exposes its services to the outside world. The goal is to ensure that the application is self-contained and does not rely on runtime injection of a webserver into the execution environment to create a web-facing service.
Best Practices
- Use environment variables to store port configurations, rather than hardcoding them into the application. This allows for easy changes without requiring code modifications
- Ensure that your application is completely self-contained. It should not rely on an external web server, but instead have one built-in
- Make sure your application can bind to and listen on an assigned address and port supplied via configuration
- Use a reverse proxy for routing external traffic and handling concerns like SSL termination when necessary, but avoid letting it dictate the port binding behavior of your application
- Document thoroughly how to configure the port and any related networking settings, so that new developers or operators can easily understand how to deploy and run your application
Anti-Patterns
- Don’t rely on specific ports being available. Your application should be able to work with any port assigned to it
- Avoid tightly coupling the application to specific network topologies or environments. Instead, make sure your application can run correctly in any environment with any network topology
- Don’t hardcode network addresses or other networking configuration details within the application. Store these values securely outside of the codebase
- Be cautious when binding to all network interfaces (0.0.0.0), as this can open up your application to traffic from any network interface, including potentially untrusted ones
- Don’t rely on system-wide network settings that are not included in the application’s configuration. This can lead to inconsistencies between environments and makes it harder to manage and update networking configurations
Concurrency
Concurrency factor focuses on how the application handles multiple tasks running in parallel. The goal is to ensure that the application can scale out via the process model, rather than scaling up via increased computing power.
Best Practices
- Use the process model to scale out the application, rather than relying on threading or increased computing power. This allows for greater flexibility and scalability
- Treat each concurrent process as a first-class citizen. Each process should be able to run independently and not rely on shared state
- Use message passing between processes to coordinate tasks. This avoids issues with shared state and allows for better isolation of processes
- Keep processes stateless and share-nothing. Any necessary state should be stored in a backing service, not in the process itself
- Use robust queueing systems to manage tasks between processes. This ensures that tasks are not lost if a process crashes or is terminated
- Document the concurrency model thoroughly, including how tasks are divided between processes and any potential issues with concurrency
Anti-Patterns
- Don’t rely on shared state between processes. This can lead to race conditions and makes it difficult to scale the application
- Avoid long-running processes that hold onto system resources. Instead, use short-lived processes that free up resources when they’re done
- Don’t hardcode the number of processes or threads within the application. This limits scalability and makes it difficult to adjust to changing load
- Be cautious when using third-party libraries or services that are not designed for concurrency. They may not behave as expected when used in a concurrent environment
- Don’t ignore errors or crashes in concurrent processes. These should be logged and handled appropriately to avoid data loss or inconsistencies
Disposability
Disposability factor focuses on how quickly the application can start or stop. The goal is to minimize startup time and gracefully handle termination, enabling the application to be robust with sudden changes and high loads.
Best Practices
- Design the application to start up quickly. This allows for more frequent deployments and better scalability
- Implement graceful shutdown procedures that properly close connections, release resources, and prepare the application for termination
- Use robust health checks to monitor the application’s status and readiness
- Design the application to be stateless so that it can easily be stopped and started without affecting functionality
- Use process managers to handle starting and stopping of the application
- Implement crash-only design, where the application is designed to just crash rather than handling complex failure modes
Anti-Patterns
- Don’t ignore
SIGTERM
signals. Applications should listen for these signals and initiate a graceful shutdown - Avoid long-running tasks that can delay shutdown or startup
- Don’t rely on manual intervention for starting or stopping services
- Avoid local caching which can lead to inconsistent state across instances
- Be cautious when using singleton patterns as they can lead to startup delays and difficulties in disposing of instances
Dev/Prod parity
Dev/Prod parity factor focuses on keeping development, staging, and production environments as similar as possible. The goal is to minimize the time and cost of software deployment by ensuring that the application behaves consistently across all environments.
Best Practices
- Use the same backing services in all environments. This means using the same database, messaging queue, caching layer, etc., in development, staging, and production
- Keep the time gap small between deployments. Regularly deploy your application to avoid “integration hell” where changes pile up and become difficult to manage
- Use continuous integration/continuous deployment (CI/CD) pipelines to automate testing and deployment processes
- Make your environments ephemeral, meaning they can be spun up and torn down easily. This allows for easy testing and reduces the risk of environment-specific bugs
- Use feature flags to enable or disable features without requiring code changes or redeployments
- Use infrastructure as code (IaC) tools to manage your environments. This ensures that all environment configurations are version-controlled and reproducible
Anti-Patterns
- Don’t use different backing services in different environments. This can lead to unexpected behavior when the application is moved from one environment to another
- Avoid long-lived branches that are only merged infrequently. These can lead to merge conflicts and integration issues
- Don’t manually configure your environments. This can lead to inconsistencies between environments and makes it harder to reproduce issues
- Avoid making changes directly in the production environment. Always make changes in a development or staging environment first, then promote those changes to production
- Don’t ignore failing tests or build errors in your CI/CD pipeline. These are signs of potential issues that could affect the stability of your application.
Logs
Logs factor focuses on how the application handles logging and monitoring. The goal is to ensure that logs are treated as event streams and can be used for debugging, analytics, and alerting.
Best Practices
- Use a centralized logging system to collect and analyze logs from all parts of the application. This allows for easy searching, filtering, and alerting based on log data
- Treat logs as event streams, where each log entry is an immutable event that occurred in the system
- Include useful context in log messages, such as timestamps, log levels (e.g., INFO, WARN, ERROR), and relevant identifiers (e.g., user ID, request ID)
- Use structured logging formats (like JSON) to make it easier to query and analyze log data
- Implement log rotation and retention policies to manage storage space and comply with data privacy regulations
- Monitor logs for errors and anomalies, and set up alerts to notify the team of potential issues
Anti-Patterns
- Don’t write logs to local files or local storage in production. This can lead to data loss if the application crashes or the server is terminated
- Avoid using print statements for logging. Instead, use a dedicated logging library that provides more flexibility and control
- Don’t include sensitive information (like passwords or API keys) in logs. This can lead to security vulnerabilities if logs are not properly secured.
- Be cautious when logging verbose or debug-level messages in production. This can lead to information overload and make it harder to identify important events
- Don’t ignore error or warning messages in logs. These can indicate serious issues that need to be addressed
Admin processes
Admin processes factor focuses on how the application handles one-off administrative tasks or jobs that are part of the application’s operation. The goal is to manage these tasks in a way that is consistent with the application’s main processes.
Best Practices
- Run admin/management tasks as one-off processes in the same environment as the regular long-running processes of the app. This ensures consistency and reduces the likelihood of environment-specific bugs
- Use the same codebase and the same release bundle for running regular and one-off processes. This ensures that all processes are running the same code
- Use the same dependency isolation techniques for both regular and one-off processes to ensure that all dependencies are explicitly declared and isolated
- Store admin process configurations in environment variables, separate from the application code. This allows for easy changes without requiring code modifications
- Document all admin processes thoroughly, including their purpose, usage, and potential impact on the application’s operation. This helps developers understand the role each process plays and avoid introducing breaking changes
Anti-Patterns
- Don’t run admin processes on a developer’s local machine. This can lead to inconsistencies between environments and makes it harder to manage and update dependencies
- Avoid running admin processes in a different environment or using a different stack than the one used by the main application. This can lead to unexpected behavior and bugs
- Don’t hardcode configurations or credentials related to admin processes within the application. Store these values securely outside of the codebase
- Be cautious when performing destructive actions (like database migrations) as part of admin processes. Always have a rollback plan in case something goes wrong
Conclusion
12-Factor App methodology offers a comprehensive set of best practices for modern application development. By adhering to these principles, developers can build scalable, maintainable, and portable applications that are well-suited for deployment in various environments.