Domain Driven Design

from the trenches for practitioners

j.Piknik Warszawa 2014

About me

Marcin Kuthan

Marcin Kuthan

Technical leader at Allegro Group

 mkuthan.github.io

The Talk

Layer 1 domain services event sourcing cohesion cohesion evident vague important essential ubiquitous language factories repositories entities value objects aggregates bounded contexts application services layered architecture specifications domain events context map shared kernel core domain CQRS invariant cohesion supple design immutability

Anemic domain model rot over time

Rich domain model is expensive

Anemic and rich domain models interact with each other

Bounded context should be highly cohesive

Lack of cohesion example


package com.acme.ecommerce.model

class Product {
  id
  externalId
  name
  description
  photos
  opinions
  categories
  packages
  prices
  discounts
  availability
  reservations
  (...)
}
        

Model behaviours not state


package com.acme.ecommerce.inventory

class InventoryService {
  addInventoryEntry(productId, numberAvailable, ...) {}
  makeReservation(productId, numberRequested, ...) {}
}
        

package com.acme.ecommerce.ordermanagement

class OrderService {
  addOrderLine(productId, productName, price, quantity, ...) {}
}
        

Aggregate should be highly cohesive

Lack of cohesion example


class Product {
  void updateDetails(...) // name, description, features
  void updatePrice(...) // amount, currency, validity period
  void makeReservation(...) // how many, how long
}
        

Look for aggregate actors

Content Editor updateDetails()

Marketing Director updatePrice()

Customer makeReservation()

Encapsulate domain

Organize packages around domain


com
└── acme
    └── ecommerce
        └── ordermanagement
            └── domain
                └── orders
                    ├── channels
                    │   ├── allegro
                    │   └── ceneo
                    ├── payments
                    │   ├── cod
                    │   └── payu
                    └── delivery
                        ├── inpost
                        └── dhl
    

Organize classes around domain


package com.acme.ecommerce.ordermanagement.domain.orders

interface OrderRepository {} // repository
class OrderFactory {} // factory
class Order {} // aggregate root
class OrderLine {} // value object
class ProductSummary {} // value object
class TaxCalculator {} // domain service
interface TaxRatesProvider{} // external service
class OrderPaymentAcceptedEvent{} // domain event
    

Be pragmatic in testing

Be pragmatic in testing

Application service


class OrderService {
  @Autowired orderRepository, taxCalculator, orderLineValidator, ...

  @Transactional @PreAuthorized
  void addOrderLine(orderId, productId, productName, quantity, ...) {
     order = orderRepository.load(orderId)

     orderLineValidator.validate(...)
     tax = taxCalculator.calculate(...)
     order.addOrderLine(...)

     orderRepository.save(order)
  }
}
        

No unit tests, acceptance tests on service layer

Be pragmatic in testing

Domain entity or service


class Order {
  Order(orderId, customerId, ...) {}

  void addOrderLine(...) {
    if (isOrderLineForProductExist(...)) {
      orderLines.update(...)
      pendingEvents.add(new OrderLineUpdated(...))
    } else {
      orderLines.add(...)
      pendingEvents.add(new OrderLineAdded(...))
    }
  }
}
        

Only unit tests, sometimes with mocks, no Spring context

Be pragmatic in testing

Infrastructure


class RestTaxRatesProvider implements TaxRatesProvider {
  @Autowired restTemplate, taxRatesAssembler

  List<TaxRate> fetchTaxRates() {
    // authentication
    List<StrangeStructure> results = restTemplate.getForObject(...)
    return taxRatesAssembler.assembly(results)
  }
}
      

Spring integration tests using production like environment

Be pragmatic in testing

  • Acceptance tests on service layer
  • Unit tests for domain
  • Integration tests for infrastructure

Domain events for bounded contexts integration

Dragons are there

Traditional way


class OrderService {
  void placeOrder(...) {
    order = orderFactory.createOrder(...)
    orderRepository.save(order)

    emailNotificationService.notifyCustomer(...)
    indexerService.updateIndex(...)
    inventoryService.makeReservation(...)
    paymentService.sendPayment(...)
    (...)
  }
}
        

It sucks

  • Temporal coupling
  • Degraded availability
  • Lack of atomicity
  • Business logic drifts from domain

Integration using domain events


class Order implements Aggragate {
  void placeOrder(...) {
    (...)
    pendingEvents.add(new OrderPlacedEvent(...))
  }
  List<DomainEvent> pendingEvents = []
}
        

class OrderService {
    void placeOrder(...) {
      order = orderFactory.createOrder(...)
      order.placeOrder(...)
      orderRepository.save(order)
    }
}
        

@AfterReturning(pointcut = "execution(public * *.Repository+.save(..))")
void publishPendingEvents(Aggregate aggregate) {
  for (DomainEvent event : aggregate.getPendingEvents()) {
    eventPublisher.publish(event)
  }
}
        

Seems to work

  • No temporal coupling
  • Availability == messaging middleware availability
  • Almost atomic
  • Domain model stays encapsulated

But …

Be fine with “eventual consistency”

Assume “at least one message delivery guarantee”

Idempotent receiver

Deduplication

Handle out of order domain events

Consistent hashing

Total order

Re-sequencing messages

Configure redelivery policy

Exponential back off

TTL

Dead Letter Queue

Apply versioning strategy

Master message middleware management and monitoring tools

Look for the most aged messages

Manage messages in DLQs

Summary

Holistic approach to design complex systems

Q & A