Skip to content

Web Development · Architecture Patterns

Domain-Driven Design, Without the Jargon: Bounded Contexts and When It's Worth It

Domain-driven design gets a reputation for ceremony: ubiquitous language, aggregates, bounded contexts, a vocabulary that scares teams off before they see the actual idea. The actual idea is simple and it solves a real problem. Here's what it is and when to reach for it.

Prathviraj Singh

Prathviraj Singh

6 min read

Domain-Driven Design, Without the Jargon: Bounded Contexts and When It's Worth It

Sponsored

Share

A “Customer” object with 40 fields, half of them nullable because billing needs some and support needs others and nobody needs all of them at once, is usually where a team’s domain-driven design education actually begins. Not from a book. From the moment someone tries to add a field for one team’s use case and breaks a query for another team that shares the same table.

Domain-driven design’s answer to that problem is more modest than its reputation suggests. Strip away the vocabulary and the core claim is: different parts of a business think about the same word differently, and pretending otherwise is what created the 40-field object in the first place.

The actual problem DDD solves

Most systems reach for one shared model per real-world concept. One Customer table. One Order type. One canonical representation, reused everywhere, because reuse is supposed to be good.

The trouble is that “customer” doesn’t mean one thing. Billing cares about a payment method, a subscription tier, and an outstanding balance. Support cares about a ticket history and a satisfaction score. Shipping cares about an address and delivery preferences. Marketing cares about consent flags and campaign attribution. None of those are wrong. They’re different models of the same real-world entity, built for different purposes, and forcing them into one shared object means every team either adds fields nobody else needs or works around a schema built for someone else’s job.

Domain-driven design’s answer is a bounded context: a boundary, usually matching a team or a subsystem, inside which a specific model of “customer” is consistent and complete for that context’s purpose. Billing’s Customer and Support’s Customer are allowed to be different types, because they are different things being asked to answer different questions.

┌─────────────────────┐      ┌─────────────────────┐      ┌─────────────────────┐
│   Billing Context     │      │   Support Context     │      │  Shipping Context     │
│                        │      │                        │      │                        │
│  Customer {            │      │  Customer {            │      │  Customer {            │
│    id                  │      │    id                  │      │    id                  │
│    paymentMethod       │      │    ticketHistory[]      │      │    shippingAddress     │
│    subscriptionTier    │      │    satisfactionScore    │      │    deliveryWindow      │
│    outstandingBalance  │      │    assignedAgent        │      │    carrierPreference   │
│  }                      │      │  }                      │      │  }                      │
└──────────┬────────────┘      └──────────┬────────────┘      └──────────┬────────────┘
           │                              │                              │
           └──────────── shared identifier: customerId ──────────────────┘

Each context owns a model that’s genuinely useful for what it does. They agree on an identifier, not a shared schema.

Ubiquitous language: the part that sounds like ceremony but isn’t

DDD’s other core rule is that code, tests, and conversations with the people who actually understand the business should use identical vocabulary, inside a given bounded context. If the support team calls something an “escalation” in every meeting, the code should have a class or method called escalation, not priorityFlag or urgentCase. This sounds like a naming convention. It’s actually a bug-prevention mechanism: when a bug report says “the escalation isn’t triggering” and the code has a function called escalate(), the person debugging it doesn’t have to reverse-engineer which internal concept the bug report is describing. The translation layer between “what the business says” and “what the code says” is the thing DDD is trying to delete, because every translation is a place meaning can drift.

Tactical patterns, briefly

Inside a bounded context, DDD offers a small set of building blocks. An entity has identity that persists through change (a specific order, trackable by ID, even as its status changes). A value object has no identity of its own and is defined entirely by its data (a money amount, an address, interchangeable with any other instance holding the same values). An aggregate is a cluster of entities and value objects treated as one unit for consistency, with a single entry point (the aggregate root) that enforces the business rules for the whole cluster, so nothing outside the aggregate can put it into an invalid state.

class Order:  # aggregate root
    def __init__(self, order_id, customer_id):
        self.id = order_id
        self.customer_id = customer_id
        self.line_items = []
        self.status = "draft"

    def add_line_item(self, product_id, quantity):
        if self.status != "draft":
            raise InvalidOrderState("cannot modify a submitted order")
        self.line_items.append(LineItem(product_id, quantity))

    def submit(self):
        if not self.line_items:
            raise InvalidOrderState("cannot submit an empty order")
        self.status = "submitted"

Nothing outside Order can add a line item or flip the status directly. The business rule, “a submitted order can’t be modified, an empty order can’t be submitted”, lives in exactly one place instead of being re-implemented, and possibly re-implemented incorrectly, at every call site that touches an order.

When it’s worth the cost

DDD is not free. Bounded contexts mean translation logic at the seams (an API layer that maps billing’s Customer to support’s Customer when data needs to cross the boundary). Aggregates mean more types and more indirection than a flat data model. For a domain that’s genuinely simple, a basic CRUD tool, an internal dashboard, most landing-page-and-form applications, this buys nothing and costs real development time.

The signal to reach for it is business rule complexity, not codebase size. A payments system with real edge cases around refunds, partial captures, and chargebacks benefits enormously from an aggregate that makes invalid states unrepresentable. A settings page that lets a user toggle email notifications does not, no matter how large the codebase around it gets.

The single most common failure mode in teams adopting DDD is applying it uniformly. Most real systems have one or two genuinely complex domains, usually whatever generates the company’s revenue or carries its core business risk, surrounded by simpler supporting contexts like user preferences or notification settings. Put the tactical patterns where the complexity actually lives. Let the supporting contexts stay plain CRUD. This is the same instinct behind choosing hexagonal architecture selectively rather than wrapping every module in ports and adapters: the pattern earns its keep where the complexity is real, and it’s dead weight everywhere else. Reserving DDD’s ceremony for where it pays off is what keeps a team using it instead of quietly reverting to one shared Customer object the next time someone’s in a hurry.

Getting the model right inside a bounded context is only half the picture. The other half is what happens when a call into one of your aggregates is slow or fails, which is exactly what patterns like the bulkhead pattern address at the infrastructure layer.

Frequently asked questions

What is domain-driven design in simple terms?
An approach to structuring software where the code's model matches how domain experts actually think and talk about the business, and where that model is scoped to specific boundaries (bounded contexts) rather than forced into one shared representation across the whole system. It's less about a specific set of patterns and more about a discipline: talk to the people who understand the business, build the language they use into the code, and don't let a model designed for one part of the business leak into a part where it doesn't fit.
What is a bounded context?
A boundary, usually aligned with a team or a subsystem, inside which a specific model and vocabulary are consistent and unambiguous. Outside that boundary, the same word can mean something different. A 'Customer' in the billing context has a payment method and a balance; a 'Customer' in the support context has a ticket history and a satisfaction score. Both are valid, and DDD says don't merge them into one god object just because they share a name.
Isn't domain-driven design the same as microservices?
No. DDD is a modeling approach; microservices is a deployment architecture. Bounded contexts are a genuinely useful input to deciding where to draw microservice boundaries, since a bounded context is a natural seam, but you can apply DDD fully inside a single monolith (often called a modular monolith) and get most of the benefit without any of the distributed-systems cost. Plenty of teams adopt DDD's modeling discipline and never split a single service.
When should a team NOT use domain-driven design?
When the domain is genuinely simple. A basic CRUD admin tool, an internal reporting dashboard, a straightforward content site, none of these have the kind of business complexity DDD is built to manage. Applying aggregates, repositories, and bounded contexts to a system with five entities and no real business rules adds indirection that buys nothing. The signal to reach for DDD is business rule complexity and conflicting terminology across teams, not codebase size alone.
What's the most common mistake teams make adopting DDD?
Applying the full tactical pattern set, aggregates, value objects, domain events, repositories, uniformly across an entire codebase instead of reserving it for the bounded context that's actually complex. Most systems have one or two genuinely complex domains (often the core business logic that makes the company money) surrounded by simpler supporting contexts like user settings or notification preferences. Those supporting contexts do fine with plain CRUD. Forcing DDD ceremony onto them is the reputation-damaging failure mode that makes teams write off the whole approach.

Sources

Sponsored

Sponsored

Discussion

Join the conversation.

Comments are powered by GitHub Discussions. Sign in with your GitHub account to leave a comment.

Sponsored