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 Kotlin 1.3 with heavy use of Spring framework.
# build
mvn clean install
# run
mvn spring-boot:run
# open in browser http://localhost:8080
Several Business Capabilities have been identified:
- Sales
- put a product for sale
- categorize a product
- update a product
- change a product price
- validate an order
- place an order
-
Warehouse
- stack goods
- fetch goods for shipping
-
Billing
- collect a payment
-
Shipping
- dispatch a delivery
Later, we can think about more supporting domains (not implemented in this project):
-
Marketing
- discount a product
- promote a product
-
User Reviews
- add a product review
-
Customer Care
- resolve a complain
- answer a question
- provide help
- loyalty program
The e-commerce system is a web application using a Portal component implementing the Backends For Frontends (BFF) pattern.
The communication among domains is implemented via events:
When the customer places an order the following process starts up (the happy path):
- Shipping prepares a new delivery.
- Sales creates a new order and publishes the
OrderPlaced
event. - Shipping accepts the delivery.
- Billing collects payment for the order and publishes the
PaymentCollected
event. - Warehouse fetches goods from the stock and publishes the
GoodsFetched
event. - Shipping dispatches the delivery and publishes the
DeliveryDispatched
event. - Warehouse updates the stock.
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.
- Low coupling, high cohesion
- Implementation hiding
- Rich domain model
- Separation of concerns
- The Dependency Rule
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.
Service is the technical authority for a specific business capability.
- There is a one-to-one mapping between a Bounded Context and a Subdomain (ideal case).
- A Bounded Context defines the boundaries of the biggest services possible.
- A Bounded Context can be decomposed into multiple service boundaries.
- For example, Sales domain contains Catalog, Cart and Order services.
- A service boundaries are based on service responsibilities and behavior.
- A service is defined by its logical boundaries, not a physical deployment unit.
Application is a deployment unit. A monolithic Application can have more Services.
- Bootstrap (application container etc.).
- Cross-cutting concerns (security, transactions, messaging, logging, etc.).
Configuration assemblies the Service as a single component.
- Has dependencies to all inner layers.
- Can be implemented by Spring's context
@Configuration
or simply by object composition and Dependency Injection. - Implements the Dependency Inversion Principle.
Gateways create the published API of the Service.
- Driving Adapters in the Hexagonal Architecture.
- REST, SOAP, or web Controllers,
- Event Listeners,
- CLI.
Use-Cases are entry points to the service capabilities and together with Entities form the Domain API.
- Ports in the Hexagonal Architecture.
- No implementation details.
- None or minimal dependencies.
Domain Implementation fulfills the Business Capabilities with particular technologies.
- Driven Adapters in the Hexagonal Architecture.
- Tools and libraries,
- persistence,
- external interfaces access.
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:
- Application is implemented via Spring Boot Application.
PaymentJdbcConfig
configures the JDBC implementations for the Domain.- Gateway is implemented as a REST Controller.
- Use-Case interface
FindPayments
is implemented withPaymentsJdbc
in Use-Cases Implementation. - Entity
Payment
is implemented withPaymentJdbc
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 Java version in action with additional concepts such as Microfrontends: