Essay · Backend architecture

Three tiers, two app containers, one useful failure

Use Docker Compose to see load balancing, network isolation, statelessness, and the difference between a layer and a tier.

After the backend series has discussed BFF boundaries, test strategy, architecture records, and custom linters, it is worth returning to a basic distinction that expensive systems still get wrong: a layer is a logical division inside code; a tier is a separately running part of the system.

A conventional three-tier web system has a presentation tier, an application tier, and a data tier. They may share one machine at small scale, but their boundaries should already be visible.

The front desk, office, and vault #

The presentation tier accepts requests, terminates HTTP, serves static files, and routes traffic. Nginx is a typical example. It is the only tier directly exposed to the internet.

The application tier runs business behavior. It should be horizontally replaceable: add another instance when load grows, remove a broken one, and route requests to any healthy copy.

The data tier stores durable state. It is harder to scale because multiple writers must preserve consistency. Scale up first, add read replicas when useful, and partition only when the problem justifies the complexity.

The separation exists because each tier scales differently and deserves a different security boundary. A database reachable only from the application network has protection that application authentication cannot provide.

Statelessness buys interchangeable workers #

If login state lives in one application process’s memory, a user routed to another process appears logged out. Upload progress stored on local disk disappears when the next request reaches another server. These bugs reproduce in proportion to your success at scaling, which is rather rude.

Keep application instances stateless. Carry required information with the request, or place shared state in a database or Redis. JWT-based authentication fits this model because the request carries its proof instead of depending on one server’s memory.

Build a miniature with Compose #

The example uses Nginx as the presentation tier, two traefik/whoami containers as the application tier, and PostgreSQL as the data tier:

services:
  web:
    image: nginx:1.27
    ports: ['8080:80']
    volumes: ['./nginx.conf:/etc/nginx/nginx.conf:ro']
    depends_on: [app]
 
  app:
    image: traefik/whoami:v1.10
    deploy:
      replicas: 2
 
  db:
    image: postgres:16
    environment:
      POSTGRES_PASSWORD: postgres

Only web publishes a host port. Repeated requests to localhost:8080 return alternating application hostnames, demonstrating that the client sees one entrance while Nginx spreads work across two interchangeable processes.

From the host, port 8080 responds and PostgreSQL’s 5432 does not. From a temporary container on the Compose network, pg_isready -h db succeeds. The database is healthy; it is simply not standing on the pavement introducing itself to scanners.

Stop one application container and send requests again. The first response may take almost two seconds while Nginx times out the dead upstream and retries the survivor. Later requests go directly to the healthy instance. Multiple replicas buy availability as well as throughput.

Physical separation can wait #

A personal project does not need three servers on day one. Running every tier on one machine is reasonable. Keep the logical contracts intact: application processes remain stateless, connection information arrives through configuration, and the database is not coupled to presentation code.

That is the continuity with the earlier architecture notes. The monolith can stay a monolith; its internal layers still protect changeability. The deployment can stay on one host; its logical tiers still preserve a path to scaling and isolation. Do not purchase distributed-systems problems before you have distributed-systems traffic.

When scale arrives, the stateless application tier is easy to replicate. The data tier usually becomes the bottleneck because it is the least interchangeable. Indexes, query behavior, connection pools, and read replicas matter before fashionable topology diagrams do.

Related articles