Practical Guide to Applying Domain‑Driven Design in Spring Boot Microservices

This guide explains Domain‑Driven Design (DDD) and shows how to structure Spring Boot microservices by organizing code around business domains, detailing strategic and tactical concepts, package layouts, layer responsibilities, cross‑domain communication, comparisons with traditional layering, evolution steps, and common FAQs.

The Dominant Programmer
The Dominant Programmer
The Dominant Programmer
Practical Guide to Applying Domain‑Driven Design in Spring Boot Microservices

What is DDD?

Domain‑Driven Design (DDD) was introduced by Eric Evans in 2003. Its core idea is to organize code around the business domain so that the code structure directly reflects the business structure.

Traditional layered architecture groups classes by technical concerns (controller, service, dao, entity). DDD groups them by domain, for example:

src/
├── domain/
│   ├── order/   ← order domain (self‑contained)
│   │   ├── api/
│   │   ├── controller/
│   │   ├── service/
│   │   ├── dao/
│   │   ├── entity/
│   │   └── dto/
│   ├── user/    ← user domain
│   └── product/ ← product domain
└── feign/       ← cross‑domain / cross‑service calls

Core difference: traditional layering expects OrderController in the controller package, while DDD expects everything related to orders inside the order package.

Core Concepts

Strategic Design (macro level)

Domain : the business problem space (e.g., e‑commerce, finance, logistics).

Subdomain : a finer‑grained business area within a domain (order, inventory, payment, user).

Bounded Context : the code boundary of a subdomain, autonomous internally (often a separate module or microservice).

Ubiquitous Language : a shared set of business terms used by the whole team (e.g., “source” = “match warehouse according to rules”).

Context Map : relationships between bounded contexts (e.g., order domain calls inventory domain).

Tactical Design (code level)

Entity : a business object with a unique identifier, mutable. Annotated with @Entity / @TableName.

Value Object : immutable, identified by its attributes (e.g., Address, Money).

Aggregate : a cluster of related objects with a consistency boundary (order + order items).

Aggregate Root : the entry entity of an aggregate; external code interacts only through it (Order is the root, OrderItem accessed via Order).

Domain Service : business logic that does not belong to any entity. Annotated with @Service.

Repository : abstraction for persisting aggregates (typically a Mapper/DAO interface).

Domain Event : a message that records something that happened in the domain (e.g., Kafka/RocketMQ messages).

Application Service : orchestrates domain objects to fulfill use cases; called by controllers.

Relationships Between Bounded Contexts

ACL (Anti‑Corruption Layer) : isolates model differences of external systems (implemented with Feign + DTO conversion).

OHS (Open Host Service) : exposes capabilities via API (REST API).

PL (Published Language) : shared data format (common DTO/Proto).

SK (Shared Kernel) : two contexts share part of the model (common JAR).

C‑S (Customer‑Supplier) : upstream provides API, downstream consumes (Feign call).

DDD Layered Architecture

Standard Four‑Layer Architecture

┌─────────────────────────────────┐
│ Interface / API (REST, GraphQL, gRPC, message consumer) │
│   – receives request, validates, calls application layer │
└───────────────────────┬─────────────┘
            ↓
┌─────────────────────────────────┐
│ Application (Application Service, DTO conversion, transaction orchestration) │
│   – use‑case orchestration, calls domain layer, no business logic │
└───────────────────────┬─────────────┘
            ↓
┌─────────────────────────────────┐
│ Domain (Entity, Value Object, Domain Service, Event) │
│   – core business logic, independent of frameworks │
└───────────────────────┬─────────────┘
            ↓
┌─────────────────────────────────┐
│ Infrastructure (Repository implementation, Feign calls, message sending, cache) │
│   – technical details, external system integration │
└─────────────────────────────────┘

Simplified Practical Version for Spring Boot

┌──────────────────────────────────────┐
│ API layer (api/) – interface definitions + OpenAPI annotations │
└──────────────┬───────────────────────┘
               ↓
┌──────────────────────────────────────┐
│ Controller layer (controller/) – request handling, logging, exception capture │
└──────────────┬───────────────────────┘
               ↓
┌──────────────────────────────────────┐
│ Service layer (service/) – business logic orchestration (application + domain merged) │
└──────────────┬───────────────────────┘
               ↓
┌──────────────────────────────────────┐
│ DAO layer (dao/mybatis/) – data access (repository implementation) │
└──────────────────────────────────────┘

This keeps the DDD “by‑domain packaging” idea while omitting a strict four‑layer split, which suits CRUD‑heavy systems.

Package Structure Design

Full Domain Package Layout

com.example.myapp/
├── Application.java                     # startup class
├── common/                             # global shared
│   ├── exception/                      # exception definitions
│   └── util/                           # utilities
├── config/                             # global configuration
│   ├── DataSourceConfig.java
│   ├── RedisConfig.java
│   └── SecurityConfig.java
├── domain/                             # business domains
│   ├── order/   # order bounded context
│   │   ├── api/
│   │   ├── controller/
│   │   ├── service/
│   │   │   ├── OrderService.java      # interface
│   │   │   └── impl/OrderServiceImpl.java
│   │   ├── dao/mybatis/OrderMapper.java
│   │   ├── entity/Order.java, OrderItem.java
│   │   ├── dto/ (CreateOrderRequest, OrderResponse, feign/StockDeductRequest)
│   │   ├── enums/OrderStatusEnum.java
│   │   ├── event/OrderCreatedEvent.java, OrderEventPublisher.java
│   │   └── feign/StockFeignClient.java
│   ├── product/   # product domain (similar sub‑structure)
│   └── user/      # user domain (similar sub‑structure)
└── feign/                               # cross‑service Feign clients (e.g., StockFeign)
    ├── StockServiceFeign.java
    ├── PaymentServiceFeign.java
    └── LogisticsServiceFeign.java

Layer Responsibilities and Prohibitions

api/ : defines interface contracts (method signatures + OpenAPI); must not contain implementation.

controller/ : receives requests, calls Service, wraps response, logs; must not contain business logic.

service/ : implements business logic, orchestrates multiple DAO/Feign calls; must not directly handle HTTP request/response.

dao/ : SQL queries and persistence; must not contain business decision logic.

entity/ : database table mapping; must not contain business methods (fat‑entity vs. anemic model).

dto/ : data transfer objects for inter‑layer communication; must not be bound to database tables.

enums/ : business status and type definitions.

feign/ : defines interfaces for calling other domains/services; must not contain implementation.

Dependency Direction Rules

Controller → Service → DAO
      ↓          ↓
      DTO        Entity
      ↓
   Feign (calls other domains)

Cross‑Domain Communication

In‑process Calls (same JVM)

Two recommended ways:

Spring dependency injection: Order Service injects Product Service and calls it directly.

Domain events: Order Service publishes an event; Product Service listens and processes it.

public class OrderServiceImpl implements OrderService {
    private final ProductService productService;
    public OrderServiceImpl(ProductService productService) {
        this.productService = productService;
    }
    @Override
    public OrderResponse createOrder(CreateOrderRequest request) {
        ProductInfo product = productService.getProductById(request.getProductId());
        // ... create order logic
    }
}

Feign Calls Between Microservices

When domains reside in different services, communication happens via HTTP/Feign.

OrderService  ── Feign (HTTP POST) ──► StockController
StockFeign   ──► StockService

Feign Client Definition

/**
 * Stock service Feign client.
 * Used by the order domain to call stock‑deduction APIs.
 */
@FeignClient(name = "stock-service", url = "{feign.stock.url:}")
public interface StockFeign {
    @GetMapping("/api/stock/query")
    Result<Integer> queryStock(@RequestParam("productId") Long productId,
                               @RequestParam("warehouseId") Long warehouseId);

    @PostMapping("/api/stock/deduct")
    Result<Void> deductStock(@RequestParam("productId") Long productId,
                              @RequestParam("quantity") Integer quantity);
}

/** DTO used by the Feign client */
@Data
class ProductInfo {
    private Long productId;
    private String name;
    private BigDecimal price;
    private String category;
}

Comparison of DDD vs Traditional Layering

Organization : traditional – controller/, service/, dao/; DDD – domain/order/, domain/product/.

Modification Scope : traditional – adding a feature touches many top‑level packages; DDD – changes stay within a single domain package.

Team Collaboration : traditional – many developers edit the same service package, causing conflicts; DDD – domains are independent, reducing conflicts.

Understandability : traditional – need to navigate multiple packages to see a business flow; DDD – opening one domain package reveals the whole business.

Splittability : traditional – hard to split into microservices; DDD – domains naturally map to microservice boundaries.

Complexity : traditional – simple projects are straightforward; DDD – may be over‑engineered for very small projects.

Suitable Scale : traditional – small projects (<10 entities); DDD – medium/large projects (>10 entities, multiple domains, multi‑person teams).

Evolution from Monolith to Microservices

DDD’s biggest advantage is that a domain is a natural microservice split point.

Phase 1: Monolith (all domains in one service)
my-shop-service
│ ├── domain/order/
│ ├── domain/product/
│ └── domain/user/

Phase 2: Isolate domains with Feign interfaces (prepare for split)
my-shop-service
│ ├── domain/order/ (contains feign/ProductFeign)
│ ├── domain/product/
│ └── domain/user/

Phase 3: Split into independent microservices
order-service   product-service   user-service
domain/order/    domain/product/    domain/user/
   Feign HTTP calls connect them

When splitting, only three steps are needed:

Move the domain package to a new project.

Replace local calls with Feign remote calls (interfaces already defined).

Configure service discovery (Nacos/Eureka).

FAQ

Q1: Small projects also need DDD? Not necessarily. If there are only 3‑5 entities and 2‑3 APIs, traditional layering is simpler. DDD shines when there are >10 entities, >3 domains, multiple developers, or future microservice plans.

Q2: Can Entity contain business methods? DDD prefers a rich model (behaviour inside Entity), but Java EE often uses anemic models (Entity only getters/setters, logic in Service). Both are acceptable.

Q3: Can a Service in one domain directly call another domain’s Mapper? No. This breaks bounded‑context isolation. Use the other domain’s Service (same service) or Feign (different service).

Q4: Should dto/api/ and dto/feign/ be shared? Not recommended. They serve different consumers (frontend vs. other services) and have different change drivers, so sharing creates coupling.

Q5: Where to place Feign clients? If a Feign client is used only by one domain, put it under that domain (e.g., domain/order/feign/). If multiple domains need it, place it in a top‑level feign/ package.

Q6: How to handle cross‑domain transactions? Within a single service, use @Transactional. For distributed transactions, prefer eventual consistency (message + compensation), Saga, TCC, or frameworks like Seata.

Conclusion

Organize code by business domain, not by technical layer.

Each bounded context is autonomous with clear boundaries.

Domain layer contains core business logic; infrastructure implements technical details.

Cross‑domain interaction must go through interfaces or Feign, never direct access.

Expose DTOs outward; keep Entities internal.

DDD provides a natural path from monolith to microservices.

Best suited for medium to large projects, multi‑person teams, and scenarios where future service splitting is expected.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Javabackend architecturemicroservicesDomain‑Driven DesignSpring BootDDD
The Dominant Programmer
Written by

The Dominant Programmer

Resources and tutorials for programmers' advanced learning journey. Advanced tracks in Java, Python, and C#. Blog: https://blog.csdn.net/badao_liumang_qizhi

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.