PoshJosh's Blog

Modifying legacy applications using domain driven design (DDD)

August 10, 2022

Why?

I have written 2 whys. Which do you think best fits this article? Let me know in the comment section below :).

Oh, shoot! I forgot that this is a statically generated site. Hmmmn. I have to think about accepting comments.

Candidate 1

On being introduced to the definition of Democracy as: “Government for the people, of the people and by the people”, Akpos jocularly replied “What a selfish bunch of people!“. Ironically, the most popular definition of democracy may not be understood by the vast majority of “the people”. Could we then say that “the people” are out of sync with the understanding of democracy? Domain driven design (DDD) is an effective way, to determine and record the understanding (behaviour, model etc), of a system and its subsystems, towards keeping various levels and parts of that system, in sync with that understanding.

Candidate 2

Any complex system has 2 enemies. How well it is expressed, and time. Language and its short-comings. Time, and the changes it brings. These breed complexity. Soon, their assault on our code base leads to “developer” pointing out to his colleagues:

No one knows exactly how that works!

Domain driven design (DDD) is an effective way, to determine and record the understanding (behaviour, model etc), of a system and its subsystems, towards keeping various levels and parts of that system, in sync with that understanding.

What

This article provides an overview of how legacy code could be refactored in line with DDD.

A basic understanding of DDD is required to fully understand this article.

A brief introduction to Domain Driven Design

What is Domain Driven Design (DDD)?

Domain-Driven Design is an approach to software development that centers the development on programming a domain model that has a rich understanding of the processes and rules of a domain. The name comes from a 2003 book by Eric Evans that describes the approach through a catalog of patterns.

At the heart of this was the idea that to develop software for a complex domain, we need to build Ubiquitous Language that embeds domain terminology into the software systems that we build.

A particularly important part of DDD is the notion of Strategic Design - how to organize large domains into a network of Bounded Contexts

Source: https://martinfowler.com/bliki/DomainDrivenDesign.html

DDD’s Focus

  • Only apply DDD to those bounded contexts for which DDD will provide value.

  • Start with the core domain. The core domain is that domain which provides the most value to your business.

  • Move domain behaviour to the domain model. For code bases without DDD, the behaviour usually exists in the code (E.g in a controller or service).

Moving behaviour to the domain model

Example of a legacy domain model (i.e. a domain model without DDD).

Note: some code has been omitted for simplicity

public class Customer {
    private BigDecimal moneySpent;
    private List<Subscription> subscriptions;

    public Customer() { }

    public BigDecimal getMoneySpent() { return this.moneySpent; }
    public void setMoneySpent(BigDecimal moneySpent) { this.moneySpent = moneySpent; }
    public List<Subscription> getSubscriptions() { return this.subscriptions; }
    public void setSubscriptions(List<Subscription> subscriptions) { this.subscriptions = subscriptions; }
}
public class Product { }
public enum SubscriptionStatus {
    Active, Cancelled, Suspended
}
public class Subscription {

    private SubscriptionStatus status;
    private Customer customer;
    private Product product;
    private BigDecimal amount;

    public Subscription() { }

    public SubscriptionStatus getStatus() { return this.status; }
    public void setStatus(SubscriptionStatus status) { this.status = status; }
    public Customer getCustomer() { return this.customer; }
    public void setCustomer(Customer customer) { this.customer = customer; }
    public Product getProduct() { return this.product; }
    public void setProduct(Product product) { this.product = product; }
    public BigDecimal getAmount() { return this.amount; }
    public void setAmount(BigDecimal amount) { this.amount = amount; }
}

The CustomerService class below is an example of how a legacy application (i.e. an application without DDD) may handle the adding of subscription for a customer.

public class CustomerService {
    
    public Subscription addSubscriptionToCustomer(Customer customer, Product product, BigDecimal amount) {
        Subscription subscription = new Subscription();
        subscription.setStatus(SubscriptionStatus.Active);
        subscription.setCustomer(customer);
        subscription.setProduct(product);
        subscription.setAmount(amount);
        customer.getSubscriptions().add(subscription);
        customer.setMoneySpent(customer.getMoneySpent().add(amount));
        return subscription;
    }
}

Steps to move behaviour to the domain model.

1. Move construction behaviour

Our subscription model becomes:

public class Subscription {

	private final SubscriptionStatus status;
	private final Customer customer;
	private final Product product;
	private final BigDecimal amount;

	public Subscription(Customer customer, Product product, BigDecimal amount) { 
		this.status = SubscriptionStatus.Active;
		this.customer = Objects.requireNonNull(customer);
		this.product = Objects.requireNonNull(product);
		this.amount = requirePositive(amount);
	}

	private boolean requirePositive(BigDecimal amount) {
		if (amount.compareTo(BigDecimal.ZERO) <= 0) {
			throw new IllegalArgumentException("Subscription amount can not be less than or equal zero");
		}
		return amount;
	}

	public SubscriptionStatus getStatus() { return this.status; }
	public Customer getCustomer() { return this.customer; }
	public Product getProduct() { return this.product; }
	public BigDecimal getAmount() { return this.amount; }
}

We have removed all setter methods.

We moved the construction behaviour to be part of the domain model. The consumer does not need to concern itself with this behaviour. In addition to other possible improvements, we can no-longer create a Subscription having:

  • An invalid initial status (e.g. Cancelled).
  • A null Product.
  • A null Customer.

Now we use the above construction behaviour to simplify our CustomerService as follows:

public class CustomerService {
    
    public Subscription addSubscriptionToCustomer(Customer customer, Product product, BigDecimal amount) {
        Subscription subscription = new Subscription(customer, product, amount);
        customer.getSubscriptions().add(subscription);
        customer.setMoneySpent(customer.getMoneySpent().add(amount));
        return subscription;
    }
}

2. Move related domain behaviour

Next we need to move the behaviour whereby a subscription is added for a customer. This behaviour is captured by the addSubscriptionToCustomer method below:

public class CustomerService {
    
    public Subscription addSubscriptionToCustomer(Customer customer, Product product, BigDecimal amount) {
        Subscription subscription = new Subscription(customer, product, amount);
        customer.getSubscriptions().add(subscription);
        customer.setMoneySpent(customer.getMoneySpent().add(amount));
        return subscription;
    }
}

In the above case, developers need to always remember to update amount of money spent by a customer every time a subscription is added for a customer. With DDD, we move the adding of a subscription to the customer model so that the money spent can be a readonly property of that model.


import static java.util.Collections.unmodifiableList;

public class Customer {

	private BigDecimal moneySpent = BigDecimal.ZERO;
	private List<Subscription> subscriptions = new ArrayList<>(); 

	public Customer() {  }

	public BigDecimal getMoneySpent() { return this.moneySpent; }
	public List<Subscription> getSubscriptions() { unmodifiableList(this.subscriptions); }

	public Subscription addSubscription(Product product, BigDecimal amount) { 
		Subscription subscription = new Subscription(this, product, amount);
		subscriptions.add(subscription);
		moneySpent = moneySpent.add(amount);
	}
}

The above code is cleaner from a BDD perspective. For example, consumers no longer need to worry about updating the money spent each time a subscription is added for a customer.

Note:

Return an unmodifiable list from the getSubscriptions() method above. This protects the relation between moneySpent and the list of subscriptions a customer has. For example the list of subscriptions may only be modified in a way consistent with expected domain behaviour. In particular, a developer can not add a subscription without increasing the money spent by the subscription amount. We achieve this through an addSubscription method that takes care of all related changes. This way, code like shown below is no longer possible.

customer.getSubscriptions().add(subscription);

How do I know which domain model to move a behaviour to?

  • Consider the domain model that will be mutated by the behaviour you are trying to move.
public class CustomerService {

    public Subscription addSubscriptionToCustomer(Customer customer, Product product, BigDecimal amount) {
        Subscription subscription = new Subscription(customer, product, amount);
        customer.getSubscriptions().add(subscription);
        customer.setMoneySpent(customer.getMoneySpent().add(amount));
        return subscription;
    }
}

Since the customer model contains a list of subscriptions as well as the money spent, then it is DDD consistent to have the addSubscriptionToCustomer method in the customer model.

  • Consider the method arguments of the method you are trying to move
Instant calculateBillingPeriodEndDate(Product product) {
    // ..        
}

The above method could be moved to the Product domain model, since it has a single dependency i.e. Product

  • Consider moving cross-cutting behaviour to a domain service. If the behaviour does not match

any of the existing entities, we can move it to a domain service. This is a class that handles cross-cutting behaviour.

Let’s go back to our customer service, which we transformed from:

public class CustomerService {

    public Subscription addSubscriptionToCustomer(Customer customer, Product product) {
        BigDecimal amount = calculateSubscriptionAmount(product);
        return addSubscriptionToCustomer(customer, product, amount);
    }

    public Subscription addSubscriptionToCustomer(Customer customer, Product product, BigDecimal amount) {
        Subscription subscription = new Subscription();
        subscription.setStatus(SubscriptionStatus.Active);
        subscription.setCustomer(customer);
        subscription.setProduct(product);
        subscription.setAmount(amount);
        customer.getSubscriptions().add(subscription);
        customer.setMoneySpent(customer.getMoneySpent().add(amount));
        return subscription;
    }
    
    private BigDecimal calculateSubscriptionAmount(Product product) {
        //
    }
}

to:

public class CustomerService {

    public Subscription addSubscriptionToCustomer(Customer customer, Product product) {
        BigDecimal amount = calculateSubscriptionAmount(product);
        return customer.addSubscription(product, amount);
    }
    
    public BigDecimal calculateSubscriptionAmount(Product product) {
        // ..
    }
}

At first glance, it would seem that the method calculateSubscriptionAmount(Product) belongs to the Product domain model, because it has a single method argument of type Product. However, the method returns a subscription amount. This amount, is used to create a Subscription for the Customer domain model. This is a cross-cutting concern: an amount, is created from a Product and used by a Customer Subscription. Therefore, we could move the method calculateSubscriptionAmount(Product) to a SubscriptionAmountCalculationService.

Now that we have handled cross-cutting concerns, we can now fully migrate all behaviour related to adding subscription for a customer to the Customer domain model. Here is our updated customer.

public class Customer {
    
    public Subscription addSubscription(Product product, SubscriptionAmountCalculator subscriptionAmountCalculator) {
        BigDecimal amount = subscriptionAmountCalculator.calculateSubscriptionAmount(product);
        Subscription subscription = new Subscription(this, product, amount);
        subscriptions.add(subscription);
        moneySpent = moneySpent.add(amount);
    }
}

Domain Events

What happens if we want to notify the customer after their subscription. This is a kind of post subscription action. We can deduce the following requirements for designing such an action:

  • The post subscription action needs to be done asynchronously.
  • The post subscription action should be decoupled from any particular action (e.g. notifying the customer) This way, we can specify other actions that may happen post subscription.

The above requirements could be met by the observer pattern using events and event listeners. Springframework makes using application events easy. Instances of org.springframework.context.ApplicationEvent may be published by using org.springframework.context.ApplicationEventPublisher. For example, we could create a SubscriptionPersistedEvent as shown below:

public class SubscriptionPersistedEvent extends org.springframework.context.ApplicationEvent {
    private final Subscription subscription;
    public SubscriptionPersistedEvent(Subscription subscription) {
        this.subscription = java.util.Objects.requireNonNull(subscription);
    }
    public Subscription getSubscription() {
        return this.subscription;
    }
}

Each time a subscription is added successfully, we could trigger the event as follows:

public class EventPublicationService {

    private final ApplicationEventPublisher applicationEventPublisher;

    public EventPublicationService(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }
    
    public void publishSubscriptionAddedEvent(Subscription subscription) {
        applicationEventPublisher.publishEvent(new SubscriptionPersistedEvent(subscription));
    }
}

This event could have any number of listeners, for example:

import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class StakeholdersNotifierOnCustomerSubscribed {
    
    private final EmailService emailService;
    
    public StakeholdersNotifierOnCustomerSubscribed(EmailService emailService) {
        this.emailService = emailService;
    }
    
    @EventListener
    public void onSubscriptionPersistedEvent(SubscriptionPersistedEvent subscriptionPersistedEvent) {
        Subscription subscription = subscriptionPersistedEvent.getSubscription();
        emailService.notifyAdmin(subscripton);
        emailService.notifyCustomer(subscripton);
    }
}

This way, each time we publish a subscription added event, all listeners get notified. In this case we have to manually call the method EventPublicationService.publishSubscriptionAddedEvent(Subscription) On the other hand, if we want this method to be called automatically, each time a new Subscription is persisted to the database, then we can rely on java persistence API (JPA) annotations as shown below:

We first create a Subscription listener.

import javax.persistence.PostPersist;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Component;

@Component
public class SubscriptionListener {

    private final ApplicationEventPublisher applicationEventPublisher;
    
    public SubscriptionListener(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }
    
    @PostPersist
    private void onSubscriptionPersisted(Subscription subscription) {
        applicationEventPublisher.publishEvent(new SubscriptionPersistedEvent(subscription));
    }
}

Then we specify which entity the SubscriptionListener applies to.

import javax.persistence.Entity;
import javax.persistence.EntityListeners;

@EntityListeners(SubscriptionListener.class)
@Entity
public class Subscription {
    // ..
}

This way, The method annotated with @PostPersist will be called each time a new Subscription is persisted.

A note on Value Objects

In DDD, value objects differ from entities by lacking the concept of identity. We do not care who they are but rather what they are. They are defined by their attributes and should be immutable.

For example, domain experts may refer to contact details. Assuming we do not have a ContactDetails domain model. We could create a ContactDetails value object from, for example, related Customer and Address domain models. Similarly, we could create a CustomerName value object from first, middle (other) and last (family) name.

Two Customer entities are equal if their IDs are equal. On the other hand, 2 CustomerName value objects are equal if all their attributes are equal.

Best practice using Entity IDs

Use a base entity ID. Preferably have strongly typed Ids.

public class EntityId {
    private String id;
    public EntityId(String id) {
        this.id = id;
    }
}
public class SubscriptionId extends EntityId{
    public SubscriptionId(String id) {
        super(id);
    }
}

Advantages

  • We could switch from String, to say Integer or UUID without breaking existing code.
  • We are prevented from accidentally using a CustomerId in place of a SubscriptionId

Conclusion

Domain driven design (DDD) is an effective way, to determine and record the understanding (behaviour, model etc), of a system and its subsystems, towards keeping various levels and parts of that system, in sync with that understanding.

Legacy code could be refactored in line with DDD by moving behaviour to the domain model.

References


Written byChinomso IkwuagwuExcélsior

Limited conversations with distributed systems.

Modifying legacy applications using domain driven design (DDD)

Gherkin Best Practices

Code Review Best Practices

Hacking Cypress in 9 minutes

Some common mistakes when developing java web applications

How to make a Spring Boot application production ready

SQL JOINS - A Refresher

Add Elasticsearch to Spring Boot Application

Add entities/tables to an existing Jhipster based project

CSS 3 Media Queries - All over again

Maven Dependency Convergence - quick reference

Amazon SNS Quick Reference

AWS API Gateway Quick Reference

Amazon SQS Quick Reference

AWS API Gateway Quick Reference

AWS Lambda Quick Reference

Amazon DynamoDB - Quick Reference

Amazon Aurora

Amazon Relational Database Service

AWS Database Services

AWS Security Essentials

Amazon Virtual Private Cloud Connectivity Options

Summary of AWS Services

AWS Certified Solutions Architect - Quick Reference

AWS CloudFront FAQs - Curated

AWS VPC FAQs - Curated

AWS EC2 FAQs - Curated

AWS Achritect 5 - Architecting for Cost Optimization

AWS Achritect 4 - Architecting for Performance Efficiency

AWS Achritect - 6 - Passing the Certification Exam

AWS Achitect 3 - Architecting for Operational Excellence

AWS Achitect 2 - Architecting for Security

AWS Achitect 1 - Architecting for Reliability

Amazon DynamoDB Accelerator (DAX)

Questions and Answers - AWS Certified Cloud Architect Associate

Questions and Answers - AWS Certified Cloud Architect Associate

AWS Connectivity - PrivateLink, VPC-Peering, Transit-gateway and Direct-connect

AWS - VPC peering vs PrivateLink

Designing Low Latency Systems

AWS EFS vs FSx

AWS Regions, Availability Zones and Local Zones

AWS VPC Endpoints and VPC Endpoint Services (AWS Private Link)

AWS - IP Addresses

AWS Elastic Network Interfaces

AWS Titbits

Jenkins on AWS - Automation

Jenkins on AWS - Setup

Jenkins on AWS - Best practices

Introduction to CIDR Blocks

AWS Lamda - Limitations and Use Cases

AWS Certified Solutions Architect Associate - Part 10 - Services and design scenarios

AWS Certified Solutions Architect Associate - Part 9 - Databases

AWS Certified Solutions Architect Associate - Part - 8 Application deployment

AWS Certified Solutions Architect Associate - Part 7 - Autoscaling and virtual network services

AWS Certified Solutions Architect Associate - Part 6 - Identity and access management

AWS Certified Solutions Architect Associate - Part 5 - Compute services design

AWS Certified Solutions Architect Associate - Part 4 - Virtual Private Cloud

AWS Certified Solutions Architect Associate - Part 3 - Storage services

AWS Certified Solutions Architect Associate - Part 2 - Introduction to Security

AWS Certified Solutions Architect Associate - Part 1 - Key services relating to the Exam

AWS Certifications - Part 1 - Certified solutions architect associate

AWS Virtual Private Cloud (VPC) Examples

Curated info on AWS Virtual Private Cloud (VPC)

Notes on Amazon Web Services 8 - Command Line Interface (CLI)

Notes on Amazon Web Services 7 - Elastic Beanstalk

Notes on Amazon Web Services 6 - Developer, Media, Migration, Productivity, IoT and Gaming

Notes on Amazon Web Services 5 - Security, Identity and Compliance

Notes on Amazon Web Services 4 - Analytics and Machine Learning

Notes on Amazon Web Services 3 - Managment Tools, App Integration and Customer Engagement

Notes on Amazon Web Services 2 - Storages databases compute and content delivery

Notes on Amazon Web Services 1 - Introduction

AWS Auto Scaling - All you need to know

AWS Load Balancers - How they work and differences between them

AWS EC2 Instance Types - Curated

Amazon Web Services - Identity and Access Management Primer

Amazon Web Services - Create IAM User

Preparing Jenkins after Installation

Jenkins titbits, and then some

Docker Titbits

How to Add Chat Functionality to a Maven Java Web App

Packer - an introduction

Terraform - an introduction

Versioning REST Resources with Spring Data REST

Installing and running Jenkins in Docker

Automate deployment of Jenkins to AWS - Part 2 - Full automation - Single EC2 instance

Automate deployment of Jenkins to AWS - Part 1 - Semi automation - Single EC2 instance

Introduction to Jenkins

Software Engineers Reference - Dictionary, Encyclopedia or Wiki - For Software Engineers