The 12-Factor App Methodology
The 12-factor app methodology is a set of best practices for building modern, scalable, cloud-native applications. These principles help create applications that are portable, resilient, and easy to deploy.
1. Codebase
“One codebase tracked in revision control, many deploys”
A 12-factor app should have a single codebase that can be deployed to multiple environments (development, staging, production).
Key Principles:
- One codebase per application
- Use a logical version control system (Git)
- Use branches for different versions, not multiple codebases
- The repository shouldn’t house multiple applications
Repository Structure:
├── main branch → Production
├── develop branch → Staging
└── feature/* branches → Development
2. Dependencies
“Explicitly declare and isolate dependencies”
Never rely on implicit existence of system-wide packages. Declare all dependencies explicitly.
Key Principles:
- Application must be self-containing
- Isolate dependencies to avoid conflicts
- Include necessary system libraries
- Use package managers (NuGet, npm, pip)
<!-- .NET Example: csproj -->
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.0" />
<PackageReference Include="Serilog" Version="3.1.1" />
</ItemGroup>
3. Config
“Store config in the environment”
Never commit environment-specific configurations (passwords, connection strings) in source code.
Key Principles:
- Application and configuration must be independent
- Store config in environment variables
- Simplifies updating config without redeployment
- Config is independent of operating system and language
// ❌ Bad - hardcoded config
var connectionString = "Server=prod-db;User=admin;Password=secret";
// ✅ Good - environment variables
var connectionString = Environment.GetEnvironmentVariable("DATABASE_URL");
// ✅ Better - ASP.NET Core configuration
public class Startup
{
public IConfiguration Configuration { get; }
public void ConfigureServices(IServiceCollection services)
{
var connectionString = Configuration.GetConnectionString("DefaultConnection");
}
}
4. Backing Services
“Treat backing services as attached resources”
External services (databases, message queues, caches) should be treated as resources that can be attached/detached without code changes.
Key Principles:
- Access services via URL/connection string in config
- No distinction between local and third-party services
- Services can be swapped without code changes
// Services accessed through configuration
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(Configuration["Services:Database"]));
services.AddStackExchangeRedisCache(options =>
options.Configuration = Configuration["Services:Redis"]);
5. Build, Release, Run
“Strictly separate build and run stages”
The deployment pipeline should have distinct stages that cannot be mixed.
Three Stages:
- Build: Convert code to executable bundle (compile, fetch dependencies)
- Release: Combine build with config for specific environment
- Run: Execute application in the target environment
# CI/CD Pipeline Example
stages:
- build: # Compile, run tests
- release: # Create versioned artifact with config
- deploy: # Deploy to environment
Key Principles:
- Each release must have a unique ID (timestamp, version number)
- Releases are immutable - changes require new release
- Rollback by deploying previous release
6. Processes
“Execute the app as one or more stateless processes”
Applications should be stateless and share-nothing. Any data that needs to persist must be stored in a stateful backing service.
Key Principles:
- No state stored in memory or filesystem
- Use backing services (database, Redis) for persistent data
- Use sticky sessions only as a last resort
// ❌ Bad - in-memory state
private static Dictionary<string, object> _cache = new();
// ✅ Good - distributed cache
public class CartService
{
private readonly IDistributedCache _cache;
public async Task SaveCartAsync(string userId, Cart cart)
{
await _cache.SetStringAsync(
$"cart:{userId}",
JsonSerializer.Serialize(cart));
}
}
7. Port Binding
“Export services via port binding”
The app should be completely self-contained and not rely on runtime injection of a webserver.
Key Principles:
- App exports HTTP as a service by binding to a port
- Web server library included as dependency
- App can become a backing service for other apps
// ASP.NET Core - self-contained web server
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();
app.MapGet("/", () => "Hello World");
app.Run("http://localhost:5000");
8. Concurrency
“Scale out via the process model”
Design application to scale horizontally by running multiple instances.
Key Principles:
- Scale by adding more processes/instances
- Different workload types can be different process types
- Never daemonize or write PID files
Scaling Model:
├── Web (3 instances) ─── Handle HTTP requests
├── Worker (5 instances) ─── Process background jobs
└── Clock (1 instance) ─── Schedule periodic tasks
9. Disposability
“Maximize robustness with fast startup and graceful shutdown”
Processes should be disposable - they can be started or stopped at any time.
Key Principles:
- Minimize startup time
- Shut down gracefully on SIGTERM
- Handle sudden death gracefully
- Make operations idempotent
// Graceful shutdown in ASP.NET Core
var app = builder.Build();
var lifetime = app.Services.GetRequiredService<IHostApplicationLifetime>();
lifetime.ApplicationStopping.Register(() =>
{
// Finish current requests
// Close connections gracefully
Log.Information("Application shutting down...");
});
10. Dev/Prod Parity
“Keep development, staging, and production as similar as possible”
Minimize gaps between development and production environments.
Three Gaps to Minimize:
- Time gap: Deploy frequently (hours, not weeks)
- Personnel gap: Developers involved in deployment
- Tools gap: Use same backing services in all environments
# Docker Compose - same services everywhere
services:
app:
build: .
environment:
- DATABASE_URL=postgres://db:5432/app
db:
image: postgres:15
redis:
image: redis:7
11. Logs
“Treat logs as event streams”
Apps should not manage log files. Instead, write logs to stdout as an event stream.
Key Principles:
- Write to stdout/stderr
- Don’t route or store logs within the app
- Use log aggregation tools (ELK, Splunk, Datadog)
// Serilog streaming to console
Log.Logger = new LoggerConfiguration()
.WriteTo.Console(new JsonFormatter())
.CreateLogger();
// Logs are captured by orchestrator and forwarded
Log.Information("Processing order {OrderId}", orderId);
12. Admin Processes
“Run admin/management tasks as one-off processes”
Administrative tasks should run as one-off processes in an identical environment.
Key Principles:
- Run against same codebase and config
- Use same isolation and dependencies
- Ship admin code with application code
// Database migration as one-off process
// dotnet ef database update
// Custom admin command
public class MigrateDataCommand
{
public async Task ExecuteAsync()
{
// Uses same DbContext, config, and dependencies
// Runs in same environment as main app
}
}
Summary Table
| Factor | Principle | .NET Implementation |
|---|---|---|
| Codebase | One repo, many deploys | Git + CI/CD |
| Dependencies | Explicit declaration | NuGet packages |
| Config | Environment variables | IConfiguration |
| Backing Services | Attached resources | Connection strings |
| Build/Release/Run | Separate stages | Docker + K8s |
| Processes | Stateless | Distributed cache |
| Port Binding | Self-contained | Kestrel |
| Concurrency | Horizontal scaling | K8s replicas |
| Disposability | Fast start/stop | Graceful shutdown |
| Dev/Prod Parity | Similar environments | Docker |
| Logs | Event streams | Serilog + ELK |
| Admin Processes | One-off tasks | EF migrations |
Sources
C#/Teorie/Microservices.docx