/ddd-example-ecommerce

Domain-driven design example in Java with Spring framework

Primary LanguageJavaMIT LicenseMIT

DDD Example Project in Java: eCommerce

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

Table of Contents

Domains

Several Business Capabilities have been identified:

Core Domain

  • Sales
    • put a product for sale
    • categorize a product
    • update a product
    • change a product price
    • validate an order
    • place an order

Supporting Subdomains

  • 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 idea of Microfrontends is implemented in an alternative branch.

Event Workflow

The communication among domains is implemented via events:

Event Workflow

When the customer places an order the following process starts up (the happy path):

  1. Shipping prepares a new delivery.
  2. Sales creates a new order and publishes the OrderPlaced event.
  3. Shipping accepts the delivery.
  4. Billing collects payment for the order and publishes the PaymentCollected event.
  5. Warehouse fetches goods from the stock and publishes the GoodsFetched event.
  6. Shipping dispatches the delivery and publishes the DeliveryDispatched event.
  7. 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 Dependencies

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.

Event and Listener

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.

Architectural Overview

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.

Screaming Architecture

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

Packaging

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.

Assembling

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.

Anatomy of a Service

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.).

Application and Services

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.).

Service Anatomy

Example of a Service Anatomy

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 with PaymentsJdbc in Use-Cases Implementation.
  • Entity Payment is implemented with PaymentJdbc in Entities Implementation.

Service Anatomy

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..

Conclusion

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.

Where to Next

Check out the alternative branches and repos to see additional concepts and technologies in action:

  • Modulith: A separate Maven module per service.
  • Microfrontends: Service Web Components as part of the service codebase.
  • Microservices: Deployments with Docker and Kubernetes.
  • Kotlin: The same project again, this time in Kotlin.