MicroservicesDomain-Driven Designbeginner
Updated:

Domain Driven Design Basics

7 min read

Learn the core DDD ideas — bounded contexts, aggregates, and ubiquitous language — that tell you where one microservice should end and the next should begin.

TL;DR – Quick Answer

Domain-driven design is an approach that models software around the real business domain, using a shared language between developers and domain experts. Its central tools are the bounded context, which sets a boundary where a model and its terms hold true, and the aggregate, which groups objects that must stay consistent together. In microservices, a bounded context usually becomes one service.

On This Page

Domain-driven design (DDD) is a way of building software so that the code mirrors the business it serves. Instead of organizing by technical layers — controllers, services, repositories — you organize around the real concepts the business cares about: orders, shipments, reservations, invoices.

For microservices this matters enormously, because the hardest question in the whole style is "where does one service end and the next begin?" DDD is the most reliable answer we have. Get the boundaries right and services stay independent; get them wrong and you build a distributed monolith that is painful to change.

Ubiquitous language: name things the way the business does

The first DDD idea costs nothing and pays off immediately. Use one shared vocabulary between developers and domain experts, and put that exact vocabulary into the code.

If the warehouse team says "a shipment is dispatched", your code has a Shipment with a dispatch() method — not an UpdateStatusService that sets a status integer to 3. When the words in the code match the words in the business, conversations stop needing translation and bugs caused by misunderstanding shrink.

Pro tip: The fastest DDD win on any team is banning vague technical names like DataManager or ProcessHandler in the domain layer and replacing them with the words your business users actually say out loud in meetings.

Bounded context: where a model is true

Here is the idea that unlocks microservice boundaries. A bounded context is a boundary inside which a particular model and its terms are consistent and unambiguous.

The word "customer" is a good example. In the Sales context a customer has a pipeline stage and a lead score. In the Billing context the same person is an account with a payment method and an invoice history. In the Support context they are a ticket requester with a satisfaction rating. These are genuinely different models. Forcing them into one shared Customer class creates a bloated object that every team fights over.

Instead, each context keeps its own model. In a microservices system, each bounded context usually becomes one service, owning its own data and exposing an API. This is why DDD and microservices architecture are so often taught together — the context boundary is the service boundary.

Aggregates: the unit of consistency

Within a context, not every object is independent. Some objects must change together to stay valid. An Order and its OrderLine items are a good example: you should never add a line that pushes the order total above a credit limit, and that rule spans both.

DDD groups such objects into an aggregate, with one entity — the aggregate root — as the single entry point. All changes go through the root, which enforces the invariants. Here is an Order aggregate root in Java:

public class Order {

    private final OrderId id;
    private final List<OrderLine> lines = new ArrayList<>();
    private OrderStatus status = OrderStatus.DRAFT;

    public Order(OrderId id) {
        this.id = id;
    }

    // The only way to add a line — the root enforces the rules
    public void addLine(ProductId product, int quantity, Money price) {
        if (status != OrderStatus.DRAFT) {
            throw new IllegalStateException("Cannot modify a submitted order");
        }
        if (quantity <= 0) {
            throw new IllegalArgumentException("Quantity must be positive");
        }
        lines.add(new OrderLine(product, quantity, price));
    }

    public Money total() {
        return lines.stream()
                .map(OrderLine::subtotal)
                .reduce(Money.ZERO, Money::add);
    }
}

Outside code never touches an OrderLine directly; it calls order.addLine(...). Because the root guards every change, the order can never end up in an invalid state. This is also why an aggregate is the natural unit of a database transaction inside a single service — you save the whole aggregate atomically. Persisting aggregates cleanly is where Spring Data JPA fits in.

A worked decomposition

Suppose you are asked to split a monolithic booking platform. A layer-driven engineer might propose a "database service", a "business logic service", and a "UI service". That is the distributed monolith trap — every feature change touches all three.

A DDD-driven decomposition instead finds the contexts. Talking to the business, you discover four cohesive areas: Catalog (what can be booked), Reservations (holding and confirming a slot), Payments (charging and refunds), and Notifications (emails and reminders). Each becomes a service:

services:
  catalog-service:
    owns: [venues, availability]
    database: catalog_db
  reservation-service:
    owns: [reservations, holds]
    database: reservation_db
  payment-service:
    owns: [charges, refunds]
    database: payment_db
  notification-service:
    owns: [email_templates, sent_log]
    database: notification_db

Notice each service owns its own database. A reservation confirmation now spans two services — Reservations and Payments — which no single transaction can cover. That gap is exactly what the saga pattern solves, and it is the predictable cost of splitting by context. DDD does not remove that cost; it makes sure you only pay it at genuine business seams, not arbitrary technical ones.

Context mapping: how contexts talk to each other

Once you have several bounded contexts, you have to decide how they relate, because they still need to exchange data. DDD calls this a context map, and a few relationship types come up constantly.

  • Customer–supplier. One context depends on another and can influence its API, like Reservations depending on Catalog.
  • Conformist. A downstream context simply accepts an upstream model it cannot change, such as integrating with a third-party payment provider.
  • Anti-corruption layer. A downstream context wraps a messy or foreign model in a translation layer so the ugliness never leaks into its own clean model.

The anti-corruption layer is the one to remember. When you integrate with a legacy system or an external vendor whose model is nothing like yours, you build a thin adapter that translates their concepts into yours at the boundary. Your domain stays clean; the mess is quarantined in one place. Skipping this is how a third-party's awkward data model slowly infects every service that touches it.

Entities versus value objects

Inside an aggregate, DDD distinguishes two kinds of objects, and the distinction changes how you write code. An entity has an identity that persists over time — an Order with a specific id is the same order even after its contents change. A value object has no identity and is defined entirely by its attributes — a Money of 500 rupees is interchangeable with any other 500 rupees.

The practical payoff is that value objects should be immutable. You never mutate a Money; you compute a new one. This removes a whole class of bugs where a shared object is changed under your feet. Modeling amounts, dates, and addresses as immutable value objects rather than bare strings and integers is one of the highest-value habits DDD teaches, and it costs almost nothing to adopt.

When DDD is overkill

Be honest about the ceremony. DDD earns its keep when the domain is rich with rules — insurance underwriting, logistics routing, financial settlement. There the modeling effort prevents expensive mistakes.

For a straightforward CRUD app that mostly stores and returns forms, the full apparatus of aggregates and repositories adds friction without much payoff. Take the cheap, high-value parts — ubiquitous language and clear boundaries — and skip the heavy tactical patterns. As with the monolith vs microservices decision, matching the tool to the complexity is the whole skill.

Common mistake: Treating every noun as its own microservice. If two "contexts" always change together and can never be released independently, they are one context wearing two hats. Over-splitting produces chatty services and constant cross-service transactions.

Start with the monolith, discover the boundaries

A hard-won lesson from teams who tried to design perfect bounded contexts up front: you rarely get them right on paper. The domain reveals its true seams only after you have built and lived with it for a while. Contexts you were sure were separate turn out to always change together; one you thought was a single context splits cleanly in two once you understand the business better.

This is why many experienced teams build a modular monolith first, using DDD to keep clean internal boundaries between contexts — separate packages, no shared tables — but without the network in between. When a boundary has proven stable and a real pressure appears, they extract it into a service. The clean internal boundaries make that extraction almost mechanical. Trying to guess every context boundary before writing code usually produces the wrong split, and a wrong service boundary is far more expensive to fix than a wrong package boundary.

Interview relevance

Microservices interviews love the boundary question: "how did you decide what became a service?" A weak answer says "we split by feature". A strong answer talks about bounded contexts, shows you looked for where the ubiquitous language changes meaning, and admits that some boundaries only became clear after living with the monolith for a while. Mentioning that aggregates gave you your transaction boundaries signals real design experience.

DDD is the front door to microservice design. Pair it with what are microservices for the foundations, then study the saga pattern to handle the cross-context consistency that clean boundaries inevitably create. Boundaries first, patterns second — that order keeps systems sane.

Frequently Asked Questions

What is a bounded context in DDD?
A bounded context is a boundary within which a particular domain model and its vocabulary are consistent and unambiguous. The word 'customer' can mean different things in billing and in support, and each meaning lives inside its own context. In microservices, a bounded context is the strongest candidate for a service boundary.
What is an aggregate in domain-driven design?
An aggregate is a cluster of related objects treated as a single unit for data changes, with one entity called the aggregate root as the only entry point. All changes go through the root, which enforces the invariants that must always hold. An aggregate is also the natural unit of a transaction inside one service.
How does DDD help with microservices?
DDD gives you a principled way to draw service boundaries instead of guessing. Each bounded context maps cleanly to a service that owns its model and data, which is exactly the independence microservices need. Splitting by technical layer instead of by domain is the classic mistake DDD helps you avoid.
What is ubiquitous language?
Ubiquitous language is a shared, precise vocabulary used by both developers and domain experts, reflected directly in the code. If the business says 'reservation', the class is called Reservation, not BookingDTO. It removes translation errors between what the business means and what the code does.
Do I need DDD for every project?
No. DDD pays off when the domain is complex and full of business rules, such as insurance, logistics, or payments. For simple CRUD applications the ceremony adds cost without much benefit. Use the parts that help — ubiquitous language and clear boundaries — and skip the heavy machinery when the domain is thin.

Want to Build Your Career in Java Full Stack with AI?

Join CodeBegun and train with working industry engineers — Explore the Program

Apply for Demo Class →
Siva Prasad Galaba
Founder, CodeBegun · Staff Engineer

Founder of CodeBegun. 15+ years building Java systems at companies like Crunchyroll. Teaches Java, Spring Boot and system design the way the industry actually works, and mentors students through projects, mock interviews and placement preparation.

Technically reviewed by CodeBegun Technical TeamLast reviewed 15 July 2026 LinkedIn
Chat with us