What DDD actually is
Strip the consultancy fog and Domain-Driven Design is one claim: the structure of your software should mirror the structure of the business problem — and a toolkit for finding that structure. It matters here, at the end of Level 7, because every hard question you've met — where do services split?, what does the gateway route to?, which entity owns the invariant? — is secretly the same question: where are the boundaries? DDD is the discipline for answering it on purpose instead of by accident.
Ubiquitous language (the cheap superpower)
Within a team and its code, one vocabulary, shared with the
business, used literally in class names. If the operations team
says "a booking expires," the code has Booking.expire() — not
ReservationRecordManager.updateStatus(7). Every translation layer
between business-speak and code-speak is a place bugs breed ("wait,
is a 'reservation' the same as a 'hold'?").
The catch that motivates everything else: words legitimately mean
different things in different places. "Customer" to billing is a
payment method and a tax id; to support, a ticket history; to
shipping, addresses. Forcing one Customer class to serve all three
gives you the god object — fields for everyone,
meaning for no one.
Bounded contexts (the headline idea)
A bounded context is the boundary within which one model and one
language hold. Billing's Customer and shipping's Customer are
different classes in different contexts, related only by a shared
id and explicit translation at the border:
Contexts map to business capabilities — which is exactly the service-boundary rule you already learned, now with its origin story: microservice boundaries are bounded contexts, discovered through the language ("when these two departments say the same word and mean different things, there's a border between them"). Borders talk via published contracts — APIs and events — and each side translates at its edge (an anti-corruption layer when the other side's model is messy: an Adapter defending your model's purity).
Aggregates (DDD's gift to your LLD)
Inside a context, the aggregate answers "which objects must change together?" An aggregate is a cluster of entities with one root that is the only entry point, and one rule: the aggregate is the transaction boundary — invariants inside it are enforced atomically; anything across aggregates is eventual.
You've already built these without the name:
Trip— the root; state transitions, fare facts and assignment all flow through it. "Trip + driver flip atomically" was an aggregate-boundary decision.Order+ itsSplits— splits never modified except through the expense; conservation enforced at the root.Show+ its seat states — the per-show lock is the aggregate boundary made concrete.
Two design rules fall out, both interview-grade: keep aggregates
small (the whole aggregate loads and locks together — an aggregate
of "User and all their orders" serializes everything a user does);
and reference other aggregates by id, never by object (a Trip
holds riderId, not a Rider — crossing the boundary means a lookup,
which keeps boundaries honest). Entities vs value objects rounds
out the toolkit: an entity has identity over time (Trip #881); a
value object is its attributes only (Money(342, INR),
Location(lat, lng)) — immutable, freely copied,
the tuple instinct formalized.
Domain events (how contexts talk)
When an aggregate commits a meaningful change, it publishes a fact in
the business's language: OrderPlaced, TripCompleted,
PaymentFailed. Other contexts subscribe and react in their own
models — the event-driven architecture
you've seen, with DDD supplying what the events are and who owns
them: events are named by the producing context's language, are
facts (past tense, immutable), and carry ids + minimal data, not
whole object graphs. Reliability is the outbox
pattern; recursion into
event sourcing — storing the events
as the state — is Level 9's chapter.
The modular monolith (the architecture this enables)
Here's the payoff. Microservices' costs are network-shaped; DDD's boundaries are model-shaped. You can have the boundaries without the network: one deployable, with bounded contexts as strictly separated modules:
src/
ordering/ ← bounded context = module
api/ ← the ONLY things other modules may import
internal/ ← domain model; imports of this are BUILD ERRORS
billing/
api/
internal/
shipping/
...
shared-kernel/ ← tiny: Money, ids. Guard it jealously.
The discipline that makes it real (and not just folders): module boundaries enforced by tooling — build-system visibility rules, import linters, per-module test suites; each module owns its tables (no cross-module joins — queries cross via the module's API); modules communicate through interfaces or an in-process event bus with the same contracts they'd use over a network. What you get: monolith operations (one deploy, real transactions where a context needs them, refactoring across the codebase) with microservice structure — and because the seams are real, extracting a module to a service later is mechanical: its API becomes REST/gRPC, its events move to Kafka, its tables move out. This is the concrete meaning of "monolith-first" — not "no structure," but structure without distribution. Shopify and Stack Overflow run famously on exactly this.
Common mistakes
- DDD as folder cosplay —
domain/,repository/,service/directories with an anemic model underneath (entities as bare getters/setters, all logic in "service" classes — the encapsulation failure with extra ceremony). DDD's tell is behavior living on the model:order.cancel(), notOrderService.setStatus(order, CANCELLED). - One Customer to rule them all — the shared-everything model that bounded contexts exist to kill.
- Giant aggregates — "load the user and their 4,000 orders to add one" — throughput death by transaction boundary.
- Module boundaries on the honor system — without import enforcement, the modular monolith decays into a tangled one in about six sprints.
- Shared kernel creep — the "shared" module accreting business logic until every context depends on everything: the distributed monolith's stay-at-home sibling.
- Applying full DDD to a CRUD app — a settings page doesn't need aggregates; DDD earns its keep where the domain logic is the complexity (the pattern-fever rule, architecture edition).
Interview perspective
Practice
- Language audit: for a project you know (StockVision works), list five nouns the "business" uses; check whether the code uses the same words. Every mismatch is a small bug factory — rename one.
- Context-map BookMyShow: draw its bounded contexts (catalog, booking, payments, notifications), mark which "movie"/"booking" means what where, and label each border API-or-event.
- Aggregate drill: for Splitwise, decide: is a Group one aggregate, or is each Expense its own? List the invariants each choice protects and breaks — then defend one in three sentences.
- Enforce a boundary: in any codebase, split two modules and add
an import-linter rule (e.g.
import-linterfor Python, ArchUnit for Java) that fails the build on a cross-internal import. Watch it catch you within the week.
Level 7 is complete. The backend arc now runs from the event loop to architecture — next stop on the roadmap: the levels above.