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.
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.
Architect's Journey
E‑commerce, SaaS, AI architect; DDD enthusiast; SKILL enthusiast
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
