Domain-driven design example in Java with Spring framework
The purpose of this project is to provide a sample implementation of an e-commerce product following Domain-Driven Design (DDD) and Service-Oriented Architecture (SOA) principles.
Programming language is Java with heavy use of Spring framework.
# build
./mvnw clean install
# run
./mvnw spring-boot:run
# open in browser http://localhost:8080
Several Business Capabilities have been identified:
Warehouse
Billing
Shipping
Later, we can think about more supporting domains (not implemented in this project):
Marketing
User Reviews
Customer Care
The e-commerce system is a web application using a Portal component implementing the Backends For Frontends (BFF) pattern.
The idea of Microfrontends is implemented in an alternative branch.
The communication among domains is implemented via events:
When the customer places an order the following process starts up (the happy path):
OrderPlaced
event.PaymentCollected
event.GoodsFetched
event.DeliveryDispatched
event.There is only the basic "happy path" workflow implemented with a big room for improvement, for example when Shipping doesn't get bot Events within a time period, the delivery process should be cancelled etc..
Services cooperate together to work out the Business Capabilities: sale and deliver goods.
The actual dependencies come only from Listeners which fulfill the role of the Anti-Corruption Layer and depend only on Domain Events.
Events contain no Domain Objects.
For communication across Services an Event Publisher abstraction is used, located in the package ..ecommerce.common.events
. The interface is an Output Port (in the Hexagonal Architecture) and as a cross-cutting concern is its implementation injected by the Application.
While no popular architecture (Onion, Clean, Hexagonal, Trinity) was strictly implemented, the used architectural style follows principles and good practices found over all of them.
The below proposed architecture tries to solve one problem often common for these architectural styles: exposing internals of objects and breaking their encapsulation. The proposed architecture employs full object encapsulation and rejects anti-patterns like Anemic Domain Model or JavaBean. An Object is a solid unit of behavior. A Service is an Object on higher level of architectural abstraction.
The architecture "screams" its intentions just by looking at the code structure:
..ecommerce
billing
payment
sales
category
order
product
shipping
delivery
warehouse
Going deeper the technical concepts are visible too:
..ecommerce
billing
payment
jdbc
listeners
rest
As shown in the previous section, the code is structured by the domain together with packages for technical concerns (jdbc
, rest
, web
, etc.).
Such a packaging style is the first step for a further modularization.
The semantic of a package is following: company.product.domain.service.[entity|impl]
, where entity
and impl
are optional. Full example: com.ttulka.ecommerce.billing.payment.jdbc
.
While a physically monolithic deployment is okay for most cases, a logically monolithic design, where everything is coupled with everything, is evil.
To show that the Monolith architectural pattern is not equal to the Big Ball Of Mud, a modular monolithic architecture was chosen as the start point.
The services can be further cut into separate modules (eg. Maven artifacts) by feature:
com.ttulka.ecommerce:ecommerce-application
com.ttulka.ecommerce.sales:catalog-service
com.ttulka.ecommerce.sales:cart-service
com.ttulka.ecommerce.sales:order-service
com.ttulka.ecommerce.billing:payment-service
com.ttulka.ecommerce.shipping:delivery-service
com.ttulka.ecommerce.warehouse:warehouse-service
Or by component:
com.ttulka.ecommerce.billing:payment-domain
com.ttulka.ecommerce.billing:payment-jdbc
com.ttulka.ecommerce.billing:payment-rest
com.ttulka.ecommerce.billing:payment-events
com.ttulka.ecommerce.billing:payment-listeners
In detail:
com.ttulka.ecommerce.billing:payment-domain
..billing
payment
Payment
PaymentId
CollectPayment
FindPayments
com.ttulka.ecommerce.billing:payment-jdbc
..billing.payment.jdbc
PaymentJdbc
CollectPaymentJdbc
FindPaymentsJdbc
com.ttulka.ecommerce.billing:payment-rest
..billing.payment.rest
PaymentController
com.ttulka.ecommerce.billing:payment-events
..billing.payment
PaymentCollected
com.ttulka.ecommerce.billing:payment-listeners
..billing.payment.listeners
OrderPlacedListener
Which can be brought together with a Spring Boot Starter, containing only Configuration classes and dependencies on other modules:
com.ttulka.ecommerce.billing:payment-spring-boot-starter
..billing.payment
jdbc
PaymentJdbcConfig
listeners
PaymentListenersConfig
META-INF
spring.factories
Note: Events are actually part of the domain, that's why they are in the package ..ecommerce.billing.payment
and not in ..ecommerce.billing.payment.events
. They are in a separate module to break the build cyclic dependencies: a dependent module (Listener) needs to know only Events and not the entire Domain.
See this approach in an alternative branch: modulith.
Service is the technical authority for a specific business capability.
Application is a deployment unit. A monolithic Application can have more Services.
Configuration assemblies the Service as a single component.
@Configuration
or simply by object composition and Dependency Injection.Gateways create the published API of the Service.
Use-Cases are entry points to the service capabilities and together with Entities form the Domain API.
Domain Implementation fulfills the Business Capabilities with particular technologies.
Source code dependencies point always inwards and, except Configuration, are strict: allows coupling only to the one layer below it (for example, Gateways mustn't call Entities directly, etc.).
As a concrete example consider the Business Capability to find payments in Billing service:
PaymentJdbcConfig
configures the JDBC implementations for the Domain.FindPayments
is implemented with PaymentsJdbc
in Use-Cases Implementation.Payment
is implemented with PaymentJdbc
in Entities Implementation.There is no arrow from Configuration to Gateways because PaymentController
is annotated with Spring's @Component
which makes it available for component scanning the application is based on. This is only one possible approach. Another option would be to put the Controller as a Bean into the Configuration, etc..
The goal of this project is to demonstrate basic principles of Domain-Driven Design in a simple but non-trivial example.
For the sake of simplicity a very well-known domain (e-commerce) was chosen. As every domain differs in context of business, several assumption must have been made.
Although all fundamental use-case were implemented, there is still a room for improvement. Cross-cutting concerns like authentication, authorization or monitoring are not implemented.
Check out the alternative branches and repos to see additional concepts and technologies in action: