Deep Dive into DDD Rich Model: Best Practices

The article explains the DDD rich (or active) model, its core characteristics, compares it with the anemic model, and shows how to apply it in a shared‑rental billing domain using Java, DDD layers, domain events, and a lightweight framework.

Architect's Journey
Architect's Journey
Architect's Journey
Deep Dive into DDD Rich Model: Best Practices

What Is the Rich Model?

The rich model is an object‑oriented design approach that belongs to Domain‑Driven Design (DDD). It places both data and the related business logic inside the same domain object, so an order object, for example, holds fields such as order number and customer ID as well as methods to calculate total price or check inventory.

Core Characteristics

Encapsulate Data and Behavior : Domain objects (entities or value objects) contain attributes and the operations that act on those attributes, e.g., an order object includes methods for price calculation and stock verification.

High Cohesion, Low Coupling : By keeping business logic inside the domain object, inter‑object coupling is reduced, which improves maintainability and extensibility.

Autonomous Domain Logic : Objects manage their own state and behavior, decreasing reliance on external service layers.

Application Scenarios

The rich model suits systems with complex business rules that need strong encapsulation, such as e‑commerce platforms where an order object should contain pricing and inventory checks rather than delegating them to a separate service.

Comparison with the Anemic Model

Anemic Model : Data and behavior are separated; domain objects only hold fields, while services contain all business logic. This simplicity can lead to bulky service layers when logic becomes complex.

Rich Model : Data and behavior are combined in the same object, adhering to OO principles and making the code easier to extend and maintain.

Developers often encounter massive Service classes with thousands of lines of code, making it hard to locate logic and risky to modify.

Billing Rental Domain Modeling

The billing‑rental aggregate includes the following core steps:

Create a billing template in the back‑office.

Deploy a device by selecting the device, location (store), and billing template.

When a C‑end user places an order, the “pay‑first‑use‑later” flow requires a deposit payment for devices with sub‑devices (e.g., power banks, swap cabinets).

Send commands to the hardware to start the service (e.g., unlock a power bank, start water dispensing).

After usage, settle the billing order and deduct fees.

The fourth step—sending different commands based on varying strategies—is highlighted as a tricky part.

DDD Implementation in Shared Rental

The project follows the classic four‑layer DDD architecture. The application.factory package is replaced by a listener package that holds event listeners for decoupling.

The core classes involved in the billing‑rental business are illustrated below:

Using the Rich Model in the Billing Order Service

The author shows a billing order service with about 620 lines of code, but most business logic is extracted into rich model methods that are either static or instance methods of the domain model. The extraction follows two simple principles: atomicity and reusability.

Operations such as placing an order, handling payment callbacks, settlement, and scheduled tasks remain in the application service (anemic style) because they do not fit well into rich model methods.

When a method needs to depend on other Spring beans, the author notes that the D3Boot framework eliminates the need for @Autowired fields; queries and saves become one‑line calls, and domain events can be published directly from the method.

Domain Event Decoupling

The listener package acts as the entry point for domain or external MQ events, providing decoupling in the billing‑rental context.

Instead of creating many if‑else branches or separate APIs for different product billing flows, the system publishes a domain event after an order is submitted. Each tenant can listen to the event and decide whether to handle it synchronously or asynchronously.

For example, after a water‑purifier order is placed, a listener sends a command to the device to start dispensing water. The same logic supports multiple manufacturers with a single code path.

Final Thoughts

The rich model is not a universal solution; the author’s current business is relatively simple and does not yet handle transaction boundaries within static rich methods. When transactions are needed, explicit transaction code must be added.

Iterative development, small incremental steps, and visible results are emphasized as the best way to evolve architecture.

Javabackend architectureDomain-Driven DesignDDDDomain EventsRich Model
Architect's Journey
Written by

Architect's Journey

E‑commerce, SaaS, AI architect; DDD enthusiast; SKILL enthusiast

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.