Reshaping a Microservice with DDD: A Practical Layered Architecture Guide
This article shares the author's experience of redesigning a microservice using Domain‑Driven Design, detailing a standardized layered architecture, directory structure, model conversions, common pitfalls, and practical recommendations to improve code clarity and testing efficiency.
Background
In 2021 a microservice project attempted to adopt Domain‑Driven Design (DDD) but suffered from inconsistent team understanding, incomplete conventions, and a focus on rapid implementation. To provide a concrete reference, one microservice was rebuilt with a strict DDD layered architecture and used as a teaching example.
Layered Architecture
The architecture follows the Dependency Inversion Principle: higher‑level modules depend on abstractions, while lower‑level infrastructure implements those abstractions. Five core layers are defined.
Presentation (User Interface) Layer : adapts services for front‑end consumption, handles RESTful requests, parses configuration, and assembles data for the application layer.
Application Layer : composes and orchestrates services to satisfy rapidly changing business processes; contains application services, command/query handlers, and event‑related code.
Domain Layer : implements core business logic; includes aggregates, entities, value objects, domain services, and domain events that together form business capabilities.
Infrastructure Layer : provides technical foundations for all other layers; hosts third‑party integrations, database persistence, and generic utilities.
Common Layer : stores shared code such as exception definitions, enums, and other cross‑cutting concerns.
Code Structure
Top‑level Packages
presentation
application
domain
infrastructure
commonEach top‑level package contains sub‑packages with specific responsibilities.
presentation : assembler, vo, facade – converts domain data to view objects and exposes coarse‑grained controller interfaces.
application : assembler, dto, event ( publish / subscribe), service, task – handles command/query processing, object conversion, event publishing, and scheduled tasks.
domain : organized by aggregates; each aggregate contains shared, model, entity, event, repository, service, util sub‑folders.
infrastructure : config, persistence, entity, converter, repository, integration – implements persistence adapters, configuration, and external system clients.
common : shared utilities, exception hierarchy, enums, etc.
Domain Model Organization
Each aggregate follows a clear boundary:
shared : abstract interfaces and base classes used across aggregates.
model : aggregate root, entities, value objects, and factories (rich domain model).
event : domain events emitted by the aggregate.
repository : interface defining persistence operations for the aggregate; a single repository per aggregate.
service : domain services that encapsulate business logic spanning multiple entities.
util : helper classes specific to the aggregate.
Data Object Types
PO (Persistent Object) : one‑to‑one mapping with database tables; used only in the infrastructure persistence layer.
DO (Domain Object) : runtime entities that carry core business state inside the domain layer.
DTO (Data Transfer Object) : carries data between the presentation layer, application layer, or across microservices.
VO (View Object) : packages data for UI components or API responses; typically built from DTOs by the presentation layer.
Observed Challenges
Excessive object conversion : PO → DO → DTO → VO introduces boilerplate and performance overhead.
Domain logic leakage : Business logic is often placed in application services instead of the domain layer, blurring responsibilities.
ORM complexity : Mixing multiple ORM frameworks (e.g., JPA, MyBatis) without clear guidelines leads to confusing data‑access code.
Unclear external client placement : Integration code is scattered between application and infrastructure layers.
Query performance & coding efficiency : Simple read‑only queries may require traversing four layers, raising doubts about DDD practicality.
Practical Recommendation for Simple Read‑Only Operations
Flow: Controller → Application Service (Query Executor) → Infrastructure (MyBatis Mapper) Input: Command object → MyBatis ExampleDO Output: DataObject → DTO (merged DetailVO and Application DTO; no separate VO)
Architectural Enforcement
The project introduces an archunit dependency to enforce layer boundaries, ensuring that higher layers never depend on lower implementation details.
Reference Diagram
Eric Tech Circle
Backend team lead & architect with 10+ years experience, full‑stack engineer, sharing insights and solo development practice.
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.
