Articles/Phase 3 — Backend Architecture & Layering

Controllers, Services, Repositories, Middlewares & Context

Deconstructing the request lifecycle: 3-layer separation of concerns, middleware interceptor pipelines, and scoped request context state.

Core Concept & First Principle

Clean backend systems isolate HTTP transport logic (Controllers), pure business logic (Services), and database queries (Repositories). Middlewares intercept requests across boundaries, while Request Context safely propagates trusted identity and cancellation signals.

1. The 3-Layer Architecture Pattern

Putting all database queries, business rules, and HTTP status codes inside a single route handler works for small scripts, but collapses under real-world scale. The 3-Layer Architecture Pattern establishes clean separation of concerns:

1Controller / Handler Layer (Transport Ingress)

Deals with the outside world: extracts HTTP request data, binds JSON to native language structs, validates schemas, delegates to the service layer, and formats final HTTP responses.

2Service Layer (Pure Business Logic)

Orchestrates the application domain: executes business rules, evaluates permissions, and coordinates multiple repository calls. It remains 100% agnostic of HTTP.

3Repository Layer (Data Persistence)

Interacts directly with databases (PostgreSQL, SQLite, Redis). Constructs SQL queries and returns raw entity data back up to the service layer.
The Golden Test for Services
If you inspect a Service function, you should not be able to tell that it is being used in an HTTP API. It takes pure parameters, calculates results, and returns data without knowing whether the caller is an Express API, a gRPC server, or a CLI command.

2. The Controller (Handler) Layer: Step-by-Step Workflow

The controller receives the (request, response) pair from the runtime immediately after route matching, executing a disciplined 6-step lifecycle:

11. Data Extraction

Pulls path parameters (/books/:id), query strings (?sort=date), and request bodies from the incoming wire.

22. Binding & Deserialization

Converts wire JSON strings into native typed structs (Go struct, TypeScript interface, Rust struct). If parsing fails, immediately halts with a 400 Bad Request.

33. Input Validation

Enforces schema constraints, non-null requirements, and regex formats before invoking any domain methods.

44. Transformation & Default Setting

Normalizes parameters (e.g. defaulting omitted query strings to sort=date so downstream layers never deal with undefined branches).

55. Delegation to Service Layer

Passes clean data and trusted request context down to the Service Layer for execution.

66. HTTP Status Selection & Response Dispatch

Maps domain results to standard HTTP status codes (200 OK, 201 Created, 204 No Content, 400, 403, 500) and serializes the response payload.

3. The Service Layer: HTTP-Agnostic Domain Logic

The Service layer is the operational brain of your backend. Its core characteristics include:

1Zero HTTP Contamination

Never accepts req or res objects. It does not set HTTP headers or know what a status code is.

2Complex Orchestration

A single service method can call 3 different repository methods, merge datasets, dispatch emails, push notifications to mobile devices, and trigger external payment webhooks.

3Unit Testability

Because it has no HTTP or database dependencies, service logic can be unit tested in milliseconds by mocking repository interfaces.

4. The Repository Layer: Single Responsibility Persistence

The Repository layer translates domain requests into physical database operations:

1Strict Single Responsibility Principle (SRP)

Each repository method performs exactly one database operation (e.g., findAll(), findById(id), create(data)). Never create ambiguous methods that conditionally return either a single record or an array based on optional flags.

2Query Construction & Isolation

Encapsulates SQL syntax, indexes, transactions, and Redis caching. If you migrate from PostgreSQL to MongoDB, only the Repository layer changes; Controllers and Services remain untouched.

5. Middleware Pipelines: Chaining Interceptors with next()

Middlewares are interceptor functions that execute across the boundaries between ingress, routing, controllers, and responses to eliminate code duplication across hundreds of endpoints:

Comparison Table↔ Scroll horizontally
PositionMiddleware TypePurposeEarly Termination Condition
1. First GateCORS & PreflightInspects origin headers and sets access control allowancesRejects unauthorized browser origins before server resources are spent
2. IngressRequest ID GeneratorAttaches unique UUID (X-Request-ID) to Request ContextNone (passes next() to downstream)
3. TelemetryLogging & ProfilingRecords path, HTTP method, client IP, and timestampsNone (passes next() to downstream)
4. DefenseRate LimiterTracks IP/Token velocity to prevent DoS attacksReturns 429 Too Many Requests if threshold exceeded
5. SecurityAuthenticationVerifies JWT/Session token and injects claims into ContextReturns 401 Unauthorized if credentials are invalid
6. Wire ParsingBody Parser & JSON DeserializerConverts incoming wire byte streams into JSON memory structuresReturns 400 Bad Request if JSON is malformed
7. HandlersRoute ControllersExecutes application logicReturns 200/201/204 response
8. Last GateGlobal Error HandlerCatches any uncaught exceptions thrown upstream and formats client errorsMust be placed LAST to receive uncaught errors via next(err)

6. Request Context: Scoped Per-Request State & Anti-Spoofing

Request Context is a per-request storage mechanism that carries trusted state across middleware and controller boundaries without tight coupling:

1Trusted Identity vs. Spoofed Body Payloads

When creating records (e.g. POST /api/books), NEVER read userId from the client request body. Malicious users can send arbitrary user IDs to overwrite other accounts (IDOR). Always extract the cryptographically verified userId from Request Context populated by Auth middleware.

2Distributed Request Tracing

Propagates X-Request-ID across microservices, database queries, and log statements to trace errors end-to-end.

3Cancellation Signals & Deadlines

Context holds timeout deadlines and cancellation signals (AbortController / Go context.Context) so stalled downstream DB queries terminate safely.

7. Multi-Language Clean Architecture Implementations

Production-grade 3-layer architecture implementations across TypeScript, Go, and Rust:

1// ─── 1. CONTROLLER LAYER ───
2export class BookController {
3 constructor(private service: BookService) {}
4
5 create = async (req: Request, res: Response, next: NextFunction) => {
6 try {
7 // Extract trusted userId from Request Context (never client body)
8 const trustedUserId = req.context.userId;
9 const newBook = await this.service.createBook(req.body, trustedUserId);
10 return res.status(201).json({ status: 201, data: newBook });
11 } catch (err) {
12 next(err); // Forward to Global Error Middleware
13 }
14 };
15}
16
17// ─── 2. SERVICE LAYER (100% HTTP Agnostic) ───
18export class BookService {
19 constructor(private repo: BookRepository) {}
20
21 async createBook(payload: { title: string; author: string }, userId: string) {
22 if (!payload.title) throw new Error('VALIDATION_ERROR: Title is required');
23 return await this.repo.insert({ ...payload, userId });
24 }
25}
26
27// ─── 3. REPOSITORY LAYER (Single Responsibility) ───
28export class BookRepository {
29 async insert(record: { title: string; author: string; userId: string }) {
30 return await db.query('INSERT INTO books (title, author, user_id) VALUES ($1, $2, $3) RETURNING *', [
31 record.title,
32 record.author,
33 record.userId
34 ]);
35 }
36}
Summary & Core Takeaways
Key Insights
1The 3-layer architecture separates HTTP transport concerns (Controller), pure domain calculations (Service), and database queries (Repository).
2The Service Layer must remain 100% HTTP-agnostic—it should take domain arguments and return results without any request, response, or HTTP status dependencies.
3Repository methods must strictly obey the Single Responsibility Principle: each method executes one specific database operation and returns one consistent type.
4Middleware order is critical: CORS and Request IDs execute first, while the Global Error Handler must sit at the very end of the pipeline to catch all upstream errors.
5Request Context carries per-request scoped metadata (like trusted user IDs and trace UUIDs) across function boundaries, preventing attacker impersonation.
API Sandbox (2 Live Endpoints)
Architecture
Live API

24. 3-Layer Architecture & Middleware Pipeline Trace

Traces the exact flow of an incoming HTTP request through Ingress Middleware, Request Context injection, Controller input validation, HTTP-agnostic Service orchestration, and Repository database persistence.

Headers:Content-Type: application/jsonX-Request-ID: req_firstprinciples_777
Request Body (JSON Payload)Valid JSON
Architecture
Live API

25. Request Context & Anti-Spoofing Security

Compares insecure client-supplied request body user IDs against cryptographically verified identity claims stored in per-request Context.

Headers:Content-Type: application/json
Request Body (JSON Payload)Valid JSON