avatarGeorge Sotiropoulos

Summary

The provided content outlines the design and implementation of a Hexagonal Architecture for a microservice-based e-commerce application using Domain-Driven Design (DDD) principles, with a focus on the Order Management subdomain and its Aggregate, the Order Aggregate.

Abstract

The text delves into the intricacies of constructing a robust e-commerce microservice architecture by applying Domain-Driven Design (DDD) and Hexagonal Architecture principles. It emphasizes the importance of identifying and understanding subdomains within the e-commerce business to create well-defined bounded contexts. The article particularly concentrates on the Order Management subdomain, detailing the process of modeling the Domain Hexagon, which includes Entities, Value Objects, Aggregates, Repositories, Domain Events, and Domain Services. It discusses the design choices for the Order Aggregate, including its identity, lifecycle management, and transactional consistency. The text also explores the use of the Specification Pattern to enforce business invariants and the role of the Application Layer in orchestrating the Domain and Framework layers. The implementation of the Order Aggregate is demonstrated with code examples, and the package structure of the Common and Domain layers is presented to illustrate the adherence to the Common Closure Principle.

Opinions

  • The author advocates for the use of Hexagonal Architecture in microservices to ensure that the business logic remains independent of external frameworks and databases.
  • There is a strong emphasis on the importance of identifying subdomains and bounded contexts to align the software architecture with the business domain effectively.
  • The choice of using a single Aggregate for the Order subdomain is justified by the simplicity of managing the entire order as a cohesive unit, although the text acknowledges the trade-offs involved in this decision.
  • The use of Value Objects, such as ProductId and CustomerId, is recommended for referencing other Aggregates by identity, ensuring clear reference and identity tracking across microservices.
  • The author suggests that the decision to model an OrderItem as an Entity or Value Object should be driven by its behavior and conceptual characteristics within the domain, not just by its attributes.
  • Behavior-Driven Development (BDD) is presented as a valuable tool for identifying the commands and queries that the Order Aggregate should handle, thus guiding the implementation of its methods.
  • The Specification Pattern is introduced as a method to encapsulate business rules and facilitate the validation of invariants within the Order Aggregate, enhancing maintainability and flexibility.
  • The Application Layer is described as a crucial component that acts as a mediator between the Domain and Framework layers, ensuring loose coupling and facilitating the flow of data and commands.

Quarkus. Microservices & Hexagonal Architecture. How to Build your Hexagon Layers. The ‘Aggregate’ and the ‘Domain Hexagon’. Part 2.

Techniques on how to design and implement the Internal Layers of a Hexagonal Architecture for microservices based on Domain Driven Design in Quarkus. E-commerce Case Study.

In part 1 of these article series of building a Hexagonal DDD microservices application with Quarkus, we took an unexpected turn by diving into constructing some parts of the application layer and framework layer before identifying our business’s smaller, more manageable parts — known as subdomains.

However, I wanted to provide with a sneak peek into the practical side of building a hexagon, although It’s like starting to build a house without knowing exactly what rooms you need!

Now, let’s take a step back and do things in a more traditional way. We’ll start by identifying and understanding these subdomains in our E-commerce business, and later we will start building the Domain Layer and the Aggregate.

Mapping our E-commerce Business Domain to Subdomains.

Let’s assume that we have just finished the Strategic Phase of Domain Driven Design, and we mapped our ecommerce business domain to the following Subdomains.

Order Management Subdomain This area deals with everything related to customer orders. How are orders created, modified, or canceled. What happens when a customer checks out. This is the subdomain we are implementing so far.

Product Catalog Subdomain In this space, we handle everything about products. What details do we need to know about each product. How do we organize and display them for customers to browse. The product catalog subdomain helps us keep our products organized and accessible.

Inventory Subdomain: Inventory helps us keep track of how much of each product we have in stock, handle restocking, and avoid running out of popular items.

Customer Management Subdomain: This subdomain focuses on managing customer information, such as profiles, preferences, and order histories.

Payment and Billing Subdomain: This subdomain handles payments and billing. How do we securely process payments? What’s the billing process like for customers? This area ensures our business transactions are smooth and secure.

Shipping and Fulfillment Subdomain: Once an order is placed, there’s the journey of getting it to the customer. The shipping and fulfillment subdomain tackles questions about logistics, packaging, and delivery.

The hypothesis: The Bounded Contexts and 1–1 alignment with Subdomains.

To derive bounded contexts, we must identify the boundaries within which each subdomain operates independently. Bounded contexts help define the scope and responsibilities of a particular subdomain in the context of your overall business.

Our hypothesis is ALWAYS the 1–1 alignment of Bounded Contexts with Subdomains

Here is a breakdown of bounded contexts along with interactions and responsibilities based on above Subdomains of our e-commerce application:

Order Management Bounded Context

Responsibilities: Shopping Cart Order Placement Checkout Process Management

Interactions with other subdomains: Interaction with Customer Management for order history and customer information. Interaction with Product Catalog for product name and pricing information. Interaction with Inventory for reserve stock, during the checkout process. Interaction with Payments, for validate payment, during the checkout process. Interaction with Inventory & Shipping, for tracking overall order process status.

Product Catalog Bounded Context

Responsibilities: Product management of details and attributes. Organization and display of products for browsing. Product reviews and ratings

Interactions with other subdomains: Interaction with Inventory for stock information.

Customer Management Bounded Context

Responsibilities: Customer profiles and preferences. Order histories.

Interactions with other subdomains: Interaction with Order Management for order-related information. Interacttions with Payment and Billing Bounded Context.

Payment and Billing Bounded Context

Responsibilities: Secure processing of payments. Billing processes for customers.

Interactions with other subdomains: Interaction with Order Management for order payment processing.

Inventory Bounded Context

Responsibilities:

Stock tracking for each product. Stock reserving for a product. Restocking and inventory management.

Interactions with other subdomains: Interaction with Product Catalog for product details.

Shipping and Fulfillment Bounded Context

Responsibilities: Logistics, packaging, and delivery.

Interactions with other subdomains: Interaction with Order Management for order details.

Decomposition into Micro-Services.

Based on the hypothesis of the 1 to 1 alignment between our Bounded Contexts and our Subdomains and according to Richardson and the “Decompose by Subdomain Pattern” as l already mentioned in another article:

“Define services corresponding to Domain-Driven Design (DDD) subdomains. DDD refers to the application’s problem space — the business — as the domain. A domain is consisting of multiple subdomains. Each subdomain corresponds to a different part of the business (Richardson, n.d.).”

We proceed and build 1 to 1 alignment between a Subdomain, a Bounded Context and a microservice.

Therefore, what we have essentially One Bounded Context aligns to  One Subdomain -> maps to one Microservice. Based on this rule with end up with 6 microservices in our final design.

Figure 1. E-Commerce Subdomains One — to — One Mapping with Microservices

The ‘Domain’ Hexagon Main Components.

Now, let’s focus our detective lens on the Domain Hexagon of our Order Management Subdomain

According to the research (Evans, 2003):

“Domain Layer (or Model Layer): Responsible for representing concepts of the business, information about the business situation, and business rules. State that reflects the business situation is controlled and used here, even though the technical details of storing it are delegated to the infrastructure. This layer is the heart of business software.

In Domain Driven Design our Domain Hexagon contains the following components. In this article we are going to analytically explain the first three — value objects, entities and aggregates — but here is a small description of each.

  • Entities: Core domain objects representing the primary concepts and business logic of the application.
  • Value Objects: Immutable objects representing attributes or characteristics of entities.
  • Aggregates: Groups of related entities and value objects that ensure consistency and transactional boundaries.
  • Repositories: Abstractions for data access and domain object retrieval, keeping the core domain layer independent of data access concerns.
  • Domain Events: Events that capture changes or important occurrences within the domain, often used to communicate between the core domain and application services.
  • Domain Services: Additional domain-specific services that do not naturally fit within the entities or value objects.

The primary and crucial element in formulating our Domain Hexagon involves pinpointing Aggregates within. The Aggregate essentially serves as the principal identifier within any Bounded Context. As Evans quotes

“An AGGREGATE is a cluster of associated objects that we treat as a unit for the purpose of data changes (Evans, 2003).”

Before putting effort into this article we need a diagram representing what we are going to build for the next 20 minutes. voilà!

Figure 2. E-Commerce Hexagonal Order Microservice using Domain Driven Design.

For this diagram I’m trying to be consistent with the colors. Each port of a hexagon has the color of the layer it belongs (well it is obvious). Each hexagon communicates to another hexagon through Ports using DI (Dependecy Injection). I didn’t know what color to give to the the actual implementations of them therefore I gave them black and call it even!

Things to notice about layer dependencies:

The Application layer relies on both the Domain and Framework layers.

Simultaneously, the Framework layer depends on the Domain layer (Although it is not obvious in this diagram somehow it must know the stucture of what to persist).

Again I choose to have a combined framework layer instead of a presentation layer and a infrastucture layer and have inbound and outbound adapters and the infrastructure as a separate package for databases, kafka etc.

The Domain layer remains independent and doesn’t rely on any other layer.

NOT LET’S MOVE FORWARD AND SEE HOW WE CAN IDENTIFY THE AGGREGATE OF OUR ORDER SUBDOMAIN. A BIT OF THEORY FIRST.

The Order Aggregate Theory

This cluster of objects consists of a primary entity, referred to as the root entity (Aggregate Root) and may include additional associated entities and value objects.In the context of our Order Subdomain, we define the Order to function as the Aggregate Root, which includes a collection of OrderLineItem objects, as well as other elements like Shipping Address, Payment, and Customer information.

Now Let’s justify our choice, explaining why the Order entity is suitable as the Aggregate Root within our Order Management Subdomain Model. Well “it is obvious” hahahah! But why?

1. Why Order Entity as the Aggregate Root?

Lifecycle Management

The Order entity is typically at the center of the order management lifecycle. It encapsulates the entire process from order creation to modification and cancellation. By designating the Order as the Aggregate Root, we ensure that the lifecycle of the entire order, along with its associated entities, is managed cohesively.

Transactional Consistency

In a prior article, I explored the significance of the Aggregate as a boundary for transactional consistency. The Order entity often involves various interconnected entities, such as OrderItems, Address, and others.

When alterations take place, such as modifying the order or adding items, preserving consistency throughout the entire order is important. By designating the Order as an Aggregate Root, these modifications can be executed within a unified transaction, guaranteeing the atomicity of the entire operation.

Identity and Global Accessibility

An Aggregate Root has a global identity, and in the case of an Order, this global identity is essential for uniquely identifying and referencing orders across our microservice architecture. By designating the Order entity as the Aggregate Root, we establish a single, globally accessible entry point for working with orders.

Enforcement of Business Rules

The Order entity often contains business rules and validations that apply to the order. By making the Order an Aggregate Root, these business rules can be enforced consistently. External components interacting with the Order must go through the Aggregate Root, ensuring that all relevant rules are checked. We will implement these Business Rules later on using the Specification Pattern.

Encapsulation and Simplified Interaction

Designating the Order as an Aggregate Root encapsulates the internal structure of the order, allowing for a simplified interaction model. External components interact primarily with the Order entity, abstracting away the complexities of dealing with individual order-related entities. This encapsulation enhances modularity and maintainability.

Now Let’s talk about another important characteristic. Aggregate Granularity. The general rule of thumb is “keep your aggregates small”.

Although we are going to have only one Aggregate the Order Aggregate for the purpose of simplicity (even one Aggregate is quite complex to implement properly) it is worth discussing the pros and cons of having a single Fat Aggregate vs Small Aggregates.

2. Aggregate Size.

Single Aggregate (This is the one going to implement)

As we model the entire order as a single aggregate, it encapsulates the order details, including the list of product items, cart-related functionalities (addition and removal of items), and the checkout process.

It is a simplified design of our domain model by treating the entire order as a cohesive unit. It is also straightforward to manage within a single aggregate.

A major disadvantage is that entire order is loaded into memory even for minor operations.

Two Aggregates

a) An Order Aggregate which manages the overall order and checkout process.

b) A Cart Aggregate which handles the addition and removal of items in the cart before the order is finalized.

This design enforces the separation of concerns, with the Cart Aggregate specifically addressing real-time cart interaction and the Order Aggregate managing the Order checkout phase.

It is also Future Proof in case we want to break our Order Subdomain even further into a Cart Subdomain and Order Processing Subdomain, thus 2 distinct microservices.

The major disadvantage is the added complexity due to coordination between two aggregates to ensure consistency across the two aggregates.

Three Aggregates

a) An Order Aggregate which manages the overall order and checkout process.

b) A Cart Aggregate which handles the addition and removal of items in the cart before the order is finalized.

c) And a third OrderItem Aggregate for partial order functionality.

A common scenario often involves granting users the ability to cancel a specific OrderItem, particularly if it is fulfilled by an external supplier rather than our own warehouse. This necessitates the introduction of a status attribute for the OrderItem, indicating that the OrderItem possesses state, and it might be beneficial to model it as a distinct Aggregate.

Consequently, this results in a total of three Aggregates in our Order Subdomain. However, we might skip the complexity of having OrderItem as a separate Aggregate and model it as Domain Entity as will we explore later on.

The choice among these approaches depends on factors such as the level of concurrency you expect, the need for transactional consistency, and the trade-offs between simplicity and granularity in your specific e-commerce domain.

It’s essential to carefully analyze the requirements and constraints of the system before deciding on the most suitable aggregate design.

WELL THAT IS A LOT OF THEORY ! BETTER START BUILDING OUR ORDER AGGREGATE. LET’S GO!

First we need to open the order-service project from the previous article in IntelliJ.

Step 1: The Common Closure Principle.

In designing the Order Aggregate for the Order subdomain I’ve chosen strategically to follow the Common Closure Principle — which is a package principle of cohesion — and put all the Aggregate related classes entities, events and business rules together within the following package

{com.ecommerce.hexagon.order_management.domain.aggregate}

The Common Closure Principle (CCP) states:

“The classes in a component should be closed together against the same kind of changes. A change that affects a component affects all the classes in that component and no other components”

This principle emphasizes grouping classes that change for the same reasons into the same package.

By consolidating all the classes related to the Order Aggregate into a single package, we ensure that they share a common closure — that is, they are likely to change together due to similar reasons (Hopefully!).

Step 2: Build the Order Aggregate.

Our Aggregate Root is going to have a unique ID, Value Objects, Entities, Domain Rules and Domain Events. This is our OrderAggregate class from the previous step. We add the main atrributes such as value objects, entities for start:

We have already created (ID , Money , Quantity, NameSurname, Payment, Address , Contact etc) in part 1.

OrderAggregate.java

Layer:Domain Package:/domain.aggregate; Type:Aggregate Root

package com.ecommerce.hexagon.order_management.domain.aggregate;

import com.ecommerce.hexagon.common.domain.DomainEntity;
import com.ecommerce.hexagon.common.domain.IAggregateRoot;
import com.ecommerce.hexagon.order_management.domain.status.OrderStatus;
import com.ecommerce.hexagon.common.valueobjects.*;
import com.ecommerce.hexagon.common.exceptions.BusinessException;
import com.ecommerce.hexagon.common.events.OrderChangedEvent;
import com.ecommerce.hexagon.common.domain.ISpecification;
import com.ecommerce.hexagon.order_management.domain.specification.NonNegativeQuantitiesSpecification;
import com.ecommerce.hexagon.order_management.domain.specification.TotalEqualsSumOfOrderItemsSpecification;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;


import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;


@Getter
public class OrderAggregate implements IAggregateRoot {

    private ID orderId;

    //Aggregate References as Value Objects
    private CustomerId customer;

    private Description orderFriendlyName;

    private OrderStatus orderStatus;

    private Money orderTotalCost;

    //Entities
    private List<OrderItem> orderItems = new ArrayList<>();

    private Checkout checkout;


    @JsonIgnore
    public ID getId() {
        return orderId;
    }


    @Override
    public void validateDefaultInvariants() throws BusinessException {
    
    }

    @Override
    public OrderAggregateRecord getImmutable() {
    }

}



The IAggregateRoot is within the common package com.ecommerce.hexagon.common.domain — a library shared accross all microsevices.

IAggregateRoot.java

Layer:Common Package:/common.domain; Type:Aggregate Root Interface

package com.ecommerce.hexagon.common.domain;


public interface IAggregateRoot {
   public  void validateDefaultInvariants() throws BusinessException;
   public AggregateRecord getImmutable();

}

In part 1 we said that our AggregateRoot can be left empty i and is used for schemantic meaning. In this part we have added the validateDefaultInvariants() method which is a common Aggregate Root DDD functionality to check the invariants of the Aggregate as well as getImmutable() to return an immutable record of the Aggregate to the Framework Layer.

Let’s create the immutable version of the OrderAggreate as a Java Record.

OrderAggregateRecord.java

Layer:Domain Package:/domain.aggregate; Type:Immutable Version of Aggregate

package com.ecommerce.hexagon.order_management.domain.aggregate;

import com.ecommerce.hexagon.common.status.OrderStatus;
import com.ecommerce.hexagon.common.valueobjects.ID;
import com.ecommerce.hexagon.common.valueobjects.Money;
import java.util.List;


public record OrderAggregateRecord(ID id, CustomerId customerId, List<OrderItem> orderItems, OrderStatus orderStatus, Money orderTotalCost) {
}


And implement the getImmutable()method in the OrderAggregate

...................
// create an immutable OrderAggregate
    @Override
    public OrderAggregateRecord getImmutable() {
        OrderAggregateRecord immutableRecord = new OrderAggregateRecord(orderId, customer,Collections.unmodifiableList(orderItems),orderStatus,orderTotalCost);
        return  immutableRecord;
    }
........................

I’ve also decided to have all the Checkout Value Objects (shipping information, payment etc) in a Bigger Value Object bag. We may mark this special kind of Value Objects as Complex Value Objects by extending a ComplexValueObject class.

CompleValueObject.java Layer:Common Package:common.domain Type:Abstract Class for a Value Object Bag (What a name!)

package com.ecommerce.hexagon.common.domain;

public abstract class ComplexValueObject extends ValueObject {
}

Checkout.java Layer:Domain Package:order_management.domain.aggregate Type:Complex Value Object

package com.ecommerce.hexagon.order_management.domain.aggregate;

import com.ecommerce.hexagon.common.domain.ComplexValueObject;
import com.ecommerce.hexagon.common.valueobjects.Address;
import com.ecommerce.hexagon.common.valueobjects.Contact;
import com.ecommerce.hexagon.common.valueobjects.NameSurname;
import com.ecommerce.hexagon.common.valueobjects.Payment;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

import java.util.Objects;

@Setter
@Getter
@ToString
public class Checkout extends ComplexValueObject {

    private NameSurname shippingNameSurname;
    private Payment payment;
    private Address shippingAddress;
    private Contact contact;

    protected Checkout(Contact contact, NameSurname shippingNameSurname, Payment payment, Address shippingAddress) {
        this.contact = contact;
        this.shippingNameSurname = shippingNameSurname;
        this.payment = payment;
        this.shippingAddress = shippingAddress;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Checkout checkout = (Checkout) o;
        return Objects.equals(shippingNameSurname, checkout.shippingNameSurname) &&
                Objects.equals(payment, checkout.payment) &&
                Objects.equals(shippingAddress, checkout.shippingAddress) &&
                Objects.equals(contact, checkout.contact);
    }

    @Override
    public int hashCode() {
        return Objects.hash(shippingNameSurname, payment, shippingAddress, contact);
    }
}

AGAIN THIS CLASS IS RELATED TO THE ORDER AGGREGATE THUS IT RESIDES WITHIN THE ‘’Aggregate’ PACKAGE ACCORDING TO THE CCP PRINCIPLE.

Let’s also add the OrderStatus class. This class represents the various state changes of the OrderAggregate. In later aticles we dive deeper into these and how to figure them out using a technique named EventStorming. Don’t worry much about them at the moment.

I’m not sure regarding the package and the name. We can either have it within common- since they might be shared accross microservices — however is related to our specific order subdomain. A wiser decision is to create a new package status within our domain.

OrderStatus.java Layer:Domain Package:domain.status Type:State Status

package com.ecommerce.hexagon.order_management.domain.status;


public  enum OrderStatus {
    PENDING(1),
    AWAITING_PAYMENT(2),
    PAID(3),
    AWAITING_WAREHOUSE_PACKAGING(4),
    PACKAGED(5),
    AWAITING_DELIVERY(6),
    DELIVERED(7),
    COMPLETED(8),
    ERROR_PAYMENT_FAILED(9),
    ERROR_RESERVE_STOCK_FAILED(10),
    ERROR_DELIVERY_FAILED(11),
    ERROR_WAREHOUSE_PROCESSING(12);



    OrderStatus(int code){
        this.code = code;
    }

    private final int code;


    public int getCode() {
        return code;
    }


    public static OrderStatus fromCode(int code) throws IllegalArgumentException {
        for (OrderStatus curstate : OrderStatus.values()) {
            if (curstate.code==code) {
                return curstate;
            }
        }
        throw new IllegalArgumentException("Invalid OrderStatus value: " + code);
    }


}

Finally let’s add the BusinessException class thrown when invariants are violated.

BusinessException.java Layer:Common Package:common.exceptions Type: Exception

package com.ecommerce.hexagon.common.exceptions;

public class BusinessException extends Throwable {
    public BusinessException(String s) {

    }
}

We have made some choices about the attributes regarding the Aggregate Identity, the Value Objects, and the reference to other Aggregates. Our OrderAggregate class tell’s a lot .

It is time to take a break, make the 9th coffee and dive into some DDD theory, regarding value objects and identities.

Step 3: Special ID Value Objects and Relations to external Aggregate Roots.

3.1:Reference Aggregates By Identity.

Product and Customer, are the Aggregate Roots of the Product Catalog Subdomain and Customer Subdomain respectively. THAT IS A FACT.

By assigning them unique global identifiers, we enable clear reference and identity tracking across our microservices. This modeling decision ensures that Products and Customers are not merely attributes but are recognized and managed as standalone entities with a persistent identity throughout the micro-services architecture, reflecting their crucial roles as Aggregate Roots in their respective subdomains.

In Domain Driven Design each Aggregate maintains its autonomy, and communication occurs via these unique identifiers. Our implementation of the Order Aggregate holds references to specific products (Product Aggregate) and customers (Customer Aggregate) using their globally unique identities.

Finally, to emphasize that these Id Value Objects are designed to reference only the Ids of other aggregate roots, we make them implementing an Interface named ExternalAggregateRoot.

A Few Thoughts

“Choosing meaningful names in software engineering is indeed crucial for code readability and maintainability.”

“Considering that ProductId is intended to represent products in an order and serves as a reference to an external aggregate root, you might want to choose a name that clearly conveys its purpose. Maybe ProductId which has an Id as an attribute is confusing. An alternative name is ProductReference or OrderProduct.”

“Also you may choose not to have a separate class but model it as ID. The product Price, product Name might be attributes of OrderItem.

ProductId.java Layer:Domain Package:order_management.domain.aggregate Type:Special ID Value Object, References by Identity another Aggregate Root

package com.ecommerce.hexagon.order_management.domain.aggregate;

import com.ecommerce.hexagon.common.domain.IExternalReferenceAggregateRoot;
import com.ecommerce.hexagon.common.domain.ValueObject;
import com.ecommerce.hexagon.common.valueobjects.Description;
import com.ecommerce.hexagon.common.valueobjects.ID;
import com.ecommerce.hexagon.common.valueobjects.Money;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.ToString;

import java.util.Objects;

@Getter
@ToString
@NoArgsConstructor
public class ProductId extends ValueObject implements IExternalReferenceAggregateRoot {

    private ID id;
    private  Money price;
    private Description name;

    public ProductId(ID id, Money price, Description name) {
        this.id = id;
        this.price = price;
        this.name = name;
    }

    public ProductId(ID id, Money price) {
        this.id = id;
        this.price = price;
    }

    protected Money getPrice() {
        return price;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ProductId productId = (ProductId) o;
        return Objects.equals(id.getUuid(), productId.id.getUuid()) &&
                Objects.equals(price, productId.price);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, price);
    }
}

CustomerId.java Layer:Domain Package:order_management.domain.aggregate Type:Special ID Value Object, References by Identity Aggregate Root

package com.ecommerce.hexagon.order_management.domain.aggregate;

import com.ecommerce.hexagon.common.domain.IExternalReferenceAggregateRoot;
import com.ecommerce.hexagon.common.domain.ValueObject;
import com.ecommerce.hexagon.common.valueobjects.ID;
import com.ecommerce.hexagon.common.valueobjects.NameSurname;

import java.util.Objects;

public class CustomerId extends ValueObject implements IExternalReferenceAggregateRoot {

    private ID id;
    private NameSurname nameSurname;

    public CustomerId() {}

    public CustomerId(ID id) {
        this.id = id;
    }

    public CustomerId(ID id, NameSurname nameSurname) {
        this.id = id;
        this.nameSurname = nameSurname;
    }

    public ID getId() {
        return id;
    }

    @Override
    public String toString() {
        if (nameSurname != null) {
            return String.format("%s - %s", id, nameSurname);
        } else {
            return id.toString();
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        CustomerId that = (CustomerId) o;
        return Objects.equals(id, that.id) &&
                Objects.equals(nameSurname, that.nameSurname);
    }

    @Override
    public int hashCode() {
        return Objects.hash(id, nameSurname);
    }
}

3.2:The Aggregate Identifier ID

In our implementation of the aggregate identifier, we opt for both a technical/surrogate key (Primary Key) and a corresponding business key (Unique Key).

Business key (UUID) acts as a Global Identifier of any Aggregate and is this key that is exposed to other microservices.

Technical key serves as a purely internal representation of the aggregate identifier.

Since we need persistence that technical key is used as the primary key. Also in cases where we have 2 or more aggregates the technical key is used by a Domain Service which orchestrates the Aggregates.

ID.java Layer:Common Package:common.valueobjects Type: Value Object

package com.ecommerce.hexagon.common.valueobjects;

import com.ecommerce.hexagon.common.domain.ValueObject;
import jakarta.persistence.Embeddable;
import lombok.Getter;
import lombok.Setter;


import java.util.UUID;

@Setter
@Getter
@Embeddable
public final class ID extends ValueObject {

    private Long id;
    private UUID uuid;

    public ID(UUID uuid) {
        this.uuid = uuid;
    }

    private ID(Long id, UUID uuid) {
        this.id = id;
        this.uuid = uuid;
    }

    public ID() {
    }

    public static ID generateFromString(String id) {
        return new ID(UUID.fromString(id));
    }

    public static ID generateRandom() {
        return new ID(UUID.randomUUID());
    }


    @Override
    public String toString() {
        return "ID{" +
                "id=" + id +
                ", uuid=" + uuid +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        ID id1 = (ID) o;
        return uuid.equals(id1.uuid);
    }

    @Override
    public int hashCode() {
        return uuid.hashCode();
    }
}

Step 4:The Order Item Entity.

I have decided to model the OrderItem as an Entity although it is also suitable for a Value Object. Here is my thought. What is an Entity and what a Value Object after all?

4.1: What is an Entity

In Domain Driven Design Entities have a conceptual life-cycle, which means they represent something that changes over time. Eric Evans describe it as

An object that is not fundamentally defined by its attributes, but rather by a thread of continuity and identity”.

4.2: How they actually distinguished from each other

This is the million dollar question. Entities are often backed by a persistence mechanism like an RDBMS, and the distinguishing characteristic is an Id attribute.

4.3: Entity Existence

Entity possess their own identity but are dependent on the root aggregate for existence. In other words, they come into being when the root aggregate is created and cease to exist when the root aggregate is destroyed.

4.4: Is the Aggregate Root an Entity

Aggregate Roots are a specific type of entity. They require other entities to enforce their invariants and they encapsulate access/modification to other entities for transactional consistency.From the external perspective (client of the system), there might not be a clear distinction between an Entity and an Aggregate Root.

4.5: Entity Vs Value Object

Value Objects lack conceptual identity and life-cycle. They can be created and destroyed without the need for tracking changes over time. They may still have a physical identity (e.g., a primary key in an RDBMS) but that identity is hidden from the domain.

T HE RULE OF THUMB IS

The classification of an object as an Entity or Value Object is driven by its behavior and conceptual characteristics within the domain, rather than being determined solely by implementation details or how it is persisted in the data store.

This distinction is crucial for designing effective domain models in DDD. In many articles, and books there is a common tendency to model OrderItem as Value Object. The main reason other than simplicity I think is that OrderItem is actually Identified by the Product and the Quantity. These two attributes actually define the OrderItem and make it a Value Object.

What About Immutability? Value Objects are often immutable, meaning their state cannot be changed once they are created. This aligns well with the idea that the characteristics of an OrderItem, such as product details, quantity, and price, remain constant once the item is added to an order. Do they?

Is the OrderItem really immutable in our Case? At what point it becomes really immutable?

Let’s remind that the classification is ‘driven by its behavior and conceptual characteristics’. The final decision depends on the behavior of the OrderItem within our Order Subdomain.

4.6:Final Decision based on Behaviour.

Our Order Aggregate has a duplicate role as we already discussed. It behaves as the Cart as well as manages the order processing after the Order is submitted. After an Order is submitted an OrderItem is immutable. But in ‘Cart’ Mode quantity changes as well total cost.

And what about cancelling a specific OrderItem? Do we want to have that Future Proof functionality ? Does the the OrderItem has state changes?

We might want to have a kind of OrderItemStatus, and maybe a dispatchedDate since in many cases an OrderItem is supplied and dispatched by a different supplier.

For sure all these changes of the OrderItem introduce a dynamic aspect that aligns more with the characteristics of an Entity rather than a typical Value Object.

We model OrderItem as an Domain Entity. We backup our decision by introducing OrderItem life-cyle functionality. We need to track the OrderItemStatus. Although the OrderItem is identified by the ProductId and Quantity, it might be smarter to model it as an Entity.

OrderItem.java Layer:Domain Package:domain.aggregate Type: Domain Entity

package com.ecommerce.hexagon.order_management.domain.aggregate;

import com.ecommerce.hexagon.common.domain.DomainEntity;
import com.ecommerce.hexagon.common.status.OrderItemStatus;
import com.ecommerce.hexagon.common.valueobjects.ID;
import com.ecommerce.hexagon.common.valueobjects.Money;
import com.ecommerce.hexagon.common.valueobjects.Quantity;
import lombok.Getter;

import java.math.BigDecimal;


@Getter
public class OrderItem extends DomainEntity {

    private ProductId product;

    private Quantity orderItemQuantity;

    private Money orderItemCost;

    private OrderItemStatus orderItemStatus;


    public void setOrderItemStatus(OrderItemStatus orderItemStatus) {
        this.orderItemStatus = orderItemStatus;
    }


    public OrderItem(ProductId product, Quantity quantity) {
        super(ID.generateRandom());
        this.product = product;
        this.orderItemQuantity = quantity;
        orderItemCost = new Money(BigDecimal.ZERO);
        calculateOrderItemCost();
    }


    protected void setQuantity(Quantity quantity) {
        this.orderItemQuantity = quantity;
        calculateOrderItemCost();
        }

    private void calculateOrderItemCost() {
        this.orderItemCost.value = product.getPrice().value.multiply(BigDecimal.valueOf(this.orderItemQuantity.value));
    }

}


DomainEntity.java Layer:Common Package:common.domain Type: Abstract class for Domain Entities

package com.ecommerce.hexagon.common.domain;

import com.ecommerce.hexagon.common.valueobjects.ID;

public abstract class DomainEntity {

    ID id;


    public DomainEntity(ID id) {
        this.id = id;
    }

}

OrderItemStatus.java Layer:Domain Package:/domain.status Type: State Status

package com.ecommerce.hexagon.order_management.domain.status;

public  enum OrderItemStatus {
    PENDING(1),
    ERROR_RESERVE_STOCK_FAILED(2),
    ERROR_DELIVERY_FAILED(3),
    COMPLETED(4),
    ITEM_RETURNED(5);



    OrderItemStatus(int code){
        this.code = code;
    }

    private final int code;


    public int getCode() {
        return code;
    }


    public static OrderItemStatus fromCode(int code) throws IllegalArgumentException {
        for (OrderItemStatus curstate : OrderItemStatus.values()) {
            if (curstate.code==code) {
                return curstate;
            }
        }
        throw new IllegalArgumentException("Invalid OrderItemStatus value: " + code);
    }


}

It is time to take a break — order a pizza and find the cat —before we move even further and identify the main methods of our Aggregate using Behaviour-Driven Development (BDD)

Step 5:Identifying Aggregate Lifecycle Handling Methods using BDD.

Behavior-Driven Development (BDD) is an approach that encourages collaboration between developers, testers, and non-technical stakeholders to define the behavior of a system through examples.

The good thing about BDD is that enables us to describe the expected behavior of our system through human-readable scenarios.

These scenarios serve as real-life examples of how users interact with our application, and lead us to the identification of Subdomain methods often categorized as commands and queries.

In part 1 we added a few — obvious for e-commerce methods without much thinking — to our OrderUIInboundPort of our Framework Layer .

OrderUIInboundPort.java

package com.ecommerce.hexagon.order_management.framework.ports_inbound.web;


import com.ecommerce.hexagon.order_management.framework.ports_inbound.web.inboundobjects.OrderRequest;
import com.ecommerce.hexagon.order_management.framework.ports_inbound.web.inboundobjects.ProductRequest;
import com.ecommerce.hexagon.order_management.framework.ports_inbound.web.inboundobjects.CheckoutRequest;
import com.ecommerce.hexagon.common.exceptions.BusinessException;
import com.ecommerce.hexagon.common.exceptions.CheckoutException;
import com.ecommerce.hexagon.common.exceptions.NotFoundException;
import jakarta.ws.rs.core.Response;



public interface OrderUIInboundPort {
    Response getOrder(String orderId) throws NotFoundException, BusinessException;
    Response createOrder(OrderRequest orderRequest) throws BusinessException, NotFoundException;
    Response addProductToOrder(String orderId, ProductRequest productRequest) throws NotFoundException, BusinessException;
    Response removeProductFromOrder(String orderId, ProductRequest productRequest) throws NotFoundException, BusinessException;
    Response checkout(String orderId, CheckoutRequest checkoutRequest) throws NotFoundException, BusinessException, CheckoutException;
    Response clearOrder(String orderId) throws NotFoundException, BusinessException;

}

Essentially those methods will be implemented by an Inbound Web Adapter — the OrderUIInboundAdapter which we show in part 1 — which in turn will invoke a Service Port of the Application Layer — and that Service Port is implemented by a UseCase or a Service — which in turn will orchestrate and issue commands and queries to which alter the state of the Aggregate Root as well as invoking other Ports of the Framework Layer. (Well it is not that hard. DI Magic.)

5.1:Identity Commands & Queries using (Gherkin syntax)

As we are trying to find the commands, we look at actions users might take that lead to a change in the state of our aggregates, such as adding or removing items from an order. For queries, we focus on the information users might seek without altering the state directly. The general rule of thumb is

Commands alter the state of the Aggregate. Queries don’t

Let’s derive some scenarios for our Order Subdomain using (Gherkin syntax).

ALTHOUGH BDD IS MOST SUITABLE AT THE APPLICATION LAYER SINCE WE ARE ACCEPTING THE FACT THAT OUR ORDERSERVICE IS ANEMIC THESE SMALL SCENARIOS MATCH PERFECTLY WITH THE ORDER AGGREGATE METHODS

Scenario 1: Add Product to Order

Given an existing order When a user adds a product with specific details Then the order should be updated with the new product, and the total cost should reflect the addition

Command: addProduct(ID productId, Money price, Quantity quantity)

Scenario 1 Implementation

Lets add the addProduct method to the OrderAggregate class

   //External Method Invoked by The Application Hexagon Service or a Domain Service
    public void addProduct(ID productId, Money price, Quantity quantity) throws BusinessException {

        //Create an Aggregate related value object
        ProductId product = new ProductId(productId,  price);

        // Check invariants before processing the command
        validateDefaultInvariants();

        //Create an Order  Item and add it to the List
        orderItems.add( new OrderItem(product, quantity));
      
        //Calculate Total Price using Addition
        orderTotalCost.value = orderTotalCost.value.add(product.getPrice().value.multiply(BigDecimal.valueOf(quantity.value)));

        // Check again default invariants after processing the command
        validateDefaultInvariants();

   
    }

A very simple method. Create the ProductId Value Object — we might use a Factory Pattern such as ProductFactory but it still does the job — then create an OrderItem Entity with this ProductId object and the requested quantity and price.

Finally we add the OrderItem to the internal list orderItems.Also we calculate the total cost (every time just add the new cost to the exisitng price, you will see later on the invariants section why we are doing this dumb way and not just using streams on the list of orderItems to find the total cost.)

Scenario 2: Remove Product to Order

Given an existing order with certain products When a user removes a specific product Then the order should be updated without the removed product, and the total cost should reflect the subtraction

Command: removeProduct(ID productId)

Scenario 2 Implementation

Add the removeProduct method to the OrderAggregate class

    public void removeProduct(ID productId) throws BusinessException {

        //Create a Domain related value object.
         ProductId product = new ProductId(productId,null);

        // Check invariants before processing the command
        validateDefaultInvariants();

        // Check if the product is already in the order
        OrderItem existingItem = findOrderItemByProduct(product);

        // Remove order item from the list
        orderItems.remove(existingItem);

        //Calculate Price - Subtraction
        orderTotalCost.value = orderTotalCost.value.subtract(existingItem.getProduct().getPrice().value.multiply(BigDecimal.valueOf(existingItem.getOrderItemQuantity().value)));

        // Check invariants after processing the command
        validateDefaultInvariants();

    }

These scenarios represent the behavior or commands that the Order Aggregate is expected to handle.If you noticed BDD also helped us not only to identify the commands as method signatures but gave us behavioral tips within each method. I’m not going to write more scenarios such as update order status, update product quantity and checkout order. But you get the point.

Our OrderAggregate after identifying the main commands using BDD becomes.

OrderAggregate.java

package com.ecommerce.hexagon.order_management.domain.aggregate;

import com.ecommerce.hexagon.common.domain.DomainEntity;
import com.ecommerce.hexagon.common.domain.IAggregateRoot;
import com.ecommerce.hexagon.order_management.domain.status.OrderStatus;
import com.ecommerce.hexagon.common.valueobjects.*;
import com.ecommerce.hexagon.common.exceptions.BusinessException;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;


import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;


public  class OrderAggregate implements IAggregateRoot {

    public ID orderId;

    private CustomerId customer;

    private Description orderFriendlyName;

    private OrderStatus orderStatus;

    private Money orderTotalCost;

    private List<OrderItem> orderItems = new ArrayList<>();

    private Checkout checkout;


    @JsonIgnore
    public ID getId() {
        return orderId;
    }


    // create an Immutable OrderAggregate
    @Override
    public OrderAggregateRecord getImmutable() {
        OrderAggregateRecord immutableRecord = new OrderAggregateRecord(orderId, customer,Collections.unmodifiableList(orderItems),orderStatus,orderTotalCost);
        return  immutableRecord;
    }

    @Override
    // Validate Invariants  
    public  void validateDefaultInvariants() throws BusinessException {
       
    }


 //---------------------External Methods------------------------------------
 
public void addProduct(ID productId, Money price, Quantity quantity) throws BusinessException {

        //Create a Domain related value object
        ProductId product = new ProductId(productId,  price);

        // Check invariants before processing the command
        validateDefaultInvariants();

        //Add Item
        orderItems.add( new OrderItem(product, quantity));
    
        //Calculate Price- Addition
        orderTotalCost.value = orderTotalCost.value.add(product.getPrice().value.multiply(BigDecimal.valueOf(quantity.value)));

        // Check invariants after processing the command
        validateDefaultInvariants();

    }

    public void removeProduct(ID productId) throws BusinessException {

        //Create a Domain related value object.
         ProductId product = new ProductId(productId,null);

        // Check invariants before processing the command
        validateDefaultInvariants();

        // Check if the product is already in the order
        OrderItem existingItem = findOrderItemByProduct(product);

        // Remove order item from the list
        orderItems.remove(existingItem);

        //Calculate Price - Subtraction
        orderTotalCost.value = orderTotalCost.value.subtract(existingItem.getProduct().getPrice().value.multiply(BigDecimal.valueOf(existingItem.getOrderItemQuantity().value)));

        // Check invariants after processing the command
        validateDefaultInvariants();
  }


    public void updateOrderStatus(OrderStatus newStatus)  {
        orderStatus = newStatus;
   }

    public void updateProductQuantity(ID productId,  Quantity newQuantity) throws BusinessException {

        //Create a Domain related value object. This time we
        ProductId product = new ProductId(productId,null);

        // Check invariants before processing the command
        validateDefaultInvariants();

        // Check if the product is already in the order
        OrderItem existingItem = findOrderItemByProduct(product);

       // Update quantity if product already exists in the order
        existingItem.setQuantity(newQuantity);

        //Calculate Order Total Cost  - Subtraction existing Product Cost * quantity + Addition New Product Cost * quantity
        orderTotalCost.value = orderTotalCost.value.subtract(existingItem.getProduct().getPrice().value.multiply(BigDecimal.valueOf(existingItem.getOrderItemQuantity().value)));
        orderTotalCost.value = orderTotalCost.value.add(product.getPrice().value.multiply(BigDecimal.valueOf(newQuantity.value)));

        // Check invariants after processing the command
        validateDefaultInvariants();
   }



    public void setCheckout(ID customerId, Contact contact, NameSurname shippingNameSurname, Payment payment, Address shippingAddress) {
        this.customer = new CustomerId(customerId);
        this.checkout = new Checkout(contact,shippingNameSurname,payment,shippingAddress);
    }

    private OrderItem findOrderItemByProduct(ProductId productId) {
        return orderItems.stream()
                .filter(orderItem -> orderItem.getProduct().equals(productId))
                .findFirst()
                .orElse(null);
    }


}

Step 6:Implement Business Invariants using the Specification Pattern.

6.1:What Is The Specification Pattern

According to the Specification Paper of Evans and Fowler (n.d. ), the specification pattern is a software design approach used to bundle business rules that describe how an object should be. It’s a powerful method to reduce connections between our OrderAggregate methods and and enhance the ability to mix and max specific rules based on certain conditions.

Let’s identify some of the common invariants of our OrderAggregate:

OrderAggregate Invariants

The orderTotalCost should always equal the sum of the costs of individual order items.

The quantity of each order item should be non-negative.

The order status should be a valid status according to the business rules (e.g., pending, processing, completed).

Each product in the order items should exist in the system.

When updating the quantity of an existing product in the order, the total cost should be updated accordingly.

Each product should appear only once in the order items list. If the order has been checked out, the checkout information should be complete.

When removing a product from the order, the total cost should be adjusted accordingly.

6.2:Implement the Specifications

Now let’s see how we can implement specifications for two of the above invariants: “Total Equals Sum of Order Line Items” and “Non-Negative Quantities.”

First we need to create an interface ISpecification<T> to represent a specification within the common package

ISpecification.java Layer:Common Package:common.domain Type: Specification Interface

package com.ecommerce.hexagon.common.domain;


public  interface ISpecification<T> {

    boolean isSatisfiedBy(T candidate);

    String getDescription();

}

We can now proceed and create our first specification within the domain layer in a new package named specification.

TotalEqualsSumOfOrderItemsSpecification.java Layer:Domain Package:/domain.specification Type: Specification Implementation

package com.ecommerce.hexagon.order_management.domain.specification;

import com.ecommerce.hexagon.common.domain.ISpecification;
import com.ecommerce.hexagon.order_management.domain.aggregate.OrderAggregate;

import java.math.BigDecimal;

public class TotalEqualsSumOfOrderItemsSpecification implements ISpecification<OrderAggregate> {


    @Override
    public boolean isSatisfiedBy(OrderAggregate order) {
        BigDecimal calculatedSum = order.getOrderItems().stream()
                .map(item -> item.getOrderItemCost().getValue())
                .reduce(BigDecimal.ZERO, BigDecimal::add);

        return order.getOrderTotalCost().equals(calculatedSum);
    }



    @Override
    public String getDescription() {
        return "Spec:Total Cost Equals Sum of Order Line Items";
    }
}

Let’s build the second specification about non-negative quantities.

NonNegativeQuantitiesSpecification.java Layer:Domain Package:/domain.specification Type: Specification Implementation

package com.ecommerce.hexagon.order_management.domain.specification;

import com.ecommerce.hexagon.common.domain.ISpecification;
import com.ecommerce.hexagon.order_management.domain.aggregate.OrderAggregate;

public class NonNegativeQuantitiesSpecification implements ISpecification<OrderAggregate> {

    @Override
    public boolean isSatisfiedBy(OrderAggregate order) {
        return order.getOrderItems().stream().allMatch(item -> item.getOrderItemQuantity().getValue() >= 0);
    }

    @Override
    public String getDescription() {
        return "Quantities of all order items should be non-negative.";
    }
}

6.3: Applying Specifications in OrderAggregate

Now, we can apply these specifications to our OrderAggregate class. We code the validateDefaultInvariants method, and we create instances of the specifications within.

public class OrderAggregate implements IAggregateRoot {

    // Existing code...

    public void validateDefaultInvariants() throws BusinessException {
        ISpecification<OrderAggregate> totalEqualsSumSpec = new TotalEqualsSumOfOrderItemsSpecification();
        ISpecification<OrderAggregate> nonNegativeQuantitiesSpec = new NonNegativeQuantitiesSpecification();

        validateCustomInvariants(totalEqualsSumSpec, nonNegativeQuantitiesSpec);
    }

    private void validateCustomInvariants(ISpecification<OrderAggregate>... specifications) throws BusinessException {
        for (ISpecification<OrderAggregate> specification : specifications) {
            if (!specification.isSatisfiedBy(this)) {
                System.out.println("Custom invariant violated: " + specification.getDescription());
                throw new BusinessException("Custom invariant violated: " + specification.getDescription());
            }
        }
    }

    // Existing code...
}

We’ve introduced another method named validateCustomInvariants, designed to receive a list of specifications. This method iterates through the provided specifications, invoking the isSatisfiedBy method for each. If any specification is not satisfied, it raises a BusinessException. The purpose behind validateCustomInvariants is to offer flexibility in scenarios where we need to validate both the default invariants and specific custom invariants simultaneously.

WE FINISHED IMPLEMENTING THIS LAYER FOR NOW. LET’S SEE THE PACKAGE STRUCTURE OF THe COMMON LAYER AND THE DOMAIN LAYER.

Fig 3. Our ‘Domain’ and ‘Common’ code-base.

Step 7:The Apllication Layer. Glue them all!

It is FINALLY the time to conect the Application Hexagon with the Franework and the Domain Hexagon. (Maybe glue is not the right word since we are 'loose-coupling' to the extreme!)

In this phase, we transition to the Application Layer, where we introduce the OrderManagementService and the corresponding OrderManagementInboundPort.

This port plays a pivotal role in invoking the methods of the OrderAggregate that we recently implemented. It is essentially the Connection between the Framework Layer and the Domain Layer.

7.1 Implement The OrderManagementInboundPort

OrderManagementInboundPort.java Layer:Application Package:\application.ports_inbound Type: Inbound (Driver) Port

package com.ecommerce.hexagon.order_management.application.ports_inbound;

import com.ecommerce.hexagon.common.exceptions.BusinessException;
import com.ecommerce.hexagon.common.exceptions.NotFoundException;
import com.ecommerce.hexagon.common.valueobjects.ID;
import com.ecommerce.hexagon.common.valueobjects.Money;
import com.ecommerce.hexagon.common.valueobjects.Quantity;
import com.ecommerce.hexagon.order_management.framework.ports_inbound.web.inboundobjects.ProductRequest;
import com.ecommerce.hexagon.order_management.domain.aggregate.OrderAggregateRecord;
import jakarta.transaction.Transactional;

import java.util.List;

public interface OrderManagementInboundPort {


    @Transactional
    public ID createOrder(ID customerId, List<ProductRequest> productsList)  throws NotFoundException, BusinessException;

    @Transactional
    public OrderAggregateRecord addProductToOrder(ID orderId, ID productId, Money price, Quantity quantity)  throws NotFoundException, BusinessException;

    @Transactional
    public OrderAggregateRecord removeProductFromOrder(ID orderId, ID productId) throws NotFoundException, BusinessException;

    @Transactional
    public OrderAggregateRecord clearOrder(ID orderId) throws NotFoundException, BusinessException;

    @Transactional
    OrderAggregateRecord getOrder(ID orderId) throws NotFoundException, BusinessException;
}

7.1: Implement the OrderManagementService.

OrderManagementService.java Layer:Application Package:application.services Type: Service

package com.ecommerce.hexagon.order_management.application.services;

import com.ecommerce.hexagon.common.exceptions.BusinessException;
import com.ecommerce.hexagon.common.exceptions.NotFoundException;
import com.ecommerce.hexagon.common.framework.ports_outbound.persistence.PersistenceOutboundPort;
import com.ecommerce.hexagon.common.valueobjects.ID;
import com.ecommerce.hexagon.common.valueobjects.Money;
import com.ecommerce.hexagon.common.valueobjects.Quantity;
import com.ecommerce.hexagon.order_management.application.ports_inbound.OrderManagementInboundPort;
import com.ecommerce.hexagon.order_management.framework.ports_inbound.web.inboundobjects.ProductRequest;
import com.ecommerce.hexagon.order_management.domain.aggregate.CustomerId;
import com.ecommerce.hexagon.order_management.domain.aggregate.OrderAggregate;
import com.ecommerce.hexagon.order_management.domain.aggregate.OrderAggregateRecord;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.transaction.Transactional;

import java.util.List;


@ApplicationScoped
public class OrderManagementService implements OrderManagementInboundPort {

    @Inject
    private PersistenceOutboundPort<OrderAggregate,ID> persistenceOutboundAdapter;



    @Transactional
    @Override
    public ID createOrder(ID customerId, List<ProductRequest> productsList)  throws NotFoundException, BusinessException {


            OrderAggregate order = OrderAggregate.build(new CustomerId(customerId));
            ID newOrderId = order.getId();

            for (ProductRequest product : productsList) {
                order.addProduct(product.productId(), product.price(), product.quantity());
            }

            // Use the orderPersistenceOutboundPort to persist the state  in local the database
            persistenceOutboundAdapter.persist(order);

            // Fetch from the database and return the  order  record object back to client
            order = persistenceOutboundAdapter.getById(newOrderId);
            return order.getId();


    }


    @Transactional
    @Override
    public OrderAggregateRecord addProductToOrder(ID orderId, ID productId, Money price, Quantity quantity)  throws NotFoundException, BusinessException {

            OrderAggregate order = persistenceOutboundAdapter.getById(orderId);
            order.addProduct(productId, price, quantity);

            // Use the orderPersistenceOutboundPort to persist the state  in local the database
            persistenceOutboundAdapter.persist(order);

            // Fetch from the database and return the  order  record object back to client
            order = persistenceOutboundAdapter.getById(order.getId());
            return order.getImmutable();
    }

    @Transactional
    @Override
    public OrderAggregateRecord removeProductFromOrder(ID orderId, ID productId) throws NotFoundException, BusinessException {

        OrderAggregate order = persistenceOutboundAdapter.getById(orderId);
        order.removeProduct(productId);

        // Use the orderPersistenceOutboundPort to persist the state  in local the database
        persistenceOutboundAdapter.persist(order);

        // Fetch from the database and return the  order  record object back to client
        order = persistenceOutboundAdapter.getById(order.getId());
        return order.getImmutable();
    }

    @Transactional
    @Override
    public OrderAggregateRecord clearOrder(ID orderId) throws NotFoundException, BusinessException {

        OrderAggregate order = persistenceOutboundAdapter.getById(orderId);
        order.clearOrder();

        // Use the orderPersistenceOutboundPort to persist the state  in local the database
        persistenceOutboundAdapter.persist(order);

        // Fetch from the database and return the  order  record object back to client
        order = persistenceOutboundAdapter.getById(order.getId());
        return order.getImmutable();
    }


    @Transactional
    @Override
    public OrderAggregateRecord getOrder(ID orderId) throws NotFoundException, BusinessException {

        OrderAggregate order = persistenceOutboundAdapter.getById(orderId);
        return order.getImmutable();
    }

}


 

   

Our OrderManagementService class maintains a straightforward design. You might describe it as somewhat ANEMIC. All the business logic happens within the Domain Hexagon.

The service receives parameters from the Framework layer’s WEB UI Adapter, retrieves the OrderAggregate from the local database of the microservice using aPersistenceOutboundPort and proceeds with the commands to alter the state of the OrderAggregate. Then it saves the state of the OrderAggregate to the local database using the PersistenceOutboundPort .

Since each microservice in our domain has it’s own database according to the ‘Database per Service Pattern’, we make our PersistenceOutboundPort abstract and generic to be shared by all microservices within our common package.

7.2: Implement a Common Abstract Generic Persistence Port.

Let’s create a common abstract generic class for the outbound port for local db persistency — PersistenceOutboundPort — and connect our service to the Framework Layer.

PersistenceOutboundPort.java Layer:Application Package:common.application.ports_outbound.persistence Type: Outbound (Driven) Port

package com.ecommerce.hexagon.common.application.ports_outbound.persistence;
import com.ecommerce.hexagon.common.exceptions.NotFoundException;


public interface PersistenceOutboundPort<T,K> {
    void persist(T entity);
    T getById(K key) throws NotFoundException;
    void delete(K key);
}

Finally let’s add the NotFoundException class

NotFoundException.java Layer:Common Package:common.exceptions Type: Exception

package com.ecommerce.hexagon.common.exceptions;

public class NotFoundException extends Throwable {
    public NotFoundException(String s) {
    }
}

Step 8:The Framework Layer. Finalize our WebUI Adapter (AKA Controller).

8.1: Connect Framework’s Adapter with the Application Layer.

In this step we finalize our WebUI Adapter OrderUIInboundAdapter of the Framework Layer from part 1. The Web Adapter facilitates the communication between users and the system by accepting Data Transfer Objects (DTOs) from users and leveraging the OrderManagementService to execute the corresponding actions.

We enable our OrderUIInboundAdapter to leverage the OrderManagementInboundPort.

OrderUIInboundAdapter.java Layer:Framework Package:adapters_inbound.web Type: Inbound Web Adapter

package com.ecommerce.hexagon.order_management.framework.adapters_inbound.web;

import com.ecommerce.hexagon.common.exceptions.BusinessException;
import com.ecommerce.hexagon.common.exceptions.CheckoutException;
import com.ecommerce.hexagon.common.exceptions.NotFoundException;
import com.ecommerce.hexagon.common.valueobjects.ID;
import com.ecommerce.hexagon.order_management.application.ports_inbound.OrderManagementInboundPort;
import com.ecommerce.hexagon.order_management.framework.ports_inbound.web.OrderUIInboundPort;
import com.ecommerce.hexagon.order_management.framework.ports_inbound.web.inboundobjects.CheckoutRequest;
import com.ecommerce.hexagon.order_management.framework.ports_inbound.web.inboundobjects.OrderRequest;
import com.ecommerce.hexagon.order_management.framework.ports_inbound.web.inboundobjects.ProductRequest;
import com.ecommerce.hexagon.order_management.domain.aggregate.OrderAggregateRecord;
import jakarta.inject.Inject;
import jakarta.ws.rs.*;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;

import java.net.URI;


@Path("order-service/hexagon/v1/order")
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)

public class OrderUIInboundAdapter  implements OrderUIInboundPort {


    @Inject
    private OrderManagementInboundPort applicationAdapter; //Application Port for Managing Order Commands  and Queries


    @GET
    @Path("/{orderId}")
    public Response getOrder(@PathParam("orderId") String orderId) throws NotFoundException, BusinessException {

           OrderAggregateRecord order  = applicationAdapter.getOrder(ID.generateFromString(orderId));
            return Response.ok(order).build();


    }


    @POST
    @Override
    public Response createOrder(OrderRequest orderRequest) throws BusinessException, NotFoundException {

             ID newOrderId = applicationAdapter.createOrder(orderRequest.customerId(),orderRequest.products());


             return Response.created(URI.create("/" + newOrderId.getUuid().toString()))
                    .entity(newOrderId)
                    .build();
    }


    @PUT
    @Path("/{orderId}/addProduct")
    @Override
    public Response addProductToOrder(@PathParam("orderId") String orderId, ProductRequest productRequest) throws NotFoundException, BusinessException {

            OrderAggregateRecord order = applicationAdapter.addProductToOrder(ID.generateFromString(orderId), productRequest.productId(), productRequest.price(), productRequest.quantity());


             return Response.ok(URI.create("/" + order.id().getUuid().toString()))
                .entity(order)
                .build();
    }

    @DELETE
    @Path("/{orderId}/removeProduct")
    @Override
    public Response removeProductFromOrder(@PathParam("orderId") String orderId, ProductRequest productRequest) throws NotFoundException, BusinessException {
            OrderAggregateRecord order = applicationAdapter.removeProductFromOrder(ID.generateFromString(orderId), productRequest.productId());

            return Response.ok(URI.create("/" + order.id().getUuid().toString()))
                    .entity(order)
                    .build();

    }



    @GET
    @Override
    @Path("/{orderId}/clear")
    public Response clearOrder(String orderId) throws NotFoundException, BusinessException {
        applicationAdapter.clearOrder(ID.generateFromString(orderId));
        return Response.ok(URI.create("/cleared")).build();

    }

    @GET
    @Override
    @Path("/{orderId}/checkout")
    public Response checkout(String orderId, CheckoutRequest checkoutRequest) throws NotFoundException, BusinessException, CheckoutException {

        return null;
    }



}


Epiloque

That’s all for this Part guys.

At first glance, the hexagon architecture may appear complex and demanding to set up, especially with the Domain Hexagon and the Aggregate. Naming and Package Structure are also a big problem.

However, as demonstrated here, it’s surprisingly straightforward to establish the foundation and get things connected. Additionally, I hope you can recognize the value of this approach in achieving loose coupling between layers, which can significantly simplify your work.

Let’s also summarize a few things, about our Aggregate.

In the Domain-Driven Design (DDD) Hexagonal approach, we moved important functions from the Service Layer of the traditional three layer architecture to the Domain hexagon within the Aggegate Root.

The key system operations, once housed in the service layer, are now integrated into the OrderAggregate domain entity class within the Domain hexagon.

The main distinction lies in the fact that the Domain hexagon operates independently, not relying on any external dependencies. This differs from the layered architecture approach, where the Service layer, containing essential system logic, is dependent on the data layer.

In part 3 we continue our ‘Hexagonal’ journey and dive into a new small group of the Framework called the ‘Infastructure’ and tackle the peristency mechanism as well as investigating some interesting state management techniques.

If you enjoyed the article, express your appreciation with claps: 0 for poor quality, under 10 if you respectfully disagree, 10+ if you agree, 20–30 for mind-blowing, and 40-50 for exceptionally unique content. Your claps reflect your feedback and enrich our community.

Thank you!

Bibliography

Evans, E. (2003). Domain-driven design : tackling complexity in the heart of software. Addison-Wesley.

Evans, E., & Fowler, M. (n.d.). Specifications. Retrieved March 22, 2023, from https://www.martinfowler.com/apsupp/spec.pdf

Richardson, C. (n.d.). Pattern: Decompose by Subdomain Context. Microservices.io; Chris Richardson. Retrieved October 28, 2023, from https://microservices.io/patterns/decomposition/decompose-by-subdomain.html

Take a look at the other parts of this article series as well as some theory of Domain Driven Design.

PART 1: Quarkus. Microservices & DDD Hexagonal Architecture. How to Build your Hexagon Layers. The ‘Framework Hexagon’.

Microservice Architecture & Domain Driven Design. Aggregates and ‘Decompose by Subdomain’ Pattern

Domain Driven Design
Hexagonal Architecture
Java
Quarkus Framework
Microservices
Recommended from ReadMedium