Domain-Driven Design (DDD) in Microservices
Microservices have become a popular way to build scalable and flexible systems. But one question decides whether they succeed: where do you draw the lines between services? Draw them wrong, and you get a tangle of services that cannot change without each other. Domain-Driven Design (DDD) gives you a reliable way to draw those lines around the business itself.
This guide explains DDD in simple terms, from strategic ideas such as bounded contexts and context mapping to tactical building blocks such as entities, value objects, aggregates and domain events, with a complete e-commerce example and formatted Java code.
1. Why Microservices Need Domain-Driven Design
A common pitfall is to split a large application into small, technical services, such as a “User Service”, “Product Service” and “Order Service”, without thinking about how the business actually works. The result is often a tangled web of dependencies:
- One business change requires changes in several services at once.
- Services constantly call each other to get the data they need.
- Teams step on each other’s toes because responsibilities overlap.
This is where Domain-Driven Design comes in. It is not a technology or a framework, but an approach to software design that aligns your code with the business reality it represents. Combined with microservices, DDD provides the blueprint for services that are not just small, but also meaningful, autonomous and resilient.
2. What Is Domain-Driven Design?
At its heart, DDD is a philosophy:
The domain is the area of business your software serves, such as online retail, banking or logistics. DDD provides a set of patterns and a shared vocabulary that bridge the gap between developers and domain experts, the people who know how the business works.
The approach was introduced by Eric Evans in his 2003 book on the subject and has since become one of the most widely used ways to design complex business software.
3. Strategic and Tactical Design
| Strategic design | Tactical design | |
|---|---|---|
| Question | How do we split the system? | How do we model the code inside one part? |
| Main concepts | Ubiquitous language, subdomains, bounded contexts, context maps | Entities, value objects, aggregates, domain events, repositories |
| Level | Whole system and teams | Classes and objects inside one service |
| Who takes part | Architects, developers and business experts together | Mainly developers, using the agreed language |
| Effect on microservices | Decides the service boundaries | Decides how each service’s business logic is built |
For microservices, strategic design matters most: good boundaries make everything else easier. Tactical patterns then keep the code inside each service clean.
4. Ubiquitous Language
The ubiquitous language is a common, precise language shared by the development team and the domain experts. The same words, such as Order, Account or Shipment, are used in conversations, documents, diagrams and code.
| Without a ubiquitous language | With a ubiquitous language |
|---|---|
The business says “cancel the order”; the code calls setStatus(4) | The code says order.cancel() |
| “Customer”, “client”, “user” and “account holder” mean the same thing, or maybe not | One agreed word per concept, written down in a glossary |
| Developers translate every requirement in their heads | Requirements map directly to classes and methods |
When code reads like the business talks, misunderstandings drop sharply, and domain experts can even review the model with you.
5. Domains and Subdomains
A large domain is split into subdomains: smaller areas of the business. DDD sorts them into three kinds, which helps decide where to invest effort:
| Kind of subdomain | Meaning | E-commerce example | Strategy |
|---|---|---|---|
| Core | What makes the business special and gives it an edge | Pricing and promotions, recommendations | Build in-house with your best people and deepest modeling |
| Supporting | Needed and specific to you, but not a differentiator | Order management, returns | Build simply, or customize existing tools |
| Generic | Common problems every company has | Login, payments, email sending | Buy or use an existing product or service |
6. Bounded Contexts
A bounded context is the cornerstone of DDD. It is a clear boundary within which a particular model, and its part of the ubiquitous language, applies.
The same word can mean different things in different parts of a business. A “Customer” in the Shipping context has a delivery address and delivery preferences. A “Customer” in the Loyalty context has points and a membership level. Instead of forcing one giant Customer class that tries to serve everyone, each context keeps its own model.
A bounded context explicitly says: “Inside this fence, this word means this.”
+------------------------+ +------------------------+
| Shipping Context | | Loyalty Context |
| | | |
| Customer | | Customer |
| - deliveryAddress | | - points |
| - deliveryWindow | | - membershipLevel |
+------------------------+ +------------------------+
7. The Perfect Match: DDD and Microservices
The magic happens when we map one bounded context to one microservice. This alignment ensures:
- High cohesion: everything related to one business capability is packaged together in one service.
- Loose coupling: each service has a clear boundary and owns its data. Services talk through clean APIs and events, never by reading each other’s databases.
- Team ownership: one team can own one context end to end.
This prevents the distributed monolith anti-pattern, where services are technically separate but so tightly coupled that a change in one forces changes in many others.
One context per service is a strong starting point, not an absolute rule. A small context can share a deployment with a neighbor, and a very large context with different scaling needs can be split further. The context boundary should never be crossed by shared code or a shared database.
8. A Detailed Example: The E-Commerce Platform
Let us design an online store using DDD principles. Without DDD, we might create a giant User service that handles everything from login to orders to invoices, which quickly becomes a bottleneck. Instead, we talk to the business experts, identify the subdomains, and define our bounded contexts.
8.1 Step 1: identify bounded contexts
| Bounded context | Responsibility |
|---|---|
| Identity & Access | User registration, login and basic profile: username, password and email |
| Order | Creating and managing orders: adding items, calculating totals and tracking status |
| Shipping | Logistics: creating shipments, assigning carriers and providing tracking |
| Payment | Processing payments, handling transactions and managing invoices |
8.2 Step 2: define the ubiquitous language in each context
Notice how similar words have different meanings in different contexts:
Userin the Identity context:{ username, passwordHash, email, verified }. Its main job is authentication.Customerin the Order context:{ customerId, shippingAddress, billingAddress }. The password does not matter here, only what is needed to fulfill an order.Orderin the Order context: a rich object with a life cycle (Created, Paid, Shipped). It contains order lines and enforces business rules, such as “items cannot be added after payment”.
8.3 Step 3: design the microservices
| Service | Example endpoints |
|---|---|
identity-service | POST /register, POST /login, GET /users/{id}/profile |
order-service | POST /orders, GET /orders/{id} |
shipping-service | POST /shipments, GET /shipments/{id}/tracking |
payment-service | POST /payments |
8.4 Step 4: establish context mapping (how they communicate)
Here is how the services work together in a “Place Order” workflow:
- The front end calls the
order-serviceto create an order, sending thecustomerIdand the list of items. - The order service needs the customer’s shipping address, which it does not store. It calls the identity service’s profile API and translates the result into its own
Customermodel, using an anti-corruption layer (section 14). - It calculates the total and saves the order in its own database with the status
PENDING_PAYMENT. - It publishes an
OrderPlacedevent to a message broker such as Kafka or RabbitMQ, containing the order ID, customer ID and total amount. - The
payment-servicereceives the event, processes the payment and publishes aPaymentConfirmedevent. - The
order-servicereceivesPaymentConfirmedand changes the order status toPAID. - The
shipping-servicealso receivesPaymentConfirmedand creates aShipmentin its own database, starting delivery.
Front end --> order-service --(API + translation)--> identity-service
|
+-- OrderPlaced --> [ Broker ] --> payment-service
|
+-------- PaymentConfirmed <-----------+
| |
order-service shipping-service
(status = PAID) (create Shipment)
9. Context Mapping Patterns
Contexts must work together, and the relationships between them have names. Choosing the right one makes the dependency between teams explicit:
| Pattern | Meaning | Example |
|---|---|---|
| Partnership | Two teams plan and release together because they depend on each other | Order and Payment teams agree on changes jointly |
| Shared kernel | Two contexts share a small, carefully agreed piece of model or code | A shared Money type; use sparingly |
| Customer–supplier | The upstream team (supplier) considers the downstream team’s (customer’s) needs | Identity provides the profile API that Order needs |
| Conformist | The downstream team simply adopts the upstream model as it is | Using a payment provider’s data model directly |
| Anti-corruption layer (ACL) | The downstream team translates the upstream model into its own | Order translates Identity’s User into its own Customer |
| Open host service / published language | A context offers a stable, documented API or event format for everyone | Payment publishes a versioned PaymentConfirmed event schema |
| Separate ways | Two contexts do not integrate at all | Marketing and Shipping duplicate a little data instead |
Drawing a context map, a simple diagram of all contexts and these relationships, is one of the most useful documents a microservices team can have.
10. Tactical Building Blocks
Inside a bounded context, tactical DDD gives you building blocks for modeling the business logic:
| Building block | What it is | Example |
|---|---|---|
| Entity | An object with an identity that stays the same while its data changes | Order with ID 1001 stays the same order even when its status changes |
| Value object | An immutable object defined only by its values; no identity | Money(49.99, "USD"), Address |
| Aggregate | A cluster of objects treated as one unit for changes, with one root entity as the entry point | Order (root) with its OrderLines |
| Domain event | Something important that happened in the domain | OrderPlaced, PaymentConfirmed |
| Repository | Loads and saves whole aggregates, hiding the database | OrderRepository |
| Domain service | Business logic that does not naturally belong to one entity | Calculating shipping cost across several rules |
| Factory | Creates complex aggregates in a valid state | Order.create(customerId) |
Aggregate rules of thumb:
- Change an aggregate only through its root; never edit an
OrderLinedirectly from outside. - One transaction changes one aggregate. Other aggregates are updated afterwards, usually through domain events.
- Refer to other aggregates by ID, not by holding the object itself.
- Keep aggregates small: only what must be consistent at the same moment.
11. Example: The Order Aggregate in Java
Let us build the Order aggregate from the Order context. First, two value objects. Records make them short and immutable:
package com.example.order.domain;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Objects;
public record Money(BigDecimal amount, String currency) {
public Money {
Objects.requireNonNull(amount, "amount is required");
Objects.requireNonNull(currency, "currency is required");
if (amount.signum() < 0) {
throw new IllegalArgumentException("Money cannot be negative");
}
amount = amount.setScale(2, RoundingMode.HALF_UP);
}
public static Money zero(String currency) {
return new Money(BigDecimal.ZERO, currency);
}
public Money add(Money other) {
if (!currency.equals(other.currency)) {
throw new IllegalArgumentException("Cannot add " + other.currency + " to " + currency);
}
return new Money(amount.add(other.amount), currency);
}
public Money times(int quantity) {
return new Money(amount.multiply(BigDecimal.valueOf(quantity)), currency);
}
}
package com.example.order.domain;
import java.util.UUID;
// Typed IDs stop you from passing a customer ID where an order ID is expected
public record OrderId(UUID value) {
public static OrderId newId() {
return new OrderId(UUID.randomUUID());
}
}
public record CustomerId(String value) {
}
public record ProductId(String value) {
}
An order line is a small value object inside the aggregate, and the order status is an enum:
package com.example.order.domain;
public record OrderLine(ProductId productId, int quantity, Money unitPrice) {
public OrderLine {
if (quantity <= 0) {
throw new IllegalArgumentException("Quantity must be at least 1");
}
}
public Money subtotal() {
return unitPrice.times(quantity);
}
}
public enum OrderStatus {
CREATED, PENDING_PAYMENT, PAID, SHIPPED, CANCELLED
}
Now the aggregate root. All business rules live here, written in the ubiquitous language:
package com.example.order.domain;
import java.time.Instant;
import java.util.ArrayList;
import java.util.List;
public class Order {
private final OrderId id;
private final CustomerId customerId;
private final String currency;
private final List<OrderLine> lines = new ArrayList<>();
private OrderStatus status = OrderStatus.CREATED;
private final List<Object> domainEvents = new ArrayList<>();
private Order(OrderId id, CustomerId customerId, String currency) {
this.id = id;
this.customerId = customerId;
this.currency = currency;
}
// Factory method: a new order always starts in a valid state
public static Order create(CustomerId customerId, String currency) {
return new Order(OrderId.newId(), customerId, currency);
}
public void addItem(ProductId productId, int quantity, Money unitPrice) {
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("Items cannot be added after the order is placed");
}
lines.add(new OrderLine(productId, quantity, unitPrice));
}
public void place() {
if (lines.isEmpty()) {
throw new IllegalStateException("An order needs at least one item");
}
if (status != OrderStatus.CREATED) {
throw new IllegalStateException("Order " + id.value() + " is already placed");
}
status = OrderStatus.PENDING_PAYMENT;
domainEvents.add(new OrderPlaced(id, customerId, total(), Instant.now()));
}
public void markPaid() {
if (status != OrderStatus.PENDING_PAYMENT) {
throw new IllegalStateException("Only orders waiting for payment can be paid");
}
status = OrderStatus.PAID;
}
public void cancel() {
if (status == OrderStatus.SHIPPED) {
throw new IllegalStateException("Shipped orders cannot be cancelled");
}
status = OrderStatus.CANCELLED;
}
public Money total() {
return lines.stream()
.map(OrderLine::subtotal)
.reduce(Money.zero(currency), Money::add);
}
// Returns the events raised so far and clears the list
public List<Object> pullDomainEvents() {
List<Object> events = List.copyOf(domainEvents);
domainEvents.clear();
return events;
}
public OrderId id() { return id; }
public CustomerId customerId() { return customerId; }
public OrderStatus status() { return status; }
public List<OrderLine> lines() { return List.copyOf(lines); }
}
Notice what is not there: no public setters, no way to change a line from outside, and no way to put the order into an invalid state. The rule “items cannot be added after payment” from the ubiquitous language is enforced in exactly one place.
12. Repositories and Application Services
The repository is defined in the domain as a simple interface. How it is stored, with JPA, MongoDB or something else, is an infrastructure detail:
package com.example.order.domain;
import java.util.Optional;
public interface OrderRepository {
Optional<Order> findById(OrderId id);
void save(Order order);
}
An application service coordinates one use case: it loads or creates aggregates, calls their methods, saves them and publishes their events. It contains no business rules of its own:
package com.example.order.application;
import java.math.BigDecimal;
import java.util.List;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.order.domain.*;
@Service
public class PlaceOrderService {
private final OrderRepository orders;
private final ApplicationEventPublisher eventPublisher;
public PlaceOrderService(OrderRepository orders, ApplicationEventPublisher eventPublisher) {
this.orders = orders;
this.eventPublisher = eventPublisher;
}
@Transactional
public OrderId placeOrder(String customerId, List<ItemRequest> items) {
Order order = Order.create(new CustomerId(customerId), "USD");
for (ItemRequest item : items) {
order.addItem(new ProductId(item.productId()), item.quantity(),
new Money(item.unitPrice(), "USD"));
}
order.place();
orders.save(order);
order.pullDomainEvents().forEach(eventPublisher::publishEvent);
return order.id();
}
public record ItemRequest(String productId, int quantity, BigDecimal unitPrice) {
}
}
A typical service is organized in layers: domain (aggregates, value objects, repository interfaces), application (use cases), and infrastructure (database, messaging, REST controllers). The domain depends on nothing else, which keeps it easy to test and change.
13. Domain Events
A domain event records something important that happened, in past tense and in the ubiquitous language:
package com.example.order.domain;
import java.time.Instant;
public record OrderPlaced(OrderId orderId, CustomerId customerId, Money total, Instant placedAt) {
}
Inside the service, a listener can forward the event to other bounded contexts once the transaction has been committed:
package com.example.order.infrastructure;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Component;
import org.springframework.transaction.event.TransactionPhase;
import org.springframework.transaction.event.TransactionalEventListener;
import com.example.order.domain.OrderPlaced;
@Component
public class OrderEventsForwarder {
private final KafkaTemplate<String, OrderPlacedMessage> kafkaTemplate;
public OrderEventsForwarder(KafkaTemplate<String, OrderPlacedMessage> kafkaTemplate) {
this.kafkaTemplate = kafkaTemplate;
}
// Runs only if the order was saved successfully
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void on(OrderPlaced event) {
// Publish a stable, public message format, not the internal domain object
OrderPlacedMessage message = OrderPlacedMessage.from(event);
kafkaTemplate.send("order-placed", message.orderId(), message);
}
}
The published message is part of the context’s published language, so it should be versioned and kept backward compatible. If the application crashes between the commit and sending, the message could be lost; the transactional outbox pattern solves this for critical events. Domain events are the foundation of event-driven architecture and the Saga pattern.
14. Anti-Corruption Layer in Code
The Order context needs a shipping address, which the Identity context provides in its own model. An anti-corruption layer (ACL) translates the foreign model into the Order context’s language, so Identity’s naming and structure never leak into the Order domain:
package com.example.order.infrastructure.identity;
import org.springframework.stereotype.Component;
import com.example.order.domain.Address;
import com.example.order.domain.Customer;
import com.example.order.domain.CustomerId;
@Component
public class CustomerTranslator {
private final IdentityClient identityClient;
public CustomerTranslator(IdentityClient identityClient) {
this.identityClient = identityClient;
}
public Customer loadCustomer(CustomerId customerId) {
// Identity's model: names and fields chosen by another team
IdentityProfileDto profile = identityClient.getProfile(customerId.value());
// Our model: only what the Order context needs, in our own words
Address shippingAddress = new Address(
profile.addressLine1(),
profile.city(),
profile.postCode(),
profile.countryCode());
return new Customer(customerId, profile.displayName(), shippingAddress);
}
}
If the Identity team renames a field, only this one class changes. The rest of the Order context, including the Order aggregate, is protected.
15. Finding Boundaries with Event Storming
How do you discover bounded contexts in practice? A popular workshop technique is Event Storming:
- Bring developers and domain experts together in front of a long wall or online whiteboard.
- Everyone writes domain events on orange sticky notes, in past tense: “Order placed”, “Payment confirmed”, “Parcel delivered”.
- Arrange the events on a timeline and discuss gaps and disagreements.
- Add the commands that cause events (“Place order”), the actors who trigger them, and the policies that react (“When payment is confirmed, create a shipment”).
- Look for clusters of events and changes in language. These clusters are strong candidates for bounded contexts.
A few hours of Event Storming often reveals more about service boundaries than weeks of technical design.
16. Why This DDD-Driven Approach Is Powerful
| Benefit | What it means in practice |
|---|---|
| Autonomy | The shipping team works independently of the payment team and can choose its own technology and database, as long as it respects the published event formats |
| Resilience | If the shipping service is down, its events wait in the broker and are processed when it returns; the rest of the system keeps working |
| Clarity | Boundaries are clear; everyone knows which service owns which part of the business logic |
| Scalability | Each service scales on its own; payments can get more resources during a sale while identity stays the same |
| Maintainability | Code reads like the business, so new developers and domain experts understand it faster |
| Evolvability | A change in one context rarely ripples into others, so the system can grow with the business |
17. When DDD Is Overkill
DDD shines in complex domains with rich business rules. It is often overkill when:
- The application is mostly simple create, read, update and delete screens with little logic.
- The team is very small and the product is still searching for its purpose.
- No domain experts are available to build the shared language with.
Even then, the strategic ideas, especially clear boundaries and a shared language, are cheap and helpful. A good path for many teams is a modular monolith organized by bounded contexts, splitting into microservices later when there is a clear reason. See Monolithic vs Microservices Architecture.
18. Common Mistakes to Avoid
| Mistake | What goes wrong | Better approach |
|---|---|---|
| Splitting services by technical layer or by entity | Every feature needs changes in several services | Split by business capability and bounded context |
| One shared model for the whole company | A giant Customer class that nobody can change safely | Let each context own its own model |
| Sharing a database between contexts | Hidden coupling; services cannot change independently | Each context owns its data; share through APIs and events |
| Anemic domain model | Entities are just getters and setters; rules are scattered across services | Put business rules in aggregates and value objects |
| Huge aggregates | Slow loading and frequent locking conflicts | Keep aggregates small; reference others by ID |
| Designing without domain experts | The model reflects developers’ guesses, not the business | Model together, for example with Event Storming |
| Applying every tactical pattern everywhere | Lots of ceremony for simple features | Use rich modeling where the business is complex; keep simple parts simple |
19. Summary
Domain-Driven Design is the strategic compass that guides the design of microservices. By first understanding the business, agreeing on a ubiquitous language, and dividing the domain into well-defined bounded contexts, you create services that are truly decoupled, business-aligned and built to evolve. Context maps make the relationships between teams explicit, while tactical patterns such as entities, value objects, aggregates and domain events keep the code inside each service clean and expressive. DDD moves you from simply “splitting a monolith” to intentionally designing a system that mirrors your business, which is the key to microservices that are not just small, but also smart and sustainable. Continue with the Saga pattern to see how bounded contexts cooperate on long-running business processes.
20. Frequently Asked Questions (FAQ)
Q1. What is Domain-Driven Design in simple words?
Domain-Driven Design (DDD) is a way of building software that starts from how the business works, not from tables or technology. Developers and business experts agree on a shared language, divide the business into clear areas called bounded contexts, and write code that uses the same words and rules as the business.
The result is software that is easier to understand, change and split into services.
Q2. Is DDD required for microservices?
Not strictly, but it is the most common and reliable way to find good service boundaries. Without some form of domain thinking, teams often split services by technical layer or by database table, which leads to chatty, tightly coupled services.
Even if you use only DDD’s strategic ideas, such as bounded contexts and a shared language, your microservices will usually be much better.
Q3. What is the difference between a domain, a subdomain and a bounded context?
- The domain is the whole business area, such as online retail.
- A subdomain is a part of that business, such as ordering, shipping or payments. It describes the problem.
- A bounded context is a boundary in the software within which one model and its language apply.
Ideally, each subdomain is covered by one bounded context, but the two do not always line up perfectly in real systems.
Q4. Should one bounded context always be one microservice?
It is the best starting point, but not a strict rule.
- A small context can share a deployment with a closely related one, as long as their code and data stay separate.
- A large context with very different scaling or release needs can be split into several services.
What you should never do is spread one context’s model across several teams, or let two contexts share a database.
Q5. What is the difference between an entity and a value object?
| Entity | Value object | |
|---|---|---|
| Identity | Has a unique ID | No ID; defined by its values |
| Equality | Same ID means same entity | Same values means equal |
| Changes | Its data can change over time | Immutable; replaced, not changed |
| Examples | Order, Customer | Money, Address, DateRange |
In Java, records are a natural fit for value objects.
Q6. What is an aggregate, and why does it matter?
An aggregate is a group of objects that must always be consistent together, with one root entity as the only entry point. For example, an Order and its order lines form one aggregate.
It matters because it defines a consistency boundary: all business rules inside an aggregate are checked in one transaction, and outside code cannot bypass them by changing the inner objects directly.
Q7. How big should an aggregate be?
As small as possible, while still protecting the rules that must be true at every moment.
- Include only data that must change together in one transaction.
- Refer to other aggregates by ID, such as
CustomerId, not by holding the object. - Use domain events to update other aggregates afterwards.
Large aggregates load slowly and cause locking conflicts when many users change them at the same time.
Q8. What is an anemic domain model?
An anemic model has classes that only hold data, with getters and setters, while all the business logic lives in separate service classes.
// Anemic: anyone can put the order into an invalid state
order.setStatus(OrderStatus.PAID);
// Rich: the rule is checked inside the aggregate
order.markPaid();
DDD prefers rich models, where entities and aggregates enforce their own rules. That keeps rules in one place and makes invalid states impossible.
Q9. What is an anti-corruption layer?
An anti-corruption layer (ACL) is a translation layer between your bounded context and another system whose model you do not control, such as another team’s service or an old legacy system.
It converts the foreign data into your own model and language, so changes or strange naming in the other system do not leak into your domain. If the other system changes, only the ACL needs updating.
Q10. What is a domain event?
A domain event records something meaningful that happened in the business, named in the past tense, such as OrderPlaced or PaymentConfirmed.
Inside a service, domain events let one aggregate trigger work in another. Between services, they are published through a message broker so other bounded contexts can react, without the publisher knowing who listens.
Q11. How do bounded contexts share data?
- APIs: one context asks another for data when it needs it.
- Events: a context publishes events, and others keep their own copy of the data they need.
- Translation: the receiving context maps foreign data into its own model, often through an anti-corruption layer.
They never share data by reading each other’s database tables.
Q12. Can the same data exist in several bounded contexts?
Yes, and that is normal. Each context keeps only the parts it needs, in its own shape. The Shipping context may store a customer’s delivery address, while the Loyalty context stores the same customer’s points.
One context is usually the owner of each piece of data and publishes changes; the others keep read-only copies that are updated through events.
Q13. What is the difference between a domain service and an application service?
- A domain service contains business logic that does not belong to a single entity, such as calculating a shipping fee from several rules. It uses the ubiquitous language and has no technical concerns.
- An application service coordinates a use case: it loads aggregates, calls their methods, saves them, handles transactions and publishes events. It contains no business rules itself.
Q14. Where do repositories belong in DDD?
The repository interface belongs in the domain layer, described in domain terms, such as findById(OrderId) and save(Order).
The implementation, for example with Spring Data JPA, belongs in the infrastructure layer. This keeps the domain independent of the database technology and easy to test with an in-memory implementation.
Q15. Can I use JPA entities as DDD entities?
Yes, many teams do, and it is fine for most applications. Keep a few points in mind:
- JPA needs a no-argument constructor; make it
protectedso domain code cannot use it. - Avoid public setters; expose business methods instead.
- Use embeddable types (
@Embeddable) for value objects such asMoney.
If database concerns start to distort the model, keep separate persistence classes and map between them in the repository implementation.
Q16. What is Event Storming?
Event Storming is a collaborative workshop for exploring a business domain. Developers and domain experts place domain events on sticky notes along a timeline, then add the commands, actors and rules around them.
Clusters of related events, and places where the language changes, reveal good candidates for bounded contexts and therefore for microservice boundaries.
Q17. What is a shared kernel, and should I use one?
A shared kernel is a small piece of model or code that two bounded contexts agree to share, such as a Money type. Any change to it needs agreement from both teams.
Use it sparingly and keep it tiny. A large shared library becomes hidden coupling between services, which is exactly what bounded contexts try to avoid. Copying a small class is often better than sharing it.
Q18. How does DDD relate to the Saga pattern?
DDD says that one transaction should change only one aggregate, and that bounded contexts own their own data. So a business process that spans several contexts, such as order, payment and shipping, cannot use one database transaction.
The Saga pattern fills this gap: each context performs its local step and publishes a domain event, which triggers the next step, with compensating actions if something fails.
Q19. Can I apply DDD to an existing monolith?
Yes, and it is a great first step before any migration:
- Run Event Storming to find the bounded contexts hidden in the monolith.
- Reorganize the code into modules per context, with clear interfaces between them.
- Stop modules from reading each other’s tables directly.
- Extract a context into a microservice only when there is a clear reason, using the Strangler Fig pattern.
Tools such as Spring Modulith help verify module boundaries inside a Spring Boot monolith.
Q20. Is DDD only for large companies and complex systems?
The full set of tactical patterns pays off mainly in complex domains with many business rules. For simple CRUD applications it can add unnecessary ceremony.
However, the core ideas, a shared language with the business and clear boundaries between areas, help projects of any size. Start with those, and add rich tactical modeling only where the business logic is genuinely complex.