Chapter 5: Designing Business Logic in a Microservice Architecture

How to organize business logic using aggregates, enforce boundaries, publish domain events, and structure services like Kitchen and Order.

4 Patterns 8 Definitions 5 Principles 3 Problems 2 Tradeoffs 2 Technologies 24 Total Concepts

5.1 Business logic organization patterns

Every service has business logic at its core. How you organize that logic has a big impact on how easy the service is to understand, test, and change. There are two main patterns for organizing business logic: Transaction Script and Domain Model. They are alternatives to each other.

Designing business logic using the Transaction script pattern

The Transaction Script pattern organizes business logic as a set of procedural methods. Each method handles one request from start to finish. The behavior (what to do) and the state (the data) live in separate places: methods contain the logic, and simple data objects hold the state.

Pattern: Transaction Script

Structure Procedural methods that each handle one request. State and behavior are separated.
Benefit Simple to write and understand for straightforward logic.
Drawback Scales poorly as business logic grows complex. Methods become long and hard to maintain.
Alternative Domain Model pattern.

Analogy: Think of a recipe written as one long list of steps. It works for a simple dish. But for a complex banquet with many dishes, a single long list becomes confusing. You need a better way to organize the work.

Designing business logic using the Domain model pattern

The Domain Model pattern organizes business logic as an object-oriented model. Classes have both state and behavior together. Service methods are thin — they delegate most of the work to domain objects.

Pattern: Domain Model

Structure Object-oriented classes where each class has both state and behavior.
Benefit Extensible and easy to test. New behavior can be added by creating new classes or extending existing ones.
Drawback More complex upfront than Transaction Script.
Alternative Transaction Script pattern.
Aspect Transaction Script Domain Model
Style Procedural Object-oriented
State & behavior Separated Together in classes
Simple logic Good fit Overkill
Complex logic Scales poorly Good fit
Extensibility Hard to extend Easy to extend

Key point: For microservices with complex business logic, the Domain Model pattern is usually the better choice. It is refined further by Domain-Driven Design (DDD) and the Aggregate pattern.

About Domain-driven design

Domain-Driven Design (DDD) is an approach to building software that closely models the business domain. DDD provides a set of building blocks for creating domain models:

The most important DDD building block for microservices is the Aggregate. It solves several problems that arise when a domain model is used across service boundaries.

Recall from Chapter 2: A Bounded Context means each service has its own domain model. The same real-world concept (like "Order") can have a different representation in each service. DDD aggregates work within these bounded contexts.

5.2 Designing a domain model using the DDD aggregate pattern

A traditional domain model is a web of interconnected objects. In a monolith, this works fine. In a microservice architecture, fuzzy boundaries between objects cause serious problems. The Aggregate pattern solves this by drawing explicit boundaries around groups of objects.

The problem with fuzzy boundaries

Without clear boundaries, a domain model is just a web of interconnected classes. This causes three problems in a microservice architecture:

  1. Cross-service reference problem — if objects freely reference each other, it is unclear which object belongs to which service. An object in Service A might hold a direct reference to an object in Service B. This creates tight coupling.
  2. Transaction scope problem — in a monolith, you can update many objects in a single database transaction. In microservices, each service has its own database. You cannot use a single transaction to update objects across services.
  3. Invariant enforcement problem — business rules that span multiple objects (like "the order total must equal the sum of line item prices") are hard to enforce when it is unclear which objects must be updated together.

Problem: A web of interconnected objects without explicit boundaries leads to tight coupling between services, unclear transaction scope, and broken business rules. The Aggregate pattern exists to solve these three problems.

Aggregates have explicit boundaries

An Aggregate is a cluster of domain objects that are treated as a single unit. Every aggregate has a root entity (the aggregate root) that is the only object outsiders can reference. All other objects inside the aggregate are accessed only through the root.

Pattern: Aggregate

What A cluster of domain objects with a root entity, treated as one unit for data changes.
Problem Fuzzy boundaries, cross-service references, and unclear transaction scope.
Solution Group related objects, enforce all access through the root, and limit transactions to one aggregate.
Benefit Loose coupling, clear invariants, fits the microservice model.

Analogy: Think of an aggregate as a household. The household has one main address (the root). Outsiders send mail to the household address, not directly to individual family members. The household manages its internal affairs on its own.

Aggregate rules

Three rules govern how aggregates work. These rules enforce loose coupling and consistency in a microservice architecture.

Rule #1: Reference only the aggregate root

From outside the aggregate, you can only reference the aggregate root. You cannot directly access or hold references to internal objects. This ensures the root can enforce all business invariants before any change is made.

Rule #2: Inter-aggregate references by primary key

Aggregates reference each other using primary keys (IDs), not direct object references. This keeps aggregates loosely coupled. It also makes the model friendly for NoSQL databases and for sharding data across multiple machines. There is no risk of accidentally creating a cross-service object reference.

Rule #3: One transaction creates or updates one aggregate

A single database transaction must only create or update one aggregate. If a business operation needs to update multiple aggregates, you must use a Saga (Chapter 4) to coordinate the updates across separate transactions.

Why Rule #3 fits microservices: Each service has its own database. You cannot have a single ACID transaction span two services. By limiting each transaction to one aggregate, the model naturally fits the one-database-per-service constraint. Cross-aggregate consistency is handled by sagas.

Rule What it says Why it matters
#1 Reference only the aggregate root from outside Enforces business invariants
#2 Inter-aggregate references by primary key Loose coupling, NoSQL/sharding friendly
#3 One transaction per aggregate Fits microservice model; use sagas for multi-aggregate updates

Aggregate granularity

How big should an aggregate be? There is a tradeoff between fine-grained and coarse-grained aggregates.

Aspect Fine-grained aggregates Coarse-grained aggregates
Size Small, focused Large, contains more objects
Scalability Better (less contention) Worse (more contention on the root)
Atomic updates Needs sagas for multi-aggregate operations Can update more objects in one transaction
Complexity More sagas to coordinate Simpler within a single aggregate

Guideline: Make aggregates as small as possible. This improves scalability and reduces contention. Accept the extra complexity of sagas for operations that span multiple aggregates.

Designing business logic with aggregates

Business logic in a microservice is organized around aggregates. A typical service contains:

The service follows a hexagonal architecture. The business logic (aggregates, services, repositories, sagas) sits in the core. Around the core, there are two types of adapters:

Architecture summary: Inbound adapters → Business logic core (Aggregates + Domain Services + Repositories + Sagas) → Outbound adapters. The core does not depend on external technologies.

5.3 Publishing domain events

Aggregates publish domain events when their state changes. Other services subscribe to these events to keep their own data up to date. This section covers the full lifecycle of domain events: why they exist, what they look like, and how to generate, publish, and consume them.

Why publish change events?

In a microservice architecture, services need to react to changes in other services. For example, when an order is created, the Kitchen Service needs to know about it so it can prepare the food. Domain events make this possible.

Common reasons to publish events:

What is a domain event?

A domain event is a message that describes something that happened to an aggregate. It is named using a past-participle verb, like OrderCreated, TicketAccepted, or DeliveryPickedUp.

Each event typically contains:

Naming convention: Always name domain events with a past-participle verb. This makes it clear that the event describes something that already happened: OrderCreated, not CreateOrder.

Event enrichment

Event enrichment means including extra data in the event that consumers will need. Instead of just publishing "OrderCreated" with only the order ID, you also include the order details (items, total, customer ID, etc.).

Approach Benefit Drawback
Minimal event (ID only) Event is stable — does not change when the aggregate changes Consumer must call back to the source service for details
Enriched event (includes data) Consumer has everything it needs — simpler and more self-contained Event structure may need to change when the aggregate changes

Tradeoff: Enriched events are simpler for consumers because they avoid extra network calls. But they couple the event structure to the aggregate's internal data, which can reduce stability. Choose based on your needs.

Identifying domain events

How do you find the domain events your system needs? Two techniques help:

Requirements analysis

Look for requirements that follow the pattern: "When X happens, do Y." The "X" part is a domain event. For example: "When an order is created, notify the kitchen" tells you that OrderCreated is a domain event.

Event Storming

Event Storming is a workshop technique. Domain experts and developers work together to map out all the events in a business process. They write events on sticky notes, put them on a timeline, and then identify the commands and aggregates that produce them. It is a fast way to discover the events in a system.

Generating and publishing domain events

There are two common ways for an aggregate to generate domain events:

Method return approach

The aggregate method that changes state returns a list of events. The service class then passes these events to the publisher. This is the preferred approach because it keeps the aggregate simple and free of infrastructure dependencies.

Accumulation approach

The aggregate extends an AbstractAggregateRoot base class that maintains an internal list of events. When the aggregate's state changes, it adds events to this list. The service class collects the accumulated events after calling the method. This approach is slightly more convenient but ties the aggregate to a base class.

Publishing events reliably

Events must be published reliably. If the service updates the database but fails to publish the event (or vice versa), the system becomes inconsistent. The solution is the Transactional Outbox pattern (Chapter 3).

The service inserts the event into an OUTBOX table as part of the same ACID database transaction that updates the aggregate. A separate process reads the OUTBOX table and publishes the events to the message broker. This guarantees that the database update and the event publication are atomic.

For type safety, a service can use a typed publisher like AbstractAggregateDomainEventPublisher<A, E> that is parameterized with the aggregate type (A) and the event type (E). This prevents accidentally publishing the wrong type of event for an aggregate.

Critical: Never publish events directly to the message broker outside of a database transaction. Always use the Transactional Outbox pattern to guarantee atomicity between database updates and event publishing.

Consuming domain events

A service consumes domain events using a DomainEventDispatcher. The dispatcher subscribes to event channels and routes each incoming event to the correct handler method based on the event type.

Consuming events is an inbound adapter in the hexagonal architecture. The handler method receives the event, looks up the relevant aggregate, and calls a method on it to react to the event.

Pattern chain: Aggregate state change → generates domain event → published via Transactional Outbox → delivered through message broker → consumed by DomainEventDispatcher → handled by target service.

5.4 Kitchen Service business logic

The Kitchen Service is a relatively simple service. It participates in sagas started by other services (like the Order Service) but does not orchestrate sagas itself. It handles commands, updates its aggregates, and publishes domain events.

The Ticket aggregate

The central aggregate in Kitchen Service is the Ticket. A Ticket represents a kitchen order — the food items that need to be prepared. The Ticket aggregate:

The Kitchen Service follows the hexagonal architecture:

Analogy: Think of the Ticket as a paper order slip in a restaurant kitchen. A waiter (saga command) places the slip. The kitchen reads it, prepares the food, and marks the slip as done. The kitchen does not decide what to cook — it just follows orders.

5.5 Order Service business logic

The Order Service is more complex than the Kitchen Service. It orchestrates sagas that span multiple services. It also maintains replicas of data from other services (like restaurant menu data) by subscribing to their domain events.

The Order Aggregate

The Order aggregate is the central aggregate in Order Service. It represents a customer's order and contains order line items, the order total, and the current order state.

PENDING semantic locks

Because the Order Service uses sagas, the Order aggregate uses *_PENDING states as semantic locks. For example, when a saga is creating an order, the order is in APPROVAL_PENDING state. While in a PENDING state, certain operations are blocked to prevent conflicting updates. Once the saga completes, the state moves to a final value like APPROVED or REJECTED.

Recall from Chapter 4: Semantic locks are a saga countermeasure against isolation anomalies. A *_PENDING state signals that the aggregate is currently being modified by a saga, so other operations should wait or fail.

Restaurant replica

The Order Service needs restaurant data (like menu items and prices) to validate orders. Instead of calling the Restaurant Service synchronously, the Order Service maintains a local Restaurant aggregate that is a replica of data from the Restaurant Service. It keeps this replica up to date by subscribing to domain events published by the Restaurant Service (e.g., RestaurantMenuUpdated).

Microservice-specific pattern: Services maintain replicas of other services' data by subscribing to domain events. This eliminates synchronous dependencies and improves availability. The trade-off is eventual consistency — the replica may be slightly behind the source.

The OrderService class

The OrderService class is the main entry point for Order Service business logic. Most of its methods create sagas rather than directly updating the Order aggregate. This is because order operations (create, revise, cancel) involve multiple services that must be coordinated.

For example, when a customer creates an order:

  1. The OrderService.createOrder() method creates an Order aggregate in APPROVAL_PENDING state.
  2. It then creates a CreateOrderSaga that orchestrates the steps across Kitchen Service, Accounting Service, and others.
  3. The saga sends commands to these services and waits for replies.
  4. When all steps succeed, the saga updates the Order to APPROVED. If any step fails, compensating transactions roll back the changes and the Order becomes REJECTED.

The Order Service follows the hexagonal architecture:

Analogy: The OrderService is like a project manager. It does not do all the work itself. Instead, it starts a project (saga), assigns tasks to different teams (services), tracks progress, and decides the final outcome based on whether all teams succeed.

Key microservice-specific differences

Designing business logic for microservices differs from designing it for a monolith in several important ways:

Summary: The aggregate pattern, primary-key references, single-aggregate transactions, domain events, and data replicas are the five key differences between monolithic and microservice business logic design.

Key Takeaways