DEV Community

kumaraish
kumaraish

Posted on

A Structured Backend Architecture for Long-Lived Systems

Formalizing Responsibility, Models, and Validation Boundaries

Structured Backend Architecture

1. Problem Statement: Why Naïve Layering Fails at Scale

Most backend architectures begin with good intentions and degrade through incremental compromise. Controllers accumulate business logic “temporarily.” DTOs are reused internally “for convenience.” Repositories enforce rules “because the data is already there.” Over time, responsibility boundaries collapse, and the system becomes difficult to reason about without full-context knowledge.

The failure mode is not the absence of layers, but the absence of clear ownership. Naïve layering treats components as technical tiers rather than as semantic roles. As a result, business meaning becomes smeared across controllers, services, repositories, and database constraints, making correctness emergent rather than explicit.

This document formalizes an architecture whose primary goal is preserving semantic clarity under growth. The system is designed such that each layer answers a distinct question, each model has a single reason to exist, and each validation rule has a clearly defined owner.

2. Model Taxonomy and Responsibility Boundaries

This architecture distinguishes four categories of models, each corresponding to a different concern and lifecycle.

2.1 HTTP Request/Response DTOs (Transport Envelopes)

HTTP DTOs describe API shape, not business data. They exist to stabilize external contracts, support versioning, and group payloads. They are transport artifacts and intentionally disposable.

They may contain metadata, nesting, or naming that is irrelevant to the application core. They are not reused outside controllers.

2.2 Application Entities (Service-Level Data Contracts)

Application entities represent use-case input and output, independent of transport and persistence. They define what an application service consumes and produces.

They are:

  • Transport-agnostic
  • Domain-agnostic
  • Persistence-agnostic

They exist to decouple application logic from both HTTP and domain internals, enabling service reuse across transports and API evolution without service churn.

2.3 Rich Domain Models (Behavior + Invariants)

Domain models represent business truth. They own identity, invariants, and state transitions. They are internal to the application core and are never serialized directly or exposed externally.

They cannot exist in an invalid state and cannot be partially constructed. They are behavioral objects, not data structures.

2.4 Persistence Entities (Database Rows)

Persistence entities exist solely to match database schemas. They encode storage concerns such as column naming, normalization, and constraint representation. They have no behavior and no business meaning.

3. Controller Layer: Transport Adaptation, Not Logic

Controllers are transport adapters. They translate HTTP requests into application-level inputs and translate application outputs into HTTP responses.

Their responsibilities are limited to:

  • Parsing HTTP requests
  • Validating request shape and format
  • Constructing request DTOs
  • Extracting application entities
  • Mapping application entities to response DTOs

Controllers do not enforce business rules, perform persistence, or coordinate use cases. They are intentionally thin to ensure that changes in transport or API shape do not propagate into the application core.

Controllers are the only layer that understands HTTP semantics.

4. Application Services as Use-Case Orchestrators

Application services represent intent. Each service corresponds to a single use case and expresses what the system is trying to accomplish, not how data is stored or transported.

Services:

  • Consume application entities
  • Load required aggregates
  • Enforce business rules that require coordination or persistence knowledge
  • Create and mutate domain models
  • Decide when persistence occurs
  • Return Rich Domain Models and/or Domain-derived projections

Services are the only layer where domain objects are created. This ensures that aggregates always come into existence through a valid use case rather than through incidental construction.

Services derive meaning from facts. They do not store data, and they do not encode transport or persistence concerns.

5. Rich Domain Models as the Source of Truth

The domain layer is the semantic core of the system. It encodes what is always true regardless of use case, transport, or persistence.

Domain models:

  • Own identity
  • Own invariants
  • Own consistency rules
  • Encapsulate state transitions
  • Are persistence-agnostic

Invariants that can be enforced with local aggregate knowledge belong here. If a rule can be expressed without querying external state, it is a domain concern.

Domain models expose their state only through controlled methods and, when required, through defensive copies (toData) to prevent external mutation.

6. Repositories as Persistence Adapters

Repositories are infrastructure adapters that translate between domain snapshots and persistence representations.

Repositories:

  • Accept fully valid domain snapshots
  • Convert domain data to persistence entities
  • Persist atomically
  • Rehydrate domain models from stored data
  • Enforce persistence-level constraints only

They do not generate identity, enforce business rules, or derive meaning. If removing a rule from a repository would change product behavior, that rule does not belong there.

Repositories return facts, not interpretations.

7. Identity, Immutability, and Aggregate Boundaries

Identity is generated by the domain, not by the database. Aggregates must exist conceptually before persistence, enabling offline creation, deterministic testing, and distributed workflows.

Immutability is enforced at the aggregate boundary, not through pervasive readonly fields. Internal state is private; external access is mediated through behavior or defensive copies. This ensures invariants remain protected while allowing efficient internal mutation.

Aggregate boundaries define consistency scopes. Only data owned by the aggregate is directly mutated; external relationships are coordinated at the service level.

8. Validation Flow and Error Propagation

Validation is layered and directional.

  • Controllers validate shape and format
  • Services validate meaning and cross-aggregate rules
  • Domain models enforce invariants
  • Databases enforce constraints

Errors propagate outward. No layer catches and reinterprets errors from a deeper layer into its own semantic domain. Controllers alone translate errors into HTTP responses.

This ensures that invalid operations fail early, partial state is never persisted, and error semantics remain consistent.

9. Transactionality, Concurrency, and Reality

The architecture assumes optimistic concurrency. Lost updates are acceptable until the business requires stronger guarantees. Transactions are used for atomicity, not for semantic correctness.

Stricter guarantees — versioning, compare-and-swap, or locking — are introduced only when demanded by business rules. Concurrency control is treated as an explicit design decision, not an implicit side effect of database usage.

10. End-to-End Request Lifecycle

Forward Path

HTTP JSON → Controller → Request DTO → Application Entity → Service → Domain Model → Repository → Database Row

Backward Path

Database Row → Repository → Domain Model → Service → Application Entity → Controller → Response DTO → HTTP JSON

Every transformation is intentional. No layer is skipped. No responsibility is shared.

11. Error Propagation Rule (Critical)

Errors move outward, never inward

That is the invariant of the architecture.

Controllers are the only place where errors become HTTP responses.

  • Controllers validate shape
  • Services validate meaning
  • Domains protect truth
  • Databases enforce reality
  1. Error Propagation Rule (Critical)

12. Trade-offs and Explicit Non-Goals

This architecture favors clarity over minimalism. It introduces additional model types and explicit mapping steps, increasing upfront structure.

It does not optimize for:

  • Rapid prototyping with minimal abstraction
  • Active Record patterns
  • Framework-driven convenience
  • Implicit persistence behavior

These are conscious exclusions.

13. Why This Architecture Remains Evolvable

The system remains evolvable because responsibilities are stable even as implementations change. APIs can evolve without touching services. Persistence can change without affecting domain logic. Domain rules can grow without leaking into transport or storage.

Most importantly, correctness remains local. Engineers can reason about changes by understanding a single layer at a time rather than reconstructing global behavior.

This architecture does not eliminate complexity. It contains it.

Top comments (0)