Implementing cart microservice using Domain Driven Design, and Port and Adapter pattern - Part 2
In part 1, we discussed the importance of domain-driven design and various aspects to consider while designing an application. We also touched upon the ports and adapters design pattern and talked about how the combination of DDD and Ports and Adapters pattern helps us to design modular applications.
In part 2, we will focus on the various layers of ports and adapters. We will use the same use case from Part 1 — Adding an item to a Shopping Cart — as the basis for discussing the various components that are involved in building the use case.
Our use case is to build a Cart service application that exposes a few APIs (REST based). Web/mobile apps will consume those APIs to execute certain defined operations like adding/updating/removing items.
To achieve the use case, we need to build a REST service that accepts input data, executes business logic, interacts with dependent services, persists data, and finally sends a response to the consumers’ request. If we plugin this use case into ports and adapter pattern, cart service will look as below. The application core remains the heart of the system, surrounded by ports. Two distinct types of ports are present — primary port (driving port) and secondary ports (driven ports).
Domain
Before we dive into all the layers of this pattern, let’s discuss various components involved in the domain layer and its purpose. It’s useful to look at the ports and adapters pattern from the domain outwards.
The domain is the core of this microservice. In part 1, we said the domain model should contain both data and the behavior. Most importantly, it shouldn’t be an anemic model. In cart application, all operations revolve around the concept of a cart.
For this example, let’s assume that a Cart entity must have the below.
Cart has,
- Customer details
- Items
- Price
The relationship between various models can be expressed as below.
Defining an aggregate root
Martin Fowler defines a DDD aggregate as a cluster of domain objects that can be treated as a single unit. In our application, Cart entity acts as an aggregate. It encapsulates the nested child entities as shown in the diagram above. Let’s also assume that each CartItem entity will have the calculated price & Catalog data. The most crucial aspect of aggregate modeling is to ensure the domain remains consistent at all times, and the business rules and invariants are always enforced. A few things to note about the aggregates (in our case, the Cart entity).
- First and foremost, an Aggregate is an entity, which means it has a unique id
- It acts as an entry point into the domain and all entities
- It exposes a set of public methods for other modules within the application (application/domain services) to interact with.
- It is retrieved and persisted as a single unit, and it always remains in a consistent state.
- It takes a responsibility to enforce the business invariants when an operation is executed.
- It should be modified and persisted in a single transaction.
- If one aggregate ,Cart- depends on another, say Customer, then aggregate Cart can only have a reference to Customer. Customer entity is not composed inside the Cart entity.
Pseudo code of Cart Aggregate domain model (Represents both data and behavior)
Few callouts from this aggregate modeling:
- As much as possible avoid using public setters, but populate values either via the constructor or by invoking a meaningful method in aggregate root. DDD is all about Ubiquitous language. Enrich the models with meaningful intention-revealing methods.
- While all the items in cart can be accessed via getCartItems() but this method returns unmodifiable list. So that caller can read the data but will not be able modify.
- It is the responsibility of the aggregate root to ensure it remains consistent and not violating any business rules. Hence, addItemToCart()validates its invariants by calling validateMaxItemsLimit() and isQtyWithinAllowedLimit().
For further reads I recommend you read the following white papers written by Vaughn’s have written and can be found here.
Entity
Entities are identified using a unique id. The id can be a sequence number or UUID or GUID, and it is associated with it when an object is created. Entities are mutable. Values can be updated based on the business operation executed. In our use case, Cart and CartItem are entities. Each cart is uniquely identified.
An entity in one microservice may become a value object in another. For example, CartItem is associated with a catalog item (SKU). In Catalog bounded context, SKU is an aggregate, whereas in cart bounded context, it is not.
Value Objects
Often time, we tend to create entities as they represent a row in the database. Columns in a row are mapped as primitive data types like int/double/string and nested objects. They have a unique ID, and no two entities are the same if their ID is different, even if they hold the exact same values for the rest of the attributes. Value objects on the opposite, they don’t have a unique id, but they hold values. Two objects with the same values are equal even if they are associated with different entities. Consider CatalogData(Product) from our use case — This represents the item the customers are adding to cart. Different customers could add the same product, but the product name and other metadata information don’t change.
- Unlike entities, value objects are immutable.
- Consider representing even primitive data types like name, currency, email as a Value Objects, so that you can compose the data, and do any meaningful validations and business rules. That is, Value Objects are non-anemic, too.
For example,
- In CartPrice Value object, we can validate if an item price returned by PricingService is non-negative value.
- Likewise, when Catalogservice returns Catalog data, CatalogItem Value object can check if the product’s maximum allowed qty (maxQty) is not 0.
- Email can have methods to validate if the email is in a valid format
Avoid doing these operations in utility classes outside of the Value Object. Enrich Value objects with meaningful methods (i.e. Ubiquituous language) that have a meaning in the domain
So far, we have discussed aggregate, entity, and Value objects. Next, we will talk about how to create, persist, and retrieve.
Repository
In most real-world applications, when the customers interact with the application, the state of some objects is persisted to a database, be it RDBMS or NOSQL. For example, when a customer adds an item to the cart, the state of the Cart, along with the items, is stored. Later, when the customers come back, the state of the Cart is retrieved from the database, so that the customers are able to proceed where they left off. In addition, state of some of the entities that are composed inside the Cart may have changed — the price of an item may have changed; the item may have become unavailable, etc. Some of these data may require calls to other microservices to refresh the most recent information. The Core Domain, as described in the section above, needn’t bother about the details of these infrastructure or I/O components, but it does dictate the core domain model that it requires to process the business logic. This isolates the domain from any changes to the underlying infrastructure — migrating to another database, for example.
Formally, a Repository is an abstraction that the Domain defines, usually in the form of an interface, with intention-revealing methods and the domain models that those methods are expected to return. Thus, Repositories are domain aware as they accept/return domain entities/aggregates, and its operations also depend on the features that the service (Core Domain) provides. The implementor of the repository is expected to store/retrieve/delete aggregates or entities along with its child/nested entities. Usually an ORM (Object Relational Mapping framework) converts each row/column of database rows into a mapped entity when data is retrieved
This is an area the Ports and Adapters pattern shine. Repository can be modeled as a Port (Secondary/Driven port) and the implementation can be defined as an adapter. In our use case, we need to retrieve Cart using cart_id, execute the required action then save it back. Domain defines the port with an expected contract that any adapter should adhere to when they implement this port. In terms of layering, Ports belong in the Domain layer, whereas the adapters belong in the Secondary Adapters layer.
Port ( Interface in java)
Adapter (Infrastructure component which includes underlying datastore implementation as well)
Always keep aggregates and entities as small as possible, but still follow the rules of high-cohesion and low-coupling. The repository stores/retrieves aggregates as whole and bigger aggregates could cause performance issues like reading/writing into multiple tables
Factory
Factory is another object that lives in the Domain. Oftentimes, services create aggregates and entities by calling the respective repositories. If an entity is purely a representation of the data stored locally within the microservice, this is sufficient and there is no need for a Factory. However, there are times when an aggregate is not just a representation of the data stored locally, but a composite of data from external microservices (possibly from another bounded context). When such a need arises, the Factory pattern comes handy. The logic of creating the complex aggregate can be delegated to a Factory, which internally constructs objects from various sources. Caller (service layer) doesn’t need to concern about how the object is created. Factory is a well-known Gang of Four design pattern. Factory pattern, along with the Builder pattern is quite powerful in abstracting the internal details of how an entity is constructed.
For instance, CartItem requires CatalogData (item information) of the item being added to cart. This includes the below operations:
- Retrieve cart from the repository
- For all the existing CartItems only the product ID is stored in cart repository, and lot of meta data information about the product, owned by the Catalog microservice, should be fetched, especially the availability/ max quantity limit, product information.
- CartItem entities should be updated with the latest CatalogData so that the domain can ensure it has the latest information to execute its business rules — for example, validating the maximum limit of items that can be added to the Cart
Hence, we will use CartFactory to create a Cart aggregate. Please note that the CatalogService is injected into the Factory. This component is responsible for calling the Catalog microservice and extracting the Catalog data, which is then composed into the Cart aggregate.
So far, We have discussed the Domain layer and some important objects that live in that layer. Now we will connect the domain with service layers, which gives a complete shape to our application.
Primary Port/Adapter
As explained in part 1, the primary port layer is responsible for bridging the external world to the application. Think of this as a bounded context, although a technical one, that contains inbound infrastructure concerns. In our use case, the Cart microservice exposes a few REST APIs for the consumers. The adapters in this layer are responsible for handling the inbound calls. For our use case, the CartController is an adapter that receives the HTTP request from consumers like Browser or mobile apps. This layer is devoid of any business logic and the adapter converts the external world request into a Command object and delegates the handling of the use case to the next layer — Application service (more on this later)..
CartController, an adapter in this layer, is also responsible for converting the data into a format/contract that’s agreed with the consumers — in our case, the response from the REST API. It is up to the consumer how to use the data. One use case is that UI could show cart with the item name, quantity, total price, and individual item price. If you do something like Consumer Driven Contracts or GraphQL, those concerns reside in this layer.
Please take care to not share the objects that marshall the REST API requests, usually some for of Data Transfer Objects (DTOs), between this layer and the application/domain layers. This will help avoid tight coupling between these layers and enables the much-needed modularity between these layers. You can avoid complexities such as one layer introducing additional attributes in the DTO just as a means of passing data around and in doing so causing cascading changes whenever the structure of the DTO is changed.
Few design aspects we liked to follow are,
- Request and response objects (DTOs) of primary adapters live and die in the primary port. This helps to easily change consumer response contract without any core layer changes. Often time you may be publishing different version of API in prod and give flexibility to consumer to switch.
- 1. The DTOs are different from Command objects. Command objects are intended to invoke a use case exposed by the Application service (the layer below).
- The Application Service layer defines the interface and the data contract (DTO) of the primary ports. The primary adapters implement this port. One way to reason about this heuristic is that the Application service determines the functionalities exposed by the application and the adapters just comply to the said contract
- Convert external request to a Command Object whose contract is dictated by the Application service as well
Sample code snippet,
Application service
Like I mentioned above, every single application is built with a purpose to solve a set of business problem/use cases. Each one of the use cases is unique and executed in a specific way. There is a need for a component within the Cart application which should perform the workflow and ensure the workflow is indeed executed in correct sequence. A component called Application Service is responsible for executing all the steps to complete this use case. The primary adapter — REST Controller in our case — calls the Application Service by passing the right Command object
For example, in the Cart microservice, this component could be responsible for the below,
- Consumer authorization and authentication: Assuming we use some form of user token (OAuth based, for instance) created at the time the user logged in, this token must be validated before processing the request. Application service is the place you handle that. The CartApplication service could delegate handling this functionality to another application service, AuthorizationService.
- Transaction management: Create/commit/rollback the operations
- Create required domain entity/aggregateroot by calling a Repository or Factory components.
- Invoke appropriate action on the aggregate, entity or domain service (more on this later).
- Returns the response back to the caller (in our case, primary port/adapter layer), which converts into JSON/XML and serves it back to the consumer.
Please be careful not to build any direct business logic in the Application Service; Business logic belongs in the domain. Although, an application service is aware of the right aggregate or entity to create, that too, by delegating the responsibility to a Factory of a Repository component, and then call the right operations on those objects.
Domain Service
A Domain service is a component that is part of Strategic DDD but nicely fits into the Domain layer of Ports and Adapters pattern. It would be good to contrast this with the Application Service and other services (components) that carry out infrastructure operations.
The primary difference from the Application Service is that the Domain Service is a domain object that performs a meaningful and domain-rich feature which otherwise doesn’t sit within a domain entity or a value object
Let’s run through our use case. When an item is added to cart, the Cart service calls the Pricing microservice to compute the price of the cart, along with the items in the cart. Refer the code snippet from Cart entity. addItemToCart() delegates the function of computing the price of the cart to the method — calculateCartPrice() method. Please note that the reason for this is the need to enrich the Cart entity with functionality that rightfully belongs with it.
public void addItemToCart(CartItem cartItem) {
if (validateMaxItemsLimit(1) && isQtyWithinAllowedLimit(cartItem)) {
this.cartItems.add(cartItem);
calculateCartPrice();
updateLastModifiedTime();
} else {
// Throw Exception for violating limit.
}
}
Now, Cart entity could call the Pricing microservice directly, but it would couple Cart entity with infrastructure dependencies, not to mention bloating the entity. But, pricing itself is a meaningful function that resides in the Domain layer. In such cases, we find delegating the responsibility to another component, a Domain service, is quite useful.
There are two options,
Option 1: Application service, instead of creating the aggregate and calling the addItemToCart on the entity, it could call a Domain Service (as below). The Domain service will be responsible for orchestrating the business logic.
- Call Cart entity to add item to cart
- Call PriceDomain service to price the cart. Please also note that the PriceDomainService delegates the calls to the Pricing microservice to another component — PriceCalculatorGateway. — further abstracting infrastructure dependencies
Few things to note:
- Domain service may have to expose methods that proxy the functions of the Cart entity. For example, every cart operations ( add/update/remove) requires cart price to be calculated. Domain service will have to expose equivalent methods for these and the application service calls them.
- Ensure this doesn’t become a default pattern for any and all business logic. Domain service is required only in cases where the functionality doesn’t clearly belong in any of the business entities
Option 2: When an application service creates an aggregate, inject PriceDomainService also into it. This way, application service can continue to call the Cart entity and the Cart entity can delegate the pricing part of the logic to PriceDomain service. This option has two benefits over Option 1.
- A Domain service need not have to provide proxy methods for add/update/remove operations. If there are more functions exposed by the entity (Cart, in our case), we can avoid having to add those proxy methods to another component.
- The function of adding to cart, along with its dependencies, is nicely encapsulated inside the Cart entity in a loosely coupled fashion
In a nutshell, just like application services, domain services are also stateless. They reside within the domain and are part of Ubiquiuous language of the domain. Sophisticated domain services could also inject repository and other domain services, as required to process the business logic. You have to be cautious about creating a domain service. It is not mandatory to have domain service unless you are sure you can’t place the business logic either in aggregate or entities or value objects
Secondary Ports/Adapters
This is a supporting layer that helps the application core to execute the workflow. The application core (Application/Domain) drives this layer. Think of this layer as an infrastructure bounded context, much like the Primary Ports/Adapters layer, but for the upstream dependencies of the application — Database, other microservices, Caches, and Messaging systems. Adapters in this layer adhere to the Port defined in the domain layer, and the dependency inversion principle helps to inject required adapters for application service/domain services to use. Also note, the application core is unaware of the adapter as it only interacts via the port (interfaces in java).
- Secondary adapters are technology aware. They are used to get/send data to another http/soap/grpc service. Retrieve/persist data in datastore. They could publish events to another bounded context/service.
- This layer also acts as an ACL (anti-corruption layer) if your application depends on another service input to execute the logic). Catalog service is an example. The cart needs Catalog data to perform its core operation while this infra layer converts external catalog service contract into the Catalog value object that the domain defines. Please note that Catalog means different things between these bounded contexts.
The below diagram is a summary of what we’ve seen so far — High-level components involved in the use case of adding an item to cart and the layer where each of them belong.
In Java application, packages represent these layers in a single Maven/Gradle module, or each of them can be an independent module of its own with dependency mapped. A high-level package structure can be as follows.
Few callouts,
- Where do command objects reside
- I have kept command in the application service layer. Application service extracts information and creates domain entities and invokes domain operation.
- Some of the command can be complicated with nested objects. In such case, application service will need to create lot of entities before invoking domain operations.
- Since the application core includes both application service and domain elements, commands can also be present in domain layer. It may simplify the work of the application service, and aggregate can encapsulate creation of the entities.
2. Why Factory in domain
- CartFactory’s whole responsibility is to create an aggregate root by fetching information from different ports (repository/external services). Hence it made more sense to keep it in the domain layer than the application layer
3. What is the difference between price service and price gateway. Like repository port, external HTTP ports are named Gateway. While Price service includes the responsibility of calling PriceGateway, but also executes business logic.
Note: Code snippets shared in this blog are mere examples to give context and not a fully executable one.
We’ve kept the scope of the these blogs — Part 1 and Part 2 — to the core concepts of Ports and Adapters and DDD. Obviously, there’s a lot more ground to cover.
1. How to deal with cases where there are multiple aggregate roots inside the same application?
2. Events that communicate critical Domain state transitions to other bounded contexts or microservices
3. Testing strategies
We intend to write on these topics in the future. In the meantime, please let us know your feedback
Further Reading
- Eric Evans’ Domain Driven Design
- Vaughn Vernon’s Implementing Domain Driven Design
- Practical Domain-Driven Design in Enterprise Java
- Martin Fowler articles
- Hexagonal-Architecture
- Designing Aggregates Part1, Part2, Part3 by Vaughn Vernon