Skip to content

12-Factor App

What is the "12-Factor App"? 🤔

The "12-Factor App" is a set of 12 principles that serve as a guide for building modern web applications, especially those running in the cloud (like microservices). Think of it as a "best practices manual" to ensure your application is easy to deploy, scale, and maintain in the long run.

Why use the 12-Factor App?

  • Portability: Your application works in any environment (dev, staging, prod) without needing major changes.
  • Scalability: The application can be easily scaled horizontally (by adding more instances).
  • Maintainability: It makes maintaining and updating the application over time easier.
  • Agility: It allows the development team to work more efficiently and with fewer headaches.

How Does the 12-Factor App Help Us in This Project? 🛠️

Let's analyze the factors that can help us build better applications:

  1. I. Codebase: One codebase tracked in revision control

    • What it is: One codebase per application, tracked in a version control system like Git. Multiple deployments share the same codebase.
    • How it helps: Ensures that all team members are working with the same version of the code and makes it easier to track changes.
    • In Our Project: We are already using Git, which is great! Make sure all commits are small and descriptive, making the history easy to understand.

      • Examples of Descriptive Commit Messages:
        • feat: add email validation to the registration form
        • fix: correct bug that prevented sending messages
        • refactor: extract product listing component
        • docs: add documentation for the authentication API
        • test: add unit tests for the shipping calculation function
  2. III. Config: Store config in the environment

    • What it is: The application's configuration (e.g., database URLs, API keys, secrets) should be stored in the environment (environment variables), not in the code.
    • How it helps: Allows the same application to run in different environments (dev, staging, prod) without needing to recompile or change the code. It improves security by avoiding the storage of sensitive information in the code.
    • In Our Project: Avoid hardcoding configurations! Use environment variables to store sensitive and environment-specific information.

      • Spring Boot: use @Value("${environment.variable}")

        @Value("${spring.datasource.url}")
        private String dbUrl;
        
      • FastAPI: use os.environ.get("ENVIRONMENT_VARIABLE")

        import os
        db_url = os.environ.get("DATABASE_URL")
        
      • Next.js:

        • Variables starting with NEXT_PUBLIC_ are automatically exposed to the browser.
        • Use the .env.local file for variables specific to your local environment (it should not be committed!).
        • Example:
          • In .env.local:
            NEXT_PUBLIC_API_URL=http://localhost:3000/api
            
          • In your Next.js component:
            function MyComponent() {
              const apiUrl = process.env.NEXT_PUBLIC_API_URL;
              // ...
            }
            
  3. VIII. Logs: Treat logs as event streams

    • What it is: The application should write its logs to stdout (standard output). The execution environment is responsible for collecting and storing these logs.
    • How it helps: Simplifies log collection and analysis, allowing the use of tools like the ELK Stack (Elasticsearch, Logstash, Kibana) or Grafana Loki.

      • Log Levels:
        • DEBUG: Detailed information for debugging during development. (e.g., variable values, function calls)
        • INFO: General information about the application's operation. (e.g., "Server started", "Request received")
        • WARN: Indicates an unexpected event that does not prevent the application from continuing to function. (e.g., "Attempt to access unauthorized resource")
        • ERROR: Indicates an error that prevents the application from performing a specific action. (e.g., "Failed to connect to the database")
        • FATAL: Indicates a critical error that prevents the application from continuing to function. (e.g., "Insufficient memory," usually causes the application to terminate)
    • In Our Project: Configure your applications to write structured logs to stdout. Use log levels appropriately. A good practice is to use a JSON format for logs, which facilitates analysis.

      • Essential Information in Logs:

        • Timestamp: When the event occurred.
        • Log Level: DEBUG, INFO, WARN, ERROR.
        • Message: Description of the event.
        • Transaction ID: A unique ID to trace a transaction end-to-end across multiple microservices.
        • Object/Data Indexer IDs: IDs of important entities (e.g., customer ID, order ID) to facilitate filtering and analysis.
      • Business Event Logs:

        • It is crucial to log events that represent significant business actions (e.g., "New order created," "Payment received," "Product shipped").
        • These logs can be used to generate business metrics (KPIs) and monitor application performance (e.g., number of orders per day, conversion rate, average delivery time).
      • Structured Logging Example (Spring Boot):

        import org.slf4j.Logger;
        import org.slf4j.LoggerFactory;
        import java.util.UUID;
        
        private static final Logger logger = LoggerFactory.getLogger(MyService.class);
        
        public void processRequest(String request, UUID transactionId) {
            logger.info("Request received", Map.of(
                "transactionId", transactionId,
                "request", request,
                "timestamp", System.currentTimeMillis()
            ));
        
            try {
               // ... processing logic ...
               logger.info("Request processed successfully", Map.of("transactionId", transactionId));
            } catch (Exception e) {
               logger.error("Error processing request", Map.of(
                   "transactionId", transactionId,
                   "error", e.getMessage()
               ), e);
            }
        }
        
        public void validateData(CustomerData data, UUID transactionId) {
            if (!isDataValid(data)) {
                logger.warn("Invalid customer data", Map.of(
                    "transactionId", transactionId,
                    "customerId", data.getId(),
                    "name", data.getName()
                ));
                throw new IllegalArgumentException("Invalid data");
            }
            logger.info("Data validated", Map.of("transactionId", transactionId, "customerId", data.getId()));
        }
        
      • Structured Logging Example (FastAPI):
        import logging
        import json
        import uuid
        
        logger = logging.getLogger(__name__)
        logger.setLevel(logging.INFO)
        
        def process_request(request: str, transaction_id: str):
            logger.info(json.dumps({
                "level": "INFO",
                "message": "Request received",
                "request": request,
                "transaction_id": transaction_id
            }))
        
            try:
                # ... processing logic ...
                logger.info(json.dumps({"level": "INFO", "message": "Request processed successfully", "transaction_id": transaction_id}))
            except Exception as e:
                logger.error(json.dumps({
                    "level": "ERROR",
                    "message": "Error processing request",
                    "transaction_id": transaction_id,
                    "error": str(e)
                }), exc_info=True)
        
        def validate_data(data, transaction_id: str):
            if not is_data_valid(data):
                logger.warning(json.dumps({
                    "level": "WARN",
                    "message": "Invalid customer data",
                    "transaction_id": transaction_id,
                    "customer_id": data["id"],
                    "name": data["name"]
                }))
                raise ValueError("Invalid data")
        
            logger.info(json.dumps({"level": "INFO", "message": "Data validated", "transaction_id": transaction_id, "customer_id": data["id"]}))
        
  4. X. Dev/Prod Parity: Keep development, staging, and production as similar as possible

    • What it is: Keep the development, staging, and production environments as similar as possible. This includes:
      • Code: Use the same code in all environments.
      • Configuration: Use different configurations, but with the same structure.
      • Services: Use the same backing services (e.g., database, message queue).
    • How it helps: Reduces the risk of bugs and issues that only appear in production.
    • In Our Project: Use containers (Docker) to ensure the application runs the same way in all environments. Use infrastructure provisioning tools (e.g., Terraform) to automate the creation of similar environments.
  5. XI. Processes: Execute the app as one or more stateless processes

    • What it is: The application should be executed as one or more stateless processes. This means the application should not store any state locally. State should be stored in a backing service (e.g., a database).
    • How it helps: Facilitates horizontal scalability of the application.
    • In Our Project: Avoid storing session data locally. Use JWT (JSON Web Tokens) to store session information securely and statelessly.

      • Next.js and JWT with HttpOnly Cookies:
        • In Next.js, after receiving the JWT upon login, store it in an HttpOnly cookie.
        • HttpOnly cookies cannot be accessed via JavaScript in the browser, making them more secure against XSS attacks.
        • On the backend (API), verify the JWT present in the cookie to authenticate the user.

Other Important Factors

Although the factors above are the most directly related to the identified gaps, the other 12-Factor App factors are also important for building robust and scalable applications:

  • II. Dependencies: Explicitly declare and isolate dependencies
    • Already implemented in the base project, with the use of dependency management tools like Maven, npm, poetry, and of course, docker-compose.
  • IV. Backing Services: Treat backing services as attached resources
    • With the use of containers and orchestration (Docker + Docker Compose), we can manage the database easily. We also reinforce the need for integrated tests, either using in-memory databases or orchestrating containers within the test suite. Furthermore, the use of ORMs like JPA/Hibernate and Peewee decouples us from the specific SQL database technology, giving more flexibility to treat them as attached and flexible resources.
  • V. Build, Release, Run: Strictly separate build, release, and run stages
    • We have a Bitbucket pipeline that handles these steps. We also manage this for each project with their respective tools that isolate these steps, like Maven, Npm, and Poetry. Additionally, we have a docker-compose-dev.yaml that provides a way to work with the development environment using docker-compose.
  • VI. Port Binding: Export services via port binding
    • In this regard, we opted to use an API gateway, but the services are exposed to this gateway via a port.
  • VII. Concurrency: Scale out via the process model
    • It is possible to configure this concurrency with docker-compose, which allows fine-tuning of how many container instances we run per service. Additionally, tools like Spring, FastAPI, and Express handle this management automatically at the language process level.
  • IX. Disposability: Maximize robustness with fast startup and graceful shutdown
    • The use of containers along with orchestration via docker-compose makes this possible.
  • XII. Admin Processes: Run admin/management tasks as one-off processes
    • We request that the creation of admin users, for example, be done via a command line that only IT can execute. This is a classic example of this factor.

Conclusion 🎉

The 12-Factor App is a valuable guide for building modern and scalable applications. By applying these principles, you will be on the right track to creating more robust, easy-to-maintain systems with fewer headaches! Remember that the goal is to learn and apply these concepts gradually.

If you have any questions, don't hesitate to ask! We are here to help. 💪