Domain-driven design (DDD)—one of the most effective architectural approaches for both agile environments in general and microservices in particular—can help you build systems that can stand up to change.

In this post, we will explore an overview of this essential architectural process, demonstrating how to use DDD to develop a microservice or other domain-focused system.

We will cover the basics of DDD (and how it fits with agile), microservices, and bounded contexts and entities. Plus, we will compare reactive and declarative systems and details how to approach an event storming session.

Overview:

Domain-Driven Design (DDD) is a software design approach that focuses on modeling software to match a specific domain based on input from domain experts. Let’s delve into the key aspects of DDD and address your questions:

  1. What is DDD?
  1. DDD emphasizes creating a shared understanding of the domain between technical and domain experts.
  2. It aims to align software code (class names, methods, variables) with the business domain.
  3. DDD helps manage complexity in complex domains by dividing them into smaller, cohesive units called Bounded Contexts.
  4. When to Use DDD?
  1. DDD is suitable for complex domains where the model provides clear benefits.
  2. Use it when you need to bridge the gap between business reality and code.
  3. Why Are Microservices Ideal for DDD?
  1. Microservices align well with Bounded Contexts, allowing each microservice to have a unified model.
  2. Microservices promote scalability, modularization, and independent development.
  3. What Are Bounded Contexts?
  1. Bounded Contexts define linguistic and organizational boundaries for specific business domain areas.
  2. Each Bounded Context correlates to a microservice and has its own unified model.
  3. Bounded Contexts help manage complexity and ensure clear communication.
  4. What Are Entities?
  1. Entities are objects with a distinct identity and a lifecycle.
  2. They represent important concepts in the domain and can change over time while preserving their identity.
  3. Entities are the first place to put domain logic.
  4. Reactive vs. Declarative Systems:
  1. Reactive systems respond to events and handle asynchronous communication.
  2. Declarative systems focus on specifying what should happen without detailing how.
  3. In DDD, communication between entities can follow a choreographed/reactive model, where events trigger reactions.
  4. Event Storming:
  1. Event Storming is a collaborative workshop technique to explore and model complex domains.
  2. It involves visualizing domain events and their interactions.
  3. Example events: “Order Submitted,” “Payment Received,” and “Nightly Reconciliation Completed.”
  4. Event Storming Example (E-Commerce):
  5. Imagine an e-commerce platform:
    • Events: “Product Added to Cart,” “Order Placed,” “Payment Processed.”
    • Entities: Customer, Product, Order, Payment.
    • Bounded Contexts: Cart Management, Order Processing, Payment Gateway.


 Better apps with domain-driven design | LinkedIn Learning

Bounded Contexts and Entities

What are contexts?

Now let’s get into the nitty-gritty details of what a domain-driven design actually looks like, how is it structured. When you’re thinking in DDD terms, you’re thinking about organizing the code along certain well-defined boundaries. And the first of those is the thing called a Bounded Context.

The idea of a Bounded Context is that it is a natural division within the business. So for example, if you were doing a bookstore, the store itself is a context. The purpose of that context is to sell books to people, but there’s another context in order to make a bookstore work and that’s the warehouse from which the books are shipped. So we have two distinct contexts here.

One of them is the store and the responsibilities of things within the store context is primarily sales.

And the other one is the warehouse, and the responsibilities within the warehouse context are shipping.

And that’s an important way to keep contexts separate from each other, is think in terms of what are the responsibilities of the people working within that context? If somebody went to work in the morning at the warehouse they would not be thinking that my job is to sell books, and the same happens over on the store side and somebody who works in the store is not going to be thinking I need to know how to ship books. So within each of the contexts you have distinct people working, and they are working within their context without thinking too much about other contexts.

Now of course in order to do that, they have to communicate with each other. And the communication is for the most part entirely within a given context, which is one of the reasons why domain-driven design makes for a good design principle. ‘Cause things that happen inside the context stay inside the context.

How does these systems communicate with each other?

Things go wrong when you end up having objects that are on both sides talking to each other without any regard to the context boundaries. So that’s something that clearly you want to avoid.

So, the way that DDD solves that problem is by allocating responsibility for communication between context to one small object.

In other words, in this picture on the store side we have one person, if you will, whose job is to talk to one person on the warehouse side in order to communicate between these two contexts. And this is the way it usually works in the real world. If you need something to be done in the accounting department you don’t walk over there and just grab a random accountant by the collar and say do this for me, there’s a process that you have to go through that involves going through proper channels in order to communicate. And all that we’re doing in a DDD world is modeling that notion within the code itself.

So, let’s look at a little bit of terminology here though. An individual person, if you will, within a context is known as an entity.

•             So an entity is a lot like a object or a class in the object-oriented world.

•             It has one job and it does only one thing,

•             Does that in a specific Context

What is Aggregate?

So an entity is of course interesting, but of more interest is the notion of an aggregate. So the idea of an aggregate is an aggregate is a collection of entities that you talk to through a single portal. So this large guy here is the portal for the aggregate.

Now, the line between an entity and an aggregate can be kind of fuzzy because from the outside world the portal into the aggregate looks like an entity, it looks like one object that does one job.

However, when you start focusing in on the code itself, then you start seeing the entities that that portal entity uses to actually get the work done. So whether something is an entity or an aggregate may not be obvious from the outside. On the other hand when you drill down, if it’s an aggregate there are things inside the aggregate that are used for the entity that controls access to actually do the work that’s requested of the aggregate as a whole.

Contexts are often implemented as single aggregates, but they might not be. A context might actually be implemented by multiple aggregates that are working together in order to get their work done.

What is Ubiquitous Language?

Now, the next concept that’s interesting is the notion of the ubiquitous language.

The idea of the ubiquitous language is that within a given context, the people who are working in that context use a language of their own and the language of one context is different from the language in other contexts.

So, for example, in the store context, if you’re talking about a book, what you care about are things that are relevant to the responsibility of the people who are working in that context, in other words, they are relevant to sales.

So if you’re thinking about a book, the language of books is going to talk about things like authors and reviews and readability and length and stuff that a reader would be interested in when you’re trying to sell a book to them.

Over on the warehouse side, the language, the definition of a book is different because you don’t really want to sell it anymore, you want to get it shipped, so over on the warehouse side, the language of books involves weight and size, and shelf, and things that are of interest to people that are taking books and putting them into boxes and sending them off to people.

So that occurs not just at the noun level, but also at the verb level, in other words, the things that you do to books is different, depending on which context you’re in and those things will be captured in the language, so on the sales side, you sell books obviously, or you shelf books, or you organize books.

Over on the warehouse side, you pick books off of shelves and you box the books and so on and so forth.

The languages are different.

So formalizing those ubiquitous languages is an important part of DDD, the basic idea here is that the language itself will be reflected in the code.

The words that are used to describe a book will be different within the warehouse context than they are within the store context.

So as a consequence, when you implement, the names of things will be different, is that warehouse kinds of names will be used on the warehouse side and book kinds of names will be used on the book side.

So this is a ubiquitous language that reflects at every level.

It’s a language that you, the programmer and the architect, will be using as you talk about the problems that you’re solving. It is a language that is reflected in the code itself, in that it will provide names for things that appear in the code. And it’s a language that’s going to be used up at the domain level itself in order to describe the work that’s going on, so it’s ubiquitous, but it’s ubiquitous within a specific context.

What are the ways to tests to see whether something belongs in a context or not?

One of the great tests to see whether something belongs in a context or not is to look at the way that you describe that something and see which language is most appropriate for that something.

Another critical thing to bear in mind as we’re talking about entities, is that what we’re talking about are the roles that people take on as they’re working, not the actor, not the person who is in that role.

Within the context of say, a timesheet system, a manager authorizes timesheets, that’s the role.

An employee fills out timesheets, that’s their role.

Nowhere do you need to be both a manager and an employee, in other words, the fact that a manager is an employee in the real world is irrelevant with respect to our domain-driven design. What we care about are the roles, the fact that an individual person sometimes logs on to the system in the role of manager in order to authorize timesheets, and that at other times, the same person logs on to the system in the role of employee to fill out a timesheet, is irrelevant from the point of view of the design, as far as we are concerned they are separate people.

So one of the things that we’re doing with the ubiquitous language is trying to identify the roles that people take on as the stories play out..

And the entities then, will be named after those roles, not after the job title of the person that happens to be filling the role in a specific situation, or to put it another way, you never model the guy typing, the guy typing is not an entity. The entities are the roles that that person takes on as they are actually doing work.

Understanding the same name, different Entity?

Now let’s take that picture we were just looking at and think of it in terms of the implementation or the data model.

Traditional way of building system:

If you were thinking about data, in particular if you’re thinking about a traditional way of building a system, where you think about the data model first, you’re probably thinking in terms of normalization and relational databases. And in that kind of world, when you look at modeling the way the entities look, you’d end up with a picture that looks rather like this one. (see below)

You’d have context that contain things that are obviously within that context, but outside of the context, you’d have entities that were independent, that somehow had to work in multiple contexts at the same time.

Domain driven design world:

In a domain driven design world, that’s considered a bad thing is that every entity should be associated with some context or another.

Now getting back to the ubiquitous language issue, you’ll remember that things like orders, and customers, and so forth will be treated differently depending on the context that you’re in.

The language surrounding orders will be different on the store side, than it would be on the warehouse side.

So as we move towards a domain driven design structure, we’re going to end up with something that looks more like this. (see below)

Notice that the store context and the warehouse context each have their own order and those orders are going to be different.

Same with products, the store context and the warehouse have different products.

Over on the store side, a product entity is going to be concerned with things like images and prices and of course the SKU number.

Over on the warehouse side, well there’s also going to be a SKU here.

There has to be something to tie them together, but the warehouse side is concerned about the shelf and the size and the weight of the product.

And on the warehouse side, you could care less about the image and the price.

So as a consequence, when we get into a domain driven design approach, we’re going to move away from the relational database thinking of trying to make giant components, if you will, that could be used everywhere.

It is considered a bad thing in the domain driven design world to have a single product-object for example, a single product component, that could be used in multiple context.

Because that product component is going to be too big. It’s going to be hard to maintain. It’s going to be containing lots of internal dependencies and so forth that you just don’t want to have.

So, in the DDD world there will be two different product entities.

One that makes sense in the store context,

another one which makes sense in the warehouse context and the fact that they’re different is good actually.

You do not try and put them together.

Now this leads to something that’s important in microservices that people often don’t understand, which is that if we are implementing a DDD design in microservices, we’ll have two different product services.

One product service will be warehouse focused, the other product will be store focused.

Those two services will have completely different databases.

You will not have a single table that contains everything that a product has to have in it.

So everything stays context sensitive, context specific, withinside a domain driven design.

Orchestration (Declarative systems)

Now let’s drill down even further and look at how the entities will communicate with each other as they’re getting work done.

There are two ways to do communication.

One easy and the other one better.

So let’s start with easy because that’s the way that is natural to most programmers. The easy way to do things is orchestrated or declarative systems.

The basic idea of an orchestrated system is that one entity tells another entity what to do.

So looking at our store example, you could have services representing the individual entities, a shopping cart service, a billing service, a warehouse service, an email service for example that might send emails off to your customers. Those services in a declarative world will tell each other what to do.

The shopping cart tells the billing service to issue an invoice. It tells the warehousing service to queue an item up for shipping.

So in the normal course of events, what you would expect is a natural flow of communication between objects.

Note:

Now, declarative systems can work okay inside a monolith. They cannot work very well inside of a microservice system and the reason for that is those blue lines, those communication lines, are network connections in the microservice world.

In a monolith, of course, they’re just function calls. Network connections can have all sorts of things going wrong with them.

Problem statement:

So for example, what happens if the network connection between the shopping cart service and the warehouse service breaks?

Well, the shopping cart service could issue that first request, but then it can’t get through to the warehouse service. What it does at that point is, well it’s up to you as a programmer, but it’s complicated. Whatever you do is going to be complicated.

When you learn about microservices, you spend a lot of time learning about various design patterns that you can use to solve this particular problem, but the problem is a hard problem. So one of the classic difficulties here is that big question mark over the billing service.

In other words, the shopping cart service said to the billing service, issue an invoice, and it thinks it’s issued the invoice. It probably has issued the invoice in fact. The shopping cart service then goes to the warehouse service and can’t get through, so we’re now in a situation where the invoice has been issued, but we are not shipping the product and that’s obviously not acceptable.

So recovering from that can be very difficult.

The other problem associated with these kinds of declarative systems is that there’s a very tight coupling relationship between the various services here.

Put another way, the shopping cart service needs to know quite a bit about the warehouse service in order to use it. It has to know that, for example, it can handle a queue item for shipping request. It needs to know what arguments to pass. It needs to know what the types of those are, perhaps.

The point being, that if you make a change to the warehouse service, you also have to change the shopping cart service.

And the same applies to every one of these downstream services. If you make a change to any downstream service, odds are the upstream service will be impacted, so we have an inherent maintenance problem here that we don’t want to have.

So, the solution to that is our next topic.

Choreography (Reactive Systems)

The other way to organize your system of services is by using choreography or reactive systems. In a sense choreography is a solution to the problems that we were just talking about when we were looking at the declarative systems.

Now, this is where we start, with our synchronous services that knew about downstreamed things.

What happens if we replace those declarative APIs with generic ones? In other words, now instead of the shopping cart service telling the billing, and warehouse, and email services what to do, the shopping cart service just announces to the world a new order has been placed if you’re interested in that do something about it.

The billing service will be sitting around waiting for that message to be generated.

And it says, oh a new order has been placed I better send the bill.

And the warehouse service says, oh a new order has been placed I better ship it out.

And the email service says, oh a new order has been placed I better notify my customer.

So at this point we have solved many of the problems associated with our declarative system.

In particular, we have eliminated the hard, tight coupling relationships between the downstream services and the upstream service.

In other words, I can make as many changes as I want to the billing service, and the shopping cart service doesn’t care. In fact, the shopping cart service doesn’t even know that the billing service exists. All that it knows is I send out an order place event and anybody who’s interested deals with it. It doesn’t know who’s interested.

So what that means for maintenance point of view is that we’re in a much better position.

We can change the way that the downstream services work without impacting the upstream service.

And at the same time we can add new downstream services and the upstream service doesn’t care about it.

So this is a much better place to be.

In general, in fact, I would argue that in any domain-driven design the vast majority of communication that goes on between your entities should be done using a choreographed or reactive model.

How is it implemented?

Now in terms of implementation, it’s called publish-subscribe model.

The basic idea is the shopping cart is publishing an event, and the downstream services subscribe to that event.

So the subscribers then are unknown to the publishers, in the pub-sub model.

Put it another way, the shopping cart is just sending events out to the services that are interested, and it doesn’t know who’s receiving.

This implementation is also called a messaging system. The most useful messaging systems, or at the least the ones that are popular right now, are things like Kafka and ZeroMQ, and RabbitMQ, but there are a host of messaging systems that you can use to implement the underlying technology that makes this kind of published subscribe communication easy to do.

Note:

If you’re going to be implementing these kinds of DDD systems, go with a reactive model.

Next Steps:

Conclusion

Domain-Driven Design provides a structured approach to modeling complex domains. By defining Bounded Contexts, identifying entities, and using event-driven communication, DDD helps bridge the gap between business requirements and software implementation. When combined with microservices, DDD enables scalable, maintainable, and efficient systems. 

 

References:

https://www.linkedin.com/learning/software-architecture-domain-driven-design

Leave a Reply

Discover more from Rajeev Singh | Coder, Blogger, YouTuber

Subscribe now to keep reading and get access to the full archive.

Continue reading