Domain Driven Design: Bounded Context, Aggregate, and Ubiquitous Language in Practice
> cd .. / HUB_EDITORIALE
Sviluppo di siti web

Domain Driven Design: Bounded Context, Aggregate, and Ubiquitous Language in Practice

[2026-06-08] Author: Ing. Calogero Bono
> share
Zenithby Meteora Web The operating system for your business. Social, clients, bookings and invoices in one platform. Gyms, barbers, professionals. Discover Zenith Free demo · no card

You have microservices, but your teams still argue about what a 'customer' is. The database is shared, APIs speak different models, and code looks like a mess of mixed concepts. You moved to a distributed architecture, but the domain mindset stayed monolithic. That's the problem we solve here.

We, at Meteora Web, see this every day in projects we inherit from clients who already 'did microservices'. They split the code, but not the reasoning. Domain-Driven Design is not an academic luxury: it's the tool that turns complexity into clarity, and the numbers prove it. We managed the ERP system of a clothing store from the inside – seasons, discounts, margins – and we know the cost of ambiguous language. A 'return' for the warehouse is not the same 'return' for accounting. Mix them and you lose money.

In this guide we tackle the three operational pillars of DDD that really matter to anyone building microservices: Ubiquitous Language, Bounded Context, and Aggregate. No useless theory: real examples, working code, decisions to make.

Ubiquitous Language: the tongue that binds business and code

Imagine a restaurant: the waiter says 'table 4 ordered a grilled chicken'. In the kitchen, the same dish is 'guest 4, chicken, no gluten, boneless'. If waiter and cook used two different dictionaries, every order would be wrong. Replace 'waiter' with the Product Owner and 'cook' with the developer, and you understand what Ubiquitous Language is.

In our daily work, we always start with a session with the client to write the domain glossary. We take a business process, for example selling a product on an e-commerce, and give a unique name to each concept: 'Order', 'Cart', 'Customer', 'Payment', 'Shipping'. Then we use those same words in code: classes, methods, tables, events. If the client calls 'order' what in accounting is 'customer transaction', we choose one word and stick with it. Once set, language is not negotiable.

Sponsored Protocol

Warning: common mistake — thinking Ubiquitous Language is just a shared document. It's not. It's a living commitment: in meetings, code comments, API names. If the developer names a variable $cliente and the domain calls it 'Buyer', there's a crack. We solve it immediately: rename or update the glossary, but don't live with ambiguity.

What to do right now

Take an hour with your team (or client) and write 5–10 domain terms you use most often. For each: unique definition, examples, synonyms to avoid. Then send a team email: 'From today, in code and conversations, we only use these words.' The cost of a linguistic error in production is always higher than an alignment meeting.

Bounded Context: the boundary that saves the project

Have you ever worked on a system where the 'Product' class has 50 fields, half used by the catalog and half by the warehouse? That's an omnivore model, symptom of missing Bounded Context. In DDD, each bounded context owns its model: in the 'Catalog' context, a product has name, description, price, images. In the 'Warehouse' context, the same product has quantity, location, lot. They are two different concepts, even if they refer to the same physical object.

Sponsored Protocol

We built a proprietary platform for managing social media for multiple clients. We clearly separated contexts: 'Publishing' (calendar, drafts, content), 'Analytics' (metrics, reports), 'Billing' (clients, prices, payments). Each context has its own team (even one person), its own database, its own logic. Bounded Context is not just technical: it's organizational. If two teams need to synchronize on the same model, the boundary is wrong.

How to identify them? Look for words that change meaning. In a clothing e-commerce, 'size' for the customer is an aesthetic choice; for the warehouse it's a shelf code. Two different contexts. Draw a Context Map: draw rectangles for contexts and relationships (Customer/Supplier, Partnership, Shared Kernel). You don't need a complex tool – a whiteboard or paper diagram is enough.

Practical exercise

Take your project and identify at least two concepts used differently by two parts of the system. Define a bounded context for each. Then verify: do the responsible teams speak the same language? Are databases separate? If not, you've found the first candidate for refactoring.

Aggregate: the unit of consistency

So far we talked about language and boundaries. But inside a context, how do we manage data that must stay consistent? Enter the Aggregate: a cluster of domain objects treated as a single unit for changes. The Aggregate Root is the only entry point: every change goes through it, which validates business invariants.

Practical example: an order and its lines. An 'Order' is the Aggregate Root; 'OrderLine' is an internal entity. You cannot add a line to a cancelled order. You cannot modify a line without the order knowing. The Aggregate ensures business rules are always enforced.

Sponsored Protocol

In our work, we often see the opposite problem: giant aggregates – an order with dozens of lines, notes, shipments, payments, returns. If every change loads everything, performance collapses. The golden rule: an aggregate should be small enough to be modified in one transaction, yet big enough to maintain invariants. At Meteora Web we use a simple criterion: if two entities can be modified independently without violating business rules, they are separate aggregates.

Here's a code example in PHP (Laravel-like) of an Aggregate Root for an order:

class Order extends AggregateRoot
{
    private string $orderId;
    private string $status; // 'pending', 'confirmed', 'shipped'
    private array $items; // array of OrderItem
    private float $total;

    public function addItem(Product $product, int $quantity): void
    {
        if ($this->status !== 'pending') {
            throw new DomainException('Cannot add items to a non-pending order.');
        }
        $this->items[] = new OrderItem($product->getId(), $quantity, $product->getPrice());
        $this->recalculateTotal();
    }

    public function confirm(): void
    {
        if ($this->status !== 'pending') {
            throw new DomainException('Order already processed.');
        }
        if (count($this->items) === 0) {
            throw new DomainException('Cannot confirm an empty order.');
        }
        $this->status = 'confirmed';
        // Record event for other contexts to listen
        $this->recordEvent(new OrderConfirmed($this->orderId));
    }

    private function recalculateTotal(): void
    {
        $this->total = array_reduce($this->items, fn($carry, $item) => $carry + $item->getSubtotal(), 0);
    }
}

Note: the aggregate does not expose its internals. Modification happens only through methods that express business intent. If an external service wants to know the total, it calls a getter, but never modifies properties directly.

Sponsored Protocol

When to use two aggregates

Think about payment and order. A payment can be created without an order? No. But can it be modified (refund) without changing the order status? It depends. If the refund does not touch the order's invariants (e.g., status 'returned' if partially refunded), then they are two separate aggregates linked by an ID. Rule of thumb: one aggregate per transaction.

How DDD drives microservices

Each microservice should own a Bounded Context. Inside it, one or more Aggregates. Communication between microservices happens via domain events, not direct calls that expose internals. Owning your own stack beats renting it: each context has its own database, model, logic. Data is never shared at the table level but only through messages.

Example: when an order is confirmed, the Order service emits an OrderConfirmed event. The Payment service listens and creates a payment in its context. The Billing service issues an invoice. No one sees another's orders table. This is true decoupling.

Sponsored Protocol

We, at Meteora Web, applied this pattern in a proprietary platform: each context (social, analytics, billing) publishes events to a message broker (Redis Streams or RabbitMQ). Clients don't have to integrate with synchronous APIs unless strictly necessary. The economic benefit? Less cascading downtime, independent scaling, and each team evolves its module without breaking others.

What to do right now

Take an existing microservice and map its context boundaries. Identify candidate aggregates. For each, write down the invariants: 'An order cannot be shipped if not paid' (example). Then check if your code enforces them. If not, you've found technical debt to repay. DDD doesn't have to be implemented all at once: start with one critical context, then expand.

In summary — what to do

  • Build Ubiquitous Language with the business: shared glossary, used in code and meetings.
  • Identify Bounded Contexts: where terms change meaning, draw a clean boundary.
  • Define Aggregates as units of consistency, with a root protecting invariants.
  • Implement event-based communication between contexts, never direct data sharing.
  • Refactor gradually: don't rewrite everything. Pick one context, apply DDD, and measure results.

For deeper reading, check Eric Evans' book or Martin Fowler's article on DDD. And remember: a project without DDD is like a store without inventory: it looks nice, but you never know what you have.

> share
Ing. Calogero Bono

> AUTHOR_EXTRACTED

Ing. Calogero Bono

Ingegnere informatico, fondatore di Meteora Web e Zenith OS. System administrator e progettista di piattaforme, app e CMS proprietari, con esperienza in sviluppo full-stack, marketing digitale ed ecosistema Google.
[ Read Full Dossier ]

> METEORA_WEB // DIGITAL AGENCY

We build the digital presence your business deserves.

Websites, social media, online advertising, e-commerce and high-performance hosting, engineered with method by computer engineers in Sciacca, for all of Italy.

> MW_JOURNAL

> READ_ALL()