04
Product
16
Backend
09
Auth
12
iOS
07
Infra
02
Real-Time

Adopt domain-driven design for the backend

ADR-0009 ACCEPTED · 2025-07-12
Adopt Domain-Driven Design for backend architecture

Context

The backend will have complex business logic spanning users, trips, accommodations, reservations, journeys, memberships, and more. The traditional alternative is layered architecture — controllers, services, repositories — which organizes code by technical concern. That makes it hard to find all the logic related to one business concept, and creates dependencies between layers that don't map to how you think about the problem.

Decision

Organize code around business domains as bounded contexts under src/domain/. Each domain owns its types, errors, business logic, and GraphQL resolvers.

The standard domain structure:

  • types.rs — domain types and value objects, no GraphQL imports
  • errors.rs — domain-specific error codes via define_error_domain!
  • graphql.rs — resolvers and into_graphql_result() calls
  • service.rs — business logic when the domain needs it

Domains communicate through direct imports and service injection via GraphQL context. No repository abstraction layer — services use sqlx directly, which keeps things simple and means tests hit the real database.

Value objects should enforce invariants at parse time (e.g. date ranges validate ordering, IDs validate non-empty). Newtype ID wrappers prevent argument-order mistakes at compile time.

Consequences

Code organization matches how you think about the problem. Everything related to accommodations lives in one place. Domain-specific errors compose cleanly into GraphQL responses. New domains follow a predictable structure that's easy for both humans and agents to navigate.

The trade-off is that cross-domain operations (like invitation acceptance touching membership and realtime) will span boundaries without formal coordination. If that becomes a problem, domain events or explicit transaction boundaries are the next step. Domains will also import from each other directly, so changes in one can ripple — though the bounded context structure should limit this.