
Ddd Architecture Onion
- 13 installs
- 1 repo stars
- Updated July 29, 2026
- full-statck-skills/ddd-skills
Implements Jeffrey Palermo's Onion Architecture with a domain core and outer-to-inner dependency direction using Java/Spring Boot.
About
Guides Onion Architecture implementation with concentric core/application/infrastructure/api layers and strict inward dependencies. A developer uses it to build a domain-centric DDD system with layered isolation.
- Ten-step scaffold-to-migration workflow
- Domain layer with zero framework dependencies
Ddd Architecture Onion by the numbers
- 13 all-time installs (skills.sh)
- Ranked #3,516 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Jul 30, 2026 (Skillselion catalog sync)
npx skills add https://github.com/full-statck-skills/ddd-skills --skill ddd-architecture-onionAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 13 |
|---|---|
| repo stars | ★ 1 |
| Last updated | July 29, 2026 |
| Repository | full-statck-skills/ddd-skills ↗ |
What it does
Implements Jeffrey Palermo's Onion Architecture with a domain core and outer-to-inner dependency direction using Java/Spring Boot.
Files
DDD Architecture — Onion (洋葱架构)
基于 Jeffrey Palermo(2008)提出的同心圆架构模型,以 Domain 为圆心,依赖方向严格指向内层。
Workflow
Step 1: 适用性检查 — 判断项目是否适合洋葱架构 | 参考:SKILL.md When to Use
Step 2: 搭建目录骨架 — 创建四层模块 core/infrastructure/api/composition | 参考:references/04
Step 3: 编写 Domain 核心 — 聚合根、值对象、Repository 接口、领域事件(零框架依赖)| 参考:references/01
Step 4: 编写 Application 接口 — 应用服务接口契约,纯编排不含业务逻辑 | 参考:references/02
Step 5: 编写 Infrastructure 实现 — 实现 Domain 层 Repository/MQ/外部客户端 | 参考:references/03
Step 6: 编写 API 适配器 — Controller、DTO、Assembler、全局异常处理 | 参考:references/04
Step 7: 配置 DI 组装 — composition 模块集中装配 | 参考:references/05
Step 8: 编写测试 — Domain 层单测 → Application Mock → Infra 集成测试 | 参考:references/06
Step 9: Gotchas 检查 — 对照常见陷阱逐条验证 | 参考:各 references 陷阱章节
Step 10: 迁移(如适用) — 三层→洋葱渐进迁移 | 参考:references/07
When to Read (触发条件)
| 用户问题 | 加载动作 |
|---|---|
| "帮我用洋葱架构搭建项目" | 打开 SKILL.md + references/01-05 |
| "Repository 接口应该放哪?" | 打开 references/01-domain-model |
| "洋葱架构怎么测试?" | 打开 references/06-testing |
| "从三层怎么迁到洋葱?" | 打开 references/07-migration-from-layered |
| "洋葱和六边形有什么区别?" | 打开 references/08-comparison |
| "洋葱支持 CQRS 吗?" | 打开 examples/example-04-cqrs-onion |
| "多入口怎么设计?" | 打开 examples/example-03-multi-entry |
| "DI 怎么配置 Spring Boot?" | 打开 references/05-di-composition |
When to Use
| ✅ 适用场景 | ❌ 不适用 |
|---|---|
| 基础设施频繁变更(DB/MQ/缓存厂商更换) | 简单 CRUD 项目(过度设计) |
| 单元测试覆盖率要求 > 80% | 两周交付的原型/PoC |
| 多入口系统(REST + CLI + MQ + gRPC) | 单入口 + 单数据库的简单服务 |
| 团队有接口抽象和 DI 设计能力 | 团队不熟悉依赖倒置 |
| 业务规则复杂,需要严格隔离 | 三层架构够用 |
Boundary
| 区域 | 归属 |
|---|---|
| 三层→洋葱迁移 | ✅ 本 Skill(references/07-migration) |
| 洋葱 vs 六边形 vs 整洁对比 | ✅ 本 Skill(references/08-comparison) |
| 选型决策 | ❌ 用 ddd-architecture-selector |
| 领域建模 | ❌ 用 ddd-domain-designer |
| 代码审查 | ❌ 用 ddd-code-reviewer |
| COLA 项目 | ❌ 用 ddd-architecture-cola |
Audience
This skill is designed for: Backend developers (implementing DDD architectures), Software architects (evaluating and selecting patterns), Tech leads (reviewing team implementations), and DDD beginners (learning domain-driven design fundamentals).
核心原理:同心圆依赖规则
所有依赖关系指向圆心(Domain),内层定义接口,外层实现接口。
Infrastructure → API/Adapters → Application → Domain Core ★
所有依赖指向圆心,内层零框架依赖。Jeffrey Palermo 四原则
1. 应用核心独立于基础设施 — Domain 层不 import 任何框架/数据库类 2. 内层定义接口,外层实现 — 如 OrderRepository 接口在 Domain,实现在 Infrastructure 3. 依赖指向圆心 — Infrastructure → Application → Domain,不允许逆向 4. 外层知道内层,内层不知道外层 — Domain 对 Infrastructure 一无所知
Rules
| # | 规则 | 检测方式 | 级别 |
|---|---|---|---|
| 1 | Domain 层零框架依赖 | 搜索 Domain 包中的框架 import | P0 |
| 2 | Repository 接口在 Domain,实现在 Infra | 检查接口/实现位置 | P0 |
| 3 | Application 层纯编排,不含业务 if/else | 代码审查 | P0 |
| 4 | 值对象不可变(final 字段,无 setter) | ValueObject 类检查 | P1 |
| 5 | 跨聚合通过 ID 引用 | 聚合根字段类型检查 | P1 |
| 6 | DTO 在 API 层定义,不泄露到 Domain | 检查 Domain 中 DTO import | P0 |
| 7 | Composition Root 集中管理 DI | 检查各层是否有 new 依赖 | P1 |
详见 references/08-comparison.md 了解洋葱 vs 六边形 vs 整洁架构的详细对比。
目录结构
{project}/
├── {project}-core/domain/ # 领域模型(零框架依赖)
├── {project}-core/application/ # 应用服务接口
├── {project}-infrastructure/ # Repository 实现、MQ、外部集成
├── {project}-api/ # Controller、DTO、Assembler
└── {project}-composition/ # DI 组装模块依赖规则
| 模块 | 可依赖 | 不可依赖 |
|---|---|---|
| core/domain | 无(纯 Java) | Spring/JPA/MyBatis |
| core/application | domain | infrastructure, api |
| infrastructure | core | api |
| api | core, infrastructure | — |
| composition | 所有模块 | — |
开发规范(详见各 references)
| # | 规范 | 违规示例 |
|---|---|---|
| 1 | Domain 零框架依赖 | import org.springframework.stereotype.Service |
| 2 | Repository 接口在 Domain | 接口和实现都在 Infra |
| 3 | Application 只编排不实现 | AppService 包含业务判断 |
| 4 | 事务边界在 Application | Controller 或 Repository 开事务 |
| 5 | 跨聚合通过领域事件 | 聚合 A 直接引用聚合 B |
| 6 | 值对象不可变 | ValueObject 有 setter |
| 7 | 实体充血模型 | 实体只有 getter/setter |
| 8 | DTO 在 API 层定义 | Domain 中有 DTO import |
| 9 | Composition Root 最外层 | 各层自己 new 依赖 |
| 10 | 严格分层 | API 直接调用 Infrastructure |
Gotchas — (详见各 references 陷阱章节)
15 条核心陷阱:Domain 层框架泄露 / Repository 接口位置 / Application 过厚 / 过度抽象 / Composition Root 分散 / 值对象可变 / 聚合根过大 / 跨聚合直接引用 / Controller 含业务逻辑 / DTO 泄露 / PO 注解 / 缺少事件 / 事务跨聚合 / Infra 侵入测试
FAQ — (详见 references/08-comparison.md 和各层 references)
涵盖:洋葱 vs 六边形、Domain 框架限制、值对象 vs 实体、Repository 接口位置、聚合大小、迁移策略、CQRS 兼容、测试层次、微服务适配、事件跨服务等。
Keywords
onion architecture, 洋葱架构, Jeffrey Palermo, domain-centric architecture,
layered isolation, dependency inversion, 依赖倒置, concentric layers,
core/domain, application interfaces, infrastructure implementation,
API adapters, composition root, DI assembly, domain model,
repository pattern, aggregate root, value object, domain service,
DDD layered, clean architecture comparison, hexagonal comparisonReferences
| 文件 | 用途 |
|---|---|
| references/01-domain-model.md | 领域层设计详解:聚合根、值对象、领域服务 |
| references/02-application-interfaces.md | 应用层接口设计:服务编排与事务边界 |
| references/03-infrastructure-implementation.md | 基础设施层实现:Repository、MQ、外部集成 |
| references/04-api-adapters.md | API 适配层:Controller、DTO、Assembler |
| references/05-di-composition.md | DI 组装:Composition Root 配置 |
| references/06-testing.md | 测试策略:单元测试、集成测试、契约测试 |
| references/07-migration-from-layered.md | 从三层架构迁移到洋葱架构的完整路径 |
| references/08-comparison.md | 洋葱 vs 六边形 vs 整洁架构详细对比 |
Examples
业务场景示例
| 示例 | 说明 |
|---|---|
| examples/example-01-order-payment.md | 订单支付完整示例(含 Domain/Application/Infra/API) |
| examples/example-02-product-catalog.md | 产品目录管理示例(含多聚合协作) |
| examples/example-03-multi-entry.md | 多入口系统示例(REST + MQ + CLI 三种适配器) |
| examples/example-04-cqrs-onion.md | CQRS + 洋葱融合示例(Command/Query 分离) |
| examples/example-05-user-registration.md | 用户注册 + 邮件验证示例 |
项目规模示例
| 示例 | 说明 |
|---|---|
| examples/06-monolith-simple.md | 单体简单:单模块项目,一个限界上下文 |
| examples/07-monolith-complex.md | 单体复杂:单模块多限界上下文,内部隔离 |
| examples/08-monolith-multi-module.md | 单体多模块:Maven 多模块强制分层 |
| examples/09-microservice-simple.md | 微服务简单:单服务单上下文洋葱 |
| examples/10-microservice-complex.md | 微服务复杂:单服务多上下文内部隔离 |
| examples/11-microservice-multi-module.md | 微服务多模块:单服务 Maven 多模块洋葱 |
| examples/12-microservice-complex-multi.md | 微服务复杂多模块:多上下文 Maven 多模块 |
Primary Sources: Onion Architecture Part 1-4 — Jeffrey Palermo (2008) · DDD Blue Book — Eric Evans (2003) · Implementing DDD — Vaughn Vernon (2013)
Implementation: Microsoft DDD Microservice · Testcontainers
Output
回答始终包含:适用性判断 → 目录结构 → 每层代码模板 → DI 装配 → 测试骨架 → Gotchas 合规检查。详细参考 references/ 和 examples/ 文件。
---
技能旅程
📍 当前:洋葱架构落地
← 上一步:selector → 下一步:domain-designer 🔗 相关:cqrs-architecture | code-reviewer 🏠 首页:awesome
核心口诀:内层定义接口,外层实现接口,依赖指向圆心。
Security & Safety
This skill is pure documentation. It contains no executable scripts, collects no user data, accesses no external services or networks.
验证清单
- [ ] Domain 层无框架 import
- [ ] Repository 接口在 Domain 层
- [ ] Repository 实现在 Infrastructure 层
- [ ] Application 层无业务 if/else
- [ ] 值对象不可变
- [ ] 跨聚合通过 ID 引用
- [ ] DTO 只定义在 API 层
- [ ] DI 集中在 composition 模块
- [ ] 领域事件已发布
Example 01 — 订单支付完整示例(Order Payment)
完整的洋葱架构订单支付示例,包含 Domain / Application / Infrastructure / API / Composition 五层。
业务描述
用户下单并支付。一个订单包含多个商品,订单有草稿、已提交、已支付、已取消等状态。
Domain 层
// core/domain/model/order/Order.java
public class Order extends AggregateRoot<OrderId> {
private OrderId id;
private CustomerId customerId;
private Money totalAmount;
private List<OrderItem> items;
private OrderStatus status;
private LocalDateTime createdAt;
private Order(OrderId id, CustomerId customerId) {
this.id = id;
this.customerId = customerId;
this.items = new ArrayList<>();
this.status = OrderStatus.DRAFT;
this.createdAt = LocalDateTime.now();
}
public static Order create(OrderId id, CustomerId customerId) {
Order order = new Order(id, customerId);
order.addDomainEvent(new OrderCreatedEvent(id, customerId));
return order;
}
public void addItem(ProductId productId, Money price, int quantity) {
if (status != OrderStatus.DRAFT) throw new DomainException("只能向草稿添加商品");
this.items.add(new OrderItem(productId, price, quantity));
this.totalAmount = calculateTotal();
}
public void submit() {
if (items.isEmpty()) throw new DomainException("不能提交空订单");
if (status != OrderStatus.DRAFT) throw new DomainException("只能提交草稿订单");
this.status = OrderStatus.SUBMITTED;
addDomainEvent(new OrderSubmittedEvent(this.id));
}
public void pay(PaymentGateway gateway) {
if (status != OrderStatus.SUBMITTED) throw new DomainException("当前状态不可支付");
gateway.charge(this.id, this.totalAmount);
this.status = OrderStatus.PAID;
addDomainEvent(new OrderPaidEvent(this.id, this.totalAmount));
}
public void cancel() {
if (status == OrderStatus.PAID) throw new DomainException("已支付订单不可取消");
this.status = OrderStatus.CANCELLED;
addDomainEvent(new OrderCancelledEvent(this.id));
}
private Money calculateTotal() {
return items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
public OrderId getId() { return id; }
public Money getTotalAmount() { return totalAmount; }
public OrderStatus getStatus() { return status; }
public LocalDateTime getCreatedAt() { return createdAt; }
}
// core/domain/model/order/OrderItem.java
public class OrderItem {
private ProductId productId;
private Money unitPrice;
private int quantity;
public OrderItem(ProductId productId, Money unitPrice, int quantity) {
this.productId = productId;
this.unitPrice = unitPrice;
this.quantity = quantity;
}
public Money getSubtotal() { return unitPrice.multiply(quantity); }
public ProductId getProductId() { return productId; }
public int getQuantity() { return quantity; }
}
// core/domain/repository/OrderRepository.java
public interface OrderRepository {
Optional<Order> findById(OrderId id);
void save(Order order);
void delete(OrderId id);
}Application 层
// core/application/service/OrderApplicationService.java
public interface OrderApplicationService {
OrderDTO createOrder(CreateOrderCommand command);
void payOrder(String orderId);
void cancelOrder(String orderId);
OrderDTO getOrder(String orderId);
}
// core/application/service/OrderApplicationServiceImpl.java
public class OrderApplicationServiceImpl implements OrderApplicationService {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
private final EventPublisher eventPublisher;
private final OrderDTOAssembler assembler;
@Override
@Transactional
public OrderDTO createOrder(CreateOrderCommand command) {
Order order = Order.create(OrderId.generate(), new CustomerId(command.getCustomerId()));
command.getItems().forEach(item ->
order.addItem(new ProductId(item.getProductId()),
Money.of(item.getUnitPrice(), "CNY"), item.getQuantity()));
order.submit();
orderRepository.save(order);
eventPublisher.publishAll(order.getDomainEvents());
return assembler.toDTO(order);
}
@Override
@Transactional
public void payOrder(String orderId) {
Order order = orderRepository.findById(new OrderId(orderId))
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.pay(paymentGateway);
orderRepository.save(order);
eventPublisher.publishAll(order.getDomainEvents());
}
@Override
@Transactional
public void cancelOrder(String orderId) {
Order order = orderRepository.findById(new OrderId(orderId))
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.cancel();
orderRepository.save(order);
eventPublisher.publishAll(order.getDomainEvents());
}
}Infrastructure 层
// infrastructure/data/repository/OrderRepositoryImpl.java
@Repository
public class OrderRepositoryImpl implements OrderRepository {
private final JpaOrderRepository jpaRepo;
private final OrderMapper mapper;
@Override
public Optional<Order> findById(OrderId id) {
return jpaRepo.findById(id.getValue()).map(mapper::toDomain);
}
@Override
public void save(Order order) {
jpaRepo.save(mapper.toPO(order));
}
}
// infrastructure/external/PaymentGatewayImpl.java
@Component
public class PaymentGatewayImpl implements PaymentGateway {
private final RestTemplate restTemplate;
@Override
public void charge(OrderId orderId, Money amount) {
// 调用支付服务
}
}
// infrastructure/messaging/KafkaEventPublisher.java
@Component
public class KafkaEventPublisher implements EventPublisher {
private final KafkaTemplate<String, Object> kafkaTemplate;
@Override
public void publish(DomainEvent event) {
kafkaTemplate.send(event.getClass().getSimpleName(), event);
}
@Override
public void publishAll(List<DomainEvent> events) {
events.forEach(this::publish);
}
}API 层
// api/controller/OrderController.java
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
private final OrderApplicationService orderAppService;
@PostMapping
public ResponseEntity<ApiResponse<OrderResponse>> createOrder(
@Valid @RequestBody CreateOrderRequest request) {
CreateOrderCommand command = CreateOrderRequestAssembler.toCommand(request);
OrderDTO dto = orderAppService.createOrder(command);
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success(OrderResponseAssembler.toResponse(dto)));
}
@PostMapping("/{orderId}/pay")
public ResponseEntity<ApiResponse<Void>> payOrder(@PathVariable String orderId) {
orderAppService.payOrder(orderId);
return ResponseEntity.ok(ApiResponse.success(null));
}
@GetMapping("/{orderId}")
public ResponseEntity<ApiResponse<OrderResponse>> getOrder(@PathVariable String orderId) {
OrderDTO dto = orderAppService.getOrder(orderId);
return ResponseEntity.ok(ApiResponse.success(OrderResponseAssembler.toResponse(dto)));
}
}
// api/dto/request/CreateOrderRequest.java
@Data
public class CreateOrderRequest {
@NotBlank private String customerId;
@NotEmpty @Valid private List<Item> items;
@Data
public static class Item {
@NotBlank private String productId;
@NotNull @Positive private BigDecimal unitPrice;
@Min(1) private int quantity;
}
}Composition 层
// composition/config/OrderModuleConfig.java
@Configuration
public class OrderModuleConfig {
@Bean
public OrderRepository orderRepository(JpaOrderRepository jpa, OrderMapper m) {
return new OrderRepositoryImpl(jpa, m);
}
@Bean
public OrderApplicationService orderAppService(
OrderRepository repo, PaymentGateway gw, EventPublisher ep, OrderDTOAssembler asm) {
return new OrderApplicationServiceImpl(repo, gw, ep, asm);
}
@Bean
public OrderController orderController(OrderApplicationService svc) {
return new OrderController(svc);
}
}Example 02 — 产品目录管理(Product Catalog)
展示洋葱架构中多个聚合通过 Application 层协作的模式。
业务描述
产品目录管理包含产品(Product)和分类(Category)两个聚合。产品归属某个分类,查询时需跨聚合获取分类名称。
模型设计
Category (聚合根) Product (聚合根)
└── categoryId (值对象) └── productId (值对象)
└── name (值对象) └── categoryId (引用,非对象)
└── parentCategoryId (引用) └── name (值对象)
└── products (不持有!) └── price (值对象)Domain 层
// core/domain/model/product/Product.java
public class Product extends AggregateRoot<ProductId> {
private ProductId id;
private CategoryId categoryId; // 跨聚合 ID 引用
private ProductName name; // 值对象
private Money price; // 值对象
private ProductStatus status;
private LocalDateTime createdAt;
private Product(ProductId id, CategoryId categoryId, ProductName name, Money price) {
this.id = id;
this.categoryId = categoryId;
this.name = name;
this.price = price;
this.status = ProductStatus.ACTIVE;
this.createdAt = LocalDateTime.now();
}
public static Product create(ProductId id, CategoryId categoryId, ProductName name, Money price) {
if (price.isNegativeOrZero()) throw new DomainException("价格必须大于0");
Product product = new Product(id, categoryId, name, price);
product.addDomainEvent(new ProductCreatedEvent(id, categoryId));
return product;
}
public void updatePrice(Money newPrice) {
if (newPrice.isNegativeOrZero()) throw new DomainException("价格必须大于0");
this.price = newPrice;
addDomainEvent(new ProductPriceChangedEvent(this.id, this.price));
}
public void deactivate() {
if (this.status == ProductStatus.INACTIVE) return;
this.status = ProductStatus.INACTIVE;
addDomainEvent(new ProductDeactivatedEvent(this.id));
}
// getter
public ProductId getId() { return id; }
public CategoryId getCategoryId() { return categoryId; }
public ProductName getName() { return name; }
public Money getPrice() { return price; }
public ProductStatus getStatus() { return status; }
}
// core/domain/model/category/Category.java
public class Category extends AggregateRoot<CategoryId> {
private CategoryId id;
private CategoryName name; // 值对象
private CategoryId parentCategoryId; // 父分类 ID 引用
private int level;
private boolean active;
public static Category create(CategoryId id, CategoryName name) {
Category category = new Category();
category.id = id;
category.name = name;
category.level = 0;
category.active = true;
category.addDomainEvent(new CategoryCreatedEvent(id));
return category;
}
public static Category createSubCategory(CategoryId id, CategoryName name, CategoryId parentId) {
Category category = create(id, name);
category.parentCategoryId = parentId;
category.level = 1;
return category;
}
public void rename(CategoryName newName) {
this.name = newName;
}
public CategoryId getId() { return id; }
public CategoryName getName() { return name; }
public CategoryId getParentCategoryId() { return parentCategoryId; }
public int getLevel() { return level; }
}
// core/domain/repository/ProductRepository.java
public interface ProductRepository {
Optional<Product> findById(ProductId id);
void save(Product product);
Page<Product> findByCategoryId(CategoryId categoryId, Pageable pageable);
Page<Product> findByNameContaining(String keyword, Pageable pageable);
}
// core/domain/repository/CategoryRepository.java
public interface CategoryRepository {
Optional<Category> findById(CategoryId id);
void save(Category category);
List<Category> findAllActive();
}Application 层(跨聚合编排)
// core/application/service/ProductCatalogService.java
public interface ProductCatalogService {
ProductDTO createProduct(CreateProductCommand command);
ProductDTO getProductWithCategory(String productId);
Page<ProductDTO> searchProducts(String keyword, int page, int size);
void updateProductPrice(String productId, BigDecimal newPrice);
}
// core/application/service/ProductCatalogServiceImpl.java
public class ProductCatalogServiceImpl implements ProductCatalogService {
private final ProductRepository productRepository;
private final CategoryRepository categoryRepository;
private final ProductDTOAssembler assembler;
@Override
@Transactional
public ProductDTO createProduct(CreateProductCommand command) {
// 1. 验证分类存在(跨聚合查询)
Category category = categoryRepository.findById(new CategoryId(command.getCategoryId()))
.orElseThrow(() -> new CategoryNotFoundException(command.getCategoryId()));
// 2. 创建产品(Domain 层)
Product product = Product.create(
ProductId.generate(),
category.getId(),
new ProductName(command.getProductName()),
Money.of(command.getPrice(), "CNY")
);
// 3. 持久化
productRepository.save(product);
// 4. 返回 DTO(含分类名称)
return assembler.toDTOWithCategory(product, category);
}
@Override
@Transactional(readOnly = true)
public ProductDTO getProductWithCategory(String productId) {
Product product = productRepository.findById(new ProductId(productId))
.orElseThrow(() -> new ProductNotFoundException(productId));
Category category = categoryRepository.findById(product.getCategoryId())
.orElse(null);
return assembler.toDTOWithCategory(product, category);
}
}Infrastructure 层
// infrastructure/data/repository/ProductRepositoryImpl.java
@Repository
public class ProductRepositoryImpl implements ProductRepository {
private final JpaProductRepository jpaRepo;
private final ProductMapper mapper;
@Override
public void save(Product product) {
jpaRepo.save(mapper.toPO(product));
}
@Override
public Optional<Product> findById(ProductId id) {
return jpaRepo.findById(id.getValue()).map(mapper::toDomain);
}
}
// infrastructure/data/repository/CategoryRepositoryImpl.java
@Repository
public class CategoryRepositoryImpl implements CategoryRepository {
private final JpaCategoryRepository jpaRepo;
private final CategoryMapper mapper;
@Override
public void save(Category category) {
jpaRepo.save(mapper.toPO(category));
}
}API 层
// api/controller/ProductController.java
@RestController
@RequestMapping("/api/v1/products")
public class ProductController {
private final ProductCatalogService catalogService;
@PostMapping
public ResponseEntity<ApiResponse<ProductResponse>> createProduct(
@Valid @RequestBody CreateProductRequest request) {
ProductDTO dto = catalogService.createProduct(request.toCommand());
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success(ProductResponseAssembler.toResponse(dto)));
}
@GetMapping("/{productId}")
public ResponseEntity<ApiResponse<ProductResponse>> getProduct(
@PathVariable String productId) {
ProductDTO dto = catalogService.getProductWithCategory(productId);
return ResponseEntity.ok(ApiResponse.success(ProductResponseAssembler.toResponse(dto)));
}
@PutMapping("/{productId}/price")
public ResponseEntity<ApiResponse<Void>> updatePrice(
@PathVariable String productId,
@RequestBody @NotNull BigDecimal newPrice) {
catalogService.updateProductPrice(productId, newPrice);
return ResponseEntity.ok(ApiResponse.success(null));
}
}关键设计点
1. 跨聚合 ID 引用:Product 持有 CategoryId(值对象),不持有 Category 对象 2. 聚合协作在 Application 层:ProductCatalogService 同时调用两个 Repository,在应用层组合数据 3. 不变的 Domain 层:Product 和 Category 彼此完全不知道对方存在 4. DTO 聚合在 Application 层:ProductDTOAssembler.toDTOWithCategory() 组装跨聚合数据
Example 03 — 多入口系统示例(REST + MQ + CLI)
洋葱架构最适合多入口系统——同一领域核心被多种适配器共享,Domain 层完全不受入口类型影响。
业务描述
一个订单处理系统,支持三种入口:
- REST API — 前端用户操作
- MQ Consumer — 第三方系统通过消息创建订单
- CLI 批处理 — 每日自动取消超时未支付订单
架构示意图
┌───────────────┐
REST ──────────▶│ API Layer │
│ Controller │
└───────┬───────┘
│
┌───────▼───────┐ ┌─────────────────────┐
MQ ────────────▶│ Application │ │ Infrastructure │
│ Layer │◀────────▶│ ┌────────────────┐ │
│ Service │ │ │ Repository │ │
└───────┬───────┘ │ │ EventPublisher │ │
│ │ │ External API │ │
┌───────▼───────┐ │ └────────────────┘ │
CLI ────────────▶│ Domain ★ │ └─────────────────────┘
│ Core │
│ (纯业务逻辑) │
└───────────────┘Domain 层(完全不受入口影响)
// core/domain/model/order/Order.java
public class Order extends AggregateRoot<OrderId> {
// 与前例相同,Domain 层对入口方式零感知
// ... 业务方法不变
}
// core/domain/service/OrderTimeoutService.java
public class OrderTimeoutService {
private final OrderRepository orderRepository;
public OrderTimeoutService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
/**
* 取消所有超时未支付订单(供 CLI 和 定时任务调用)
*/
public int cancelTimeoutOrders(Duration timeout) {
LocalDateTime deadline = LocalDateTime.now().minus(timeout);
List<Order> timeoutOrders = orderRepository.findSubmittedBefore(deadline);
for (Order order : timeoutOrders) {
order.cancel();
orderRepository.save(order);
}
return timeoutOrders.size();
}
}Application 层
// core/application/service/OrderCommandService.java
public interface OrderCommandService {
OrderDTO createOrderFromREST(CreateOrderCommand command);
OrderDTO createOrderFromMQ(MQCreateOrderCommand command);
int cancelTimeoutOrders();
}
// 注意:无论来自 REST 还是 MQ,最终都调用相同的 Domain 方法
// 区别仅在于 Command 对象和校验逻辑不同
public class OrderCommandServiceImpl implements OrderCommandService {
private final OrderRepository orderRepository;
private final OrderTimeoutService timeoutService;
private final EventPublisher eventPublisher;
private final OrderDTOAssembler assembler;
@Override
@Transactional
public OrderDTO createOrderFromREST(CreateOrderCommand command) {
// REST 入口:标准校验
validateCustomer(command.getCustomerId());
return doCreateOrder(command.getCustomerId(), command.getItems());
}
@Override
@Transactional
public OrderDTO createOrderFromMQ(MQCreateOrderCommand command) {
// MQ 入口:MQ 特有校验(幂等性、重试检测)
if (isDuplicateMQMessage(command.getMessageId())) {
return getExistingOrder(command.getOrderId());
}
return doCreateOrder(command.getCustomerId(), command.getItems());
}
@Override
@Transactional
public int cancelTimeoutOrders() {
return timeoutService.cancelTimeoutOrders(Duration.ofHours(2));
}
// 共享的核心创建逻辑
private OrderDTO doCreateOrder(String customerId, List<Item> items) {
Order order = Order.create(OrderId.generate(), new CustomerId(customerId));
items.forEach(item -> order.addItem(
new ProductId(item.getProductId()),
Money.of(item.getUnitPrice(), "CNY"),
item.getQuantity()
));
order.submit();
orderRepository.save(order);
eventPublisher.publishAll(order.getDomainEvents());
return assembler.toDTO(order);
}
}入口 1:REST API 适配器
// api/controller/OrderController.java
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
private final OrderCommandService orderCommandService;
@PostMapping
public ResponseEntity<ApiResponse<OrderResponse>> createOrder(
@Valid @RequestBody CreateOrderRequest request) {
CreateOrderCommand command = /* 转换 */;
OrderDTO dto = orderCommandService.createOrderFromREST(command);
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success(/* 转换 */));
}
}入口 2:MQ 消费者适配器
// api/consumer/OrderMessageConsumer.java
@Component
public class OrderMessageConsumer {
private final OrderCommandService orderCommandService;
@KafkaListener(topics = "order-commands")
public void onOrderMessage(ConsumerRecord<String, MQCreateOrderCommand> record) {
try {
MQCreateOrderCommand command = record.value();
OrderDTO dto = orderCommandService.createOrderFromMQ(command);
log.info("MQ 订单创建成功: {}", dto.getOrderId());
} catch (Exception e) {
log.error("MQ 订单处理失败", e);
// 发到 DLQ,后续补偿
}
}
}入口 3:CLI / 定时任务适配器
// api/job/OrderTimeoutJob.java
@Component
public class OrderTimeoutJob {
private final OrderCommandService orderCommandService;
@Scheduled(cron = "0 0 2 * * ?") // 每天凌晨 2 点
public void cancelTimeoutOrders() {
int cancelled = orderCommandService.cancelTimeoutOrders();
log.info("定时取消超时订单: {} 个", cancelled);
}
}
// api/cli/CancelTimeoutCommand.java
@Component
public class CancelTimeoutCommand implements CommandLineRunner {
private final OrderCommandService orderCommandService;
@Override
public void run(String... args) {
if (args.length > 0 && "cancel-timeout".equals(args[0])) {
int count = orderCommandService.cancelTimeoutOrders();
System.out.println("已取消 " + count + " 个超时订单");
}
}
}关键设计点
1. Domain 层零感知:Order 和 OrderTimeoutService 完全不知道自己是 REST/MQ/CLI 调用的 2. Application 层一视同仁:OrderCommandService 为所有入口服务,入口特有逻辑(幂等、校验)在各适配器 3. 新增入口零成本:加 gRPC?只需新增一个 GrpcOrderService,Domain/Application 层完全不动 4. 每个适配器只关心协议转换:不做业务判断
Example 04 — CQRS + 洋葱架构融合示例
在洋葱架构中融入 CQRS 模式,实现 Application 层的读写分离。
CQRS 集成点在洋葱中的位置
Command Path Query Path
──────────── ──────────
API Layer: POST /orders GET /orders/{id}
│ │
▼ ▼
Application: OrderCommandService OrderQueryService
Layer: (写操作:创建/支付/取消) (读操作:查询详情/列表)
│ │
▼ ▼
Domain Domain 实体 + Repository 接口 (可能绕过 Domain,直接读)
Layer: (业务规则 + 不变式) (查询不修改状态)
│
▼
Infra: OrderRepositoryImpl OrderQueryRepositoryImpl
(JPA 写库) (JPA 读库 / 物化视图)Domain 层(只处理 Command 写操作)
// core/domain/model/order/Order.java
// 与 Example 01 相同 — Domain 层只处理写操作(Command)
// 查询不进入 Domain 层
// core/domain/repository/OrderRepository.java
public interface OrderRepository {
Optional<Order> findById(OrderId id);
void save(Order order);
}Application 层:Command 写模型
// core/application/command/OrderCommandService.java
public interface OrderCommandService {
OrderDTO createOrder(CreateOrderCommand command);
void payOrder(String orderId);
void cancelOrder(String orderId);
}
// core/application/command/OrderCommandServiceImpl.java
public class OrderCommandServiceImpl implements OrderCommandService {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
private final EventPublisher eventPublisher;
private final OrderDTOAssembler assembler;
@Override
@Transactional
public OrderDTO createOrder(CreateOrderCommand command) {
Order order = Order.create(OrderId.generate(), new CustomerId(command.getCustomerId()));
command.getItems().forEach(item ->
order.addItem(new ProductId(item.getProductId()),
Money.of(item.getUnitPrice(), "CNY"), item.getQuantity()));
order.submit();
orderRepository.save(order);
eventPublisher.publishAll(order.getDomainEvents());
return assembler.toDTO(order);
}
@Override
@Transactional
public void payOrder(String orderId) {
Order order = orderRepository.findById(new OrderId(orderId))
.orElseThrow(() -> new OrderNotFoundException(orderId));
order.pay(paymentGateway);
orderRepository.save(order);
eventPublisher.publishAll(order.getDomainEvents());
}
}Application 层:Query 读模型
// core/application/query/OrderQueryService.java
public interface OrderQueryService {
OrderDetailDTO getOrderDetail(String orderId);
OrderListDTO listOrders(String customerId, int page, int size);
OrderStatisticsDTO getOrderStatistics(LocalDate start, LocalDate end);
}
// core/application/query/OrderQueryServiceImpl.java
public class OrderQueryServiceImpl implements OrderQueryService {
// 注意:这里注入的是查询专用的 Repository(可指向读库)
private final OrderQueryRepository queryRepository;
public OrderQueryServiceImpl(OrderQueryRepository queryRepository) {
this.queryRepository = queryRepository;
}
@Override
@Transactional(readOnly = true)
public OrderDetailDTO getOrderDetail(String orderId) {
return queryRepository.findDetailById(orderId)
.orElseThrow(() -> new OrderNotFoundException(orderId));
}
@Override
@Transactional(readOnly = true)
public OrderListDTO listOrders(String customerId, int page, int size) {
return queryRepository.findListByCustomer(customerId, page, size);
}
@Override
@Transactional(readOnly = true)
public OrderStatisticsDTO getOrderStatistics(LocalDate start, LocalDate end) {
return queryRepository.getStatistics(start, end);
}
}查询 DTO(读模型专用,扁平化结构)
// core/application/query/dto/OrderDetailDTO.java
@Data
public class OrderDetailDTO {
private String orderId;
private String customerId;
private String customerName; // 来自读模型(不经过 Domain 实体)
private String status;
private List<OrderItemDTO> items;
private BigDecimal totalAmount;
private String currency;
private LocalDateTime createdAt;
private LocalDateTime paidAt;
@Data
public static class OrderItemDTO {
private String productId;
private String productName; // 读模型额外字段
private BigDecimal unitPrice;
private int quantity;
private BigDecimal subtotal;
}
}
// core/application/query/dto/OrderStatisticsDTO.java
@Data
public class OrderStatisticsDTO {
private long totalOrders;
private BigDecimal totalRevenue;
private Map<String, Long> statusDistribution;
private Map<String, BigDecimal> dailyRevenue;
}Infrastructure 层:查询 Repository
// infrastructure/data/query/OrderQueryRepositoryImpl.java
@Repository
public class OrderQueryRepositoryImpl implements OrderQueryRepository {
private final JdbcTemplate jdbcTemplate;
private final ObjectMapper objectMapper;
public OrderQueryRepositoryImpl(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) {
this.jdbcTemplate = jdbcTemplate;
this.objectMapper = objectMapper;
}
@Override
public Optional<OrderDetailDTO> findDetailById(String orderId) {
// 直接 SQL 查询,不经过 Domain 实体
String sql = """
SELECT o.*, c.name as customer_name,
json_agg(json_build_object(
'productId', oi.product_id,
'productName', p.name,
'unitPrice', oi.unit_price,
'quantity', oi.quantity,
'subtotal', oi.unit_price * oi.quantity
)) as items
FROM orders o
JOIN customers c ON o.customer_id = c.id
LEFT JOIN order_items oi ON o.id = oi.order_id
LEFT JOIN products p ON oi.product_id = p.id
WHERE o.id = ?
GROUP BY o.id, c.name
""";
return jdbcTemplate.query(sql, new Object[]{orderId}, rs -> {
if (rs.next()) {
return Optional.of(mapToDetailDTO(rs));
}
return Optional.empty();
});
}
private OrderDetailDTO mapToDetailDTO(ResultSet rs) throws SQLException {
OrderDetailDTO dto = new OrderDetailDTO();
dto.setOrderId(rs.getString("id"));
dto.setCustomerId(rs.getString("customer_id"));
dto.setCustomerName(rs.getString("customer_name"));
dto.setStatus(rs.getString("status"));
dto.setTotalAmount(rs.getBigDecimal("total_amount"));
dto.setCreatedAt(rs.getTimestamp("created_at").toLocalDateTime());
// items 从 JSON 解析
try {
String itemsJson = rs.getString("items");
if (itemsJson != null) {
var items = objectMapper.readValue(itemsJson,
new TypeReference<List<OrderDetailDTO.OrderItemDTO>>() {});
dto.setItems(items);
}
} catch (Exception e) {
throw new RuntimeException("解析订单项失败", e);
}
return dto;
}
}API 层:读写分离的 Controller
// api/controller/OrderCommandController.java
@RestController
@RequestMapping("/api/v1/orders")
public class OrderCommandController {
private final OrderCommandService commandService;
@PostMapping
public ResponseEntity<ApiResponse<OrderResponse>> createOrder(
@Valid @RequestBody CreateOrderRequest request) {
OrderDTO dto = commandService.createOrder(request.toCommand());
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success(OrderResponseAssembler.toResponse(dto)));
}
@PostMapping("/{orderId}/pay")
public ResponseEntity<ApiResponse<Void>> payOrder(@PathVariable String orderId) {
commandService.payOrder(orderId);
return ResponseEntity.ok(ApiResponse.success(null));
}
}
// api/controller/OrderQueryController.java
@RestController
@RequestMapping("/api/v1/orders")
public class OrderQueryController {
private final OrderQueryService queryService;
@GetMapping("/{orderId}")
public ResponseEntity<ApiResponse<OrderDetailResponse>> getOrderDetail(
@PathVariable String orderId) {
OrderDetailDTO dto = queryService.getOrderDetail(orderId);
return ResponseEntity.ok(
ApiResponse.success(OrderDetailResponseAssembler.toResponse(dto)));
}
@GetMapping
public ResponseEntity<ApiResponse<OrderListResponse>> listOrders(
@RequestParam String customerId,
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "20") int size) {
OrderListDTO dto = queryService.listOrders(customerId, page, size);
return ResponseEntity.ok(
ApiResponse.success(OrderListResponseAssembler.toResponse(dto)));
}
@GetMapping("/statistics")
public ResponseEntity<ApiResponse<OrderStatisticsResponse>> getStatistics(
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate start,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate end) {
OrderStatisticsDTO dto = queryService.getOrderStatistics(start, end);
return ResponseEntity.ok(
ApiResponse.success(OrderStatisticsResponseAssembler.toResponse(dto)));
}
}关键设计点
1. 读写分离:OrderCommandService(写)和 OrderQueryService(读)是完全独立的服务 2. 读模型绕过 Domain:查询直接使用 SQL/JPQL 映射到扁平 DTO,不实例化 Domain 实体 3. 查询 Repository 独立:OrderQueryRepository 与 OrderRepository 是不同接口,可指向不同数据源 4. 统计查询优化:getOrderStatistics() 直接聚合查询,不经过对象映射 5. CQRS 级别:本示例为 L1(模型分离,同一数据源),可扩展为 L2(物理分离数据库)
Example 5: 用户注册 + 邮件验证(User Registration)
展示洋葱架构下用户注册功能的完整实现,包含邮件验证流程。
业务场景
用户通过邮箱注册账号,系统发送验证邮件,用户点击链接完成激活。
目录结构
user-registration/
├── core/domain/ # Domain 层(零框架依赖)
│ ├── model/
│ │ ├── User.java # 聚合根
│ │ ├── UserId.java # 值对象
│ │ ├── Email.java # 值对象(含格式校验)
│ │ └── Password.java # 值对象(含强度校验)
│ ├── service/
│ │ └── PasswordEncryption.java # 领域服务接口
│ ├── repository/
│ │ └── UserRepository.java # 仓储接口
│ └── event/
│ └── UserRegisteredEvent.java # 领域事件
├── core/application/ # Application 层
│ └── service/
│ ├── RegisterUserUseCase.java # 应用服务接口
│ └── impl/
│ └── RegisterUserService.java # 编排实现
├── infrastructure/ # Infrastructure 层
│ ├── data/
│ │ ├── entity/
│ │ │ └── UserPO.java # JPA 持久化对象
│ │ ├── repository/
│ │ │ └── UserRepositoryImpl.java
│ │ └── mapper/
│ │ └── UserMapper.java # PO ↔ Domain 映射
│ └── email/
│ └── SmtpEmailSender.java # 邮件发送实现
├── api/ # API 适配层
│ ├── controller/
│ │ └── RegistrationController.java
│ ├── dto/
│ │ ├── request/
│ │ │ └── RegisterRequest.java
│ │ └── response/
│ │ └── RegisterResponse.java
│ └── assembler/
│ └── UserAssembler.java
└── composition/ # DI 组装层
└── config/
└── UserRegistrationConfig.java关键代码
Domain 层:值对象(不可变)
// core/domain/model/Email.java
public final class Email {
private final String value;
public Email(String value) {
if (value == null || !value.matches("^[A-Za-z0-9+_.-]+@(.+)$")) {
throw new IllegalArgumentException("Invalid email format");
}
this.value = value;
}
public String getValue() { return value; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Email)) return false;
Email email = (Email) o;
return value.equals(email.value);
}
@Override
public int hashCode() { return value.hashCode(); }
}Domain 层:聚合根
// core/domain/model/User.java
public class User {
private final UserId userId;
private final Email email;
private Password password;
private boolean isActive;
private final List<DomainEvent> domainEvents = new ArrayList<>();
public static User register(Email email, Password password, PasswordEncryption encryptor) {
User user = new User(UserId.generate(), email, encryptor.encode(password));
user.isActive = false;
user.addDomainEvent(new UserRegisteredEvent(user.userId, user.email));
return user;
}
public void activate() {
if (isActive) throw new IllegalStateException("User already active");
this.isActive = true;
}
public void addDomainEvent(DomainEvent event) {
domainEvents.add(event);
}
public List<DomainEvent> getDomainEvents() {
return Collections.unmodifiableList(domainEvents);
}
public void clearDomainEvents() {
domainEvents.clear();
}
}Domain 层:仓储接口
// core/domain/repository/UserRepository.java
public interface UserRepository {
Optional<User> findByEmail(Email email);
Optional<User> findById(UserId id);
void save(User user);
boolean existsByEmail(Email email);
}Application 层:编排实现
// core/application/service/impl/RegisterUserService.java
@Service
@Transactional
public class RegisterUserService implements RegisterUserUseCase {
private final UserRepository userRepo;
private final PasswordEncryption encryptor;
private final EmailSender emailSender;
public RegisterUserService(UserRepository userRepo,
PasswordEncryption encryptor,
EmailSender emailSender) {
this.userRepo = userRepo;
this.encryptor = encryptor;
this.emailSender = emailSender;
}
public RegisterResult execute(RegisterCommand cmd) {
// Application 层只编排,不包含业务逻辑
Email email = new Email(cmd.getEmail());
Password password = new Password(cmd.getPassword());
if (userRepo.existsByEmail(email)) {
throw new DuplicateEmailException(email);
}
User user = User.register(email, password, encryptor);
userRepo.save(user);
// 发布领域事件(由 Infrastructure 的 EventPublisher 处理)
user.getDomainEvents().forEach(event -> {
if (event instanceof UserRegisteredEvent) {
emailSender.sendVerificationEmail(
((UserRegisteredEvent) event).getEmail(),
generateVerificationLink(((UserRegisteredEvent) event).getUserId())
);
}
});
user.clearDomainEvents();
return new RegisterResult(user.getUserId().getValue(), false);
}
}Infrastructure 层:仓储实现
// infrastructure/data/repository/UserRepositoryImpl.java
@Repository
public class UserRepositoryImpl implements UserRepository {
private final JpaUserRepository jpaRepo; // Spring Data JPA
private final UserMapper mapper;
@Override
public Optional<User> findByEmail(Email email) {
return jpaRepo.findByEmail(email.getValue())
.map(mapper::toDomain);
}
@Override
public void save(User user) {
UserPO po = mapper.toPO(user);
jpaRepo.save(po);
}
}API 层:Controller
// api/controller/RegistrationController.java
@RestController
@RequestMapping("/api/users")
public class RegistrationController {
private final RegisterUserUseCase registerUseCase;
@PostMapping("/register")
public ResponseEntity<RegisterResponse> register(@Valid @RequestBody RegisterRequest request) {
RegisterCommand cmd = UserAssembler.toCommand(request);
RegisterResult result = registerUseCase.execute(cmd);
return ResponseEntity.status(HttpStatus.CREATED)
.body(UserAssembler.toResponse(result));
}
@PostMapping("/verify")
public ResponseEntity<Void> verify(@RequestParam String token) {
verifyUseCase.execute(new VerifyCommand(token));
return ResponseEntity.ok().build();
}
}洋葱架构合规检查
| 检查项 | 合规状态 |
|---|---|
| Domain 层无框架 import | ✅ 合格(纯 Java) |
| Repository 接口定义在 Domain | ✅ 合格(UserRepository 在 domain/repository) |
| Repository 实现在 Infrastructure | ✅ 合格(UserRepositoryImpl 在 infrastructure) |
| Application 层只编排无业务逻辑 | ✅ 合格(RegisterUserService 仅协调) |
| 值对象不可变 | ✅ 合格(Email/Password 均为 final) |
| DTO 在 API 层定义 | ✅ 合格(RegisterRequest/Response 在 api/dto) |
| DI 集中在 composition 模块 | ✅ 合格(UserRegistrationConfig) |
Example 06 — 单体简单规模(Monolith Simple)
单模块 Maven/Gradle 项目,所有洋葱层放在同一个模块的不同 package 中。适合 1-3 人团队、单一限界上下文。
适用场景
| 条件 | 说明 |
|---|---|
| 团队规模 | 1-3 人 |
| 限界上下文 | 1 个 |
| 部署单元 | 1 个 JAR/WAR |
| 数据库 | 1 个 RDBMS |
| 外部集成 | < 3 个 |
目录树
order-system/
├── pom.xml / build.gradle # 单模块依赖管理
├── src/main/java/com/example/order/
│ ├── core/
│ │ ├── domain/ # Domain 层
│ │ │ ├── model/
│ │ │ │ ├── Order.java # 聚合根
│ │ │ │ ├── OrderId.java # 值对象
│ │ │ │ ├── OrderItem.java # 实体
│ │ │ │ └── OrderStatus.java # 枚举
│ │ │ ├── service/
│ │ │ │ └── PricingService.java # 领域服务
│ │ │ ├── repository/
│ │ │ │ └── OrderRepository.java # 仓储接口
│ │ │ └── event/
│ │ │ ├── OrderCreatedEvent.java
│ │ │ └── OrderPaidEvent.java
│ │ └── application/ # Application 层
│ │ └── service/
│ │ ├── OrderApplicationService.java
│ │ └── impl/
│ │ └── OrderApplicationServiceImpl.java
│ ├── infrastructure/ # Infrastructure 层
│ │ ├── data/
│ │ │ ├── entity/
│ │ │ │ └── OrderPO.java # JPA Entity
│ │ │ ├── repository/
│ │ │ │ └── OrderRepositoryImpl.java
│ │ │ └── mapper/
│ │ │ └── OrderMapper.java # PO ⇄ Domain
│ │ └── messaging/
│ │ └── KafkaEventPublisher.java
│ ├── api/ # API 层
│ │ ├── controller/
│ │ │ └── OrderController.java
│ │ ├── dto/
│ │ │ ├── request/
│ │ │ │ └── CreateOrderRequest.java
│ │ │ └── response/
│ │ │ └── OrderResponse.java
│ │ └── assembler/
│ │ └── OrderDTOAssembler.java
│ └── composition/ # Composition 层
│ └── config/
│ └── OrderModuleConfig.java
└── src/test/java/com/example/order/
├── core/domain/model/OrderTest.java # Domain 单元测试
├── core/application/... # Application Mock 测试
└── infrastructure/... # Infrastructure 集成测试包结构与依赖方向
┌──────────────────────────────────────────┐
│ composition/ │
│ config/OrderModuleConfig.java │ ← 组装所有 Bean
└──────────┬───────────────────────────────┘
│ 依赖所有模块
┌──────┴──────┐
│ │
▼ ▼
┌──────────┐ ┌──────────────┐
│ api/ │ │infrastructure│
│controller│ │ /data │
│ /dto │ │ /messaging │
└────┬─────┘ └───┬──────────┘
│ │
│ │ 实现 Domain 接口
▼ ▼
┌──────────────────────────────┐
│ core/application/ │
│ OrderApplicationService │
└──────────────┬───────────────┘
│ 依赖 Domain
▼
┌──────────────────────────────┐
│ core/domain/ │ ★ 核心(零框架依赖)
│ Order, OrderRepository, │
│ PricingService │
└──────────────────────────────┘依赖规则:所有箭头指向 core/domain,内层不知道外层存在。
Maven 依赖关系
<!-- 单模块,所有依赖在同一个 pom.xml -->
<dependencies>
<!-- Domain 层零框架依赖(纯 Java) -->
<!-- Application 层引入 JSR-330 -->
<dependency>
<groupId>jakarta.inject</groupId>
<artifactId>jakarta.inject-api</artifactId>
</dependency>
<!-- Infrastructure 层引入具体实现 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
<!-- API 层引入 Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>合规检查
| 检查项 | 状态 | 说明 |
|---|---|---|
| Domain 层零框架依赖 | ✅ | 仅使用 java.* 和 jakarta.inject |
| Repository 接口在 Domain | ✅ | OrderRepository 在 core/domain/repository |
| Repository 实现在 Infra | ✅ | OrderRepositoryImpl 在 infrastructure/data |
| Application 层纯编排 | ✅ | OrderApplicationServiceImpl 无业务判断 |
| DTO 仅在 API 层 | ✅ | Request/Response 在 api/dto |
| DI 集中在 composition | ✅ | OrderModuleConfig 装配所有 Bean |
何时选择此结构
- 项目起步,团队小,业务简单
- 后续可能拆分,但当前单模块够用
- 希望用 package 约定模拟模块边界,为未来拆分做准备
Example 07 — 单体复杂规模(Monolith Complex)
单模块多限界上下文。通过顶层包名(订单/商品/用户)划分上下文,每个上下文内部独立应用洋葱分层。适合 3-8 人团队、多个限界上下文但部署为单体。
适用场景
| 条件 | 说明 |
|---|---|
| 团队规模 | 3-8 人 |
| 限界上下文 | 3-5 个(订单、商品、用户、库存、支付) |
| 部署单元 | 1 个 JAR/WAR |
| 数据库 | 1 个 RDBMS(多 Schema) |
| 外部集成 | 5-10 个 |
目录树
ecommerce-platform/
├── pom.xml
├── src/main/java/com/example/ec/
│ ├── shared/ # 共享内核
│ │ ├── domain/
│ │ │ └── model/
│ │ │ ├── Money.java # 共享值对象
│ │ │ └── DomainEvent.java # 基础事件接口
│ │ └── infrastructure/
│ │ └── messaging/
│ │ └── EventPublisher.java
│ │
│ ├── order/ # 订单上下文
│ │ ├── core/domain/
│ │ │ ├── model/
│ │ │ │ ├── Order.java # 聚合根
│ │ │ │ ├── OrderItem.java
│ │ │ │ └── OrderStatus.java
│ │ │ ├── service/
│ │ │ │ └── PricingService.java
│ │ │ ├── repository/
│ │ │ │ └── OrderRepository.java
│ │ │ └── event/
│ │ │ └── OrderPlacedEvent.java # 集成事件(通知其他上下文)
│ │ ├── core/application/
│ │ │ └── service/
│ │ │ ├── PlaceOrderUseCase.java
│ │ │ └── impl/PlaceOrderService.java
│ │ ├── infrastructure/data/
│ │ │ ├── entity/OrderPO.java
│ │ │ ├── repository/OrderRepositoryImpl.java
│ │ │ └── mapper/OrderMapper.java
│ │ ├── api/controller/OrderController.java
│ │ └── composition/config/OrderBoundedContextConfig.java
│ │
│ ├── product/ # 商品上下文
│ │ ├── core/domain/
│ │ │ ├── model/
│ │ │ │ ├── Product.java
│ │ │ │ ├── ProductId.java
│ │ │ │ └── Category.java
│ │ │ ├── repository/
│ │ │ │ └── ProductRepository.java
│ │ │ └── event/
│ │ │ └── ProductCreatedEvent.java
│ │ ├── core/application/
│ │ │ └── service/
│ │ │ └── impl/ProductManagementService.java
│ │ ├── infrastructure/data/...
│ │ ├── api/controller/ProductController.java
│ │ └── composition/config/ProductBoundedContextConfig.java
│ │
│ ├── user/ # 用户上下文
│ │ ├── core/domain/
│ │ │ ├── model/
│ │ │ │ ├── User.java
│ │ │ │ ├── Email.java
│ │ │ │ └── Address.java
│ │ │ ├── repository/
│ │ │ │ └── UserRepository.java
│ │ │ └── event/
│ │ │ └── UserRegisteredEvent.java
│ │ ├── core/application/...
│ │ ├── infrastructure/data/...
│ │ ├── api/controller/UserController.java
│ │ └── composition/config/UserBoundedContextConfig.java
│ │
│ ├── inventory/ # 库存上下文
│ │ ├── core/domain/...
│ │ ├── core/application/...
│ │ ├── infrastructure/...
│ │ └── api/...
│ │
│ └── payment/ # 支付上下文
│ ├── core/domain/...
│ ├── core/application/...
│ ├── infrastructure/...
│ └── api/...
│
└── composition/
└── config/
└── AppConfig.java # 汇总装配所有上下文上下文间依赖方向
┌─────────┐ ★ 跨上下文仅通过 ID 引用和事件通信
│ order │────→ 引用 ProductId, UserId
└────┬────┘
│
┌────▼────┐
│ payment │──→ 引用 OrderId
└────┬────┘
│
┌────▼─────────┐
│ inventory │──→ 引用 ProductId, OrderId
└───────────────┘
┌──────┐
│ user │ ← 被引用,不依赖其他上下文
└──────┘
┌─────────┐
│ product │ ← 被引用,不依赖其他上下文
└─────────┘规则:
- 上下文间仅通过 ID 值对象引用(如
ProductId放在shared/domain) - 跨上下文事务通过领域事件 + 最终一致性
- 每个上下文独立配置自己的 DI(避免循环依赖)
上下文内依赖方向(每个上下文独立)
每个上下文内部:
composition → api / infrastructure → application → domain ★合规检查
| 检查项 | 状态 | 说明 |
|---|---|---|
| 上下文隔离 | ✅ | 每个上下文独立 domain/application/infra/api |
| 跨上下文仅 ID 引用 | ✅ | order 只引用 ProductId,不引用 Product 实体 |
| shared 内核最小化 | ✅ | 仅 Money, DomainEvent,无业务逻辑 |
| 共享事件放 shared | ⚠️ | 集成事件建议各上下文独立定义 |
| 测试隔离 | ✅ | 每个上下文独立测试 |
何时选择此结构
- 业务复杂度高但运维资源有限(只有 1 个部署单元)
- 多个限界上下文需要隔离,但微服务运维成本过高
- 单体内部先做模块化,为未来微服务拆分打基础
- 团队已按上下文分工(order 小团队、product 小团队等)
风险提示
- 共享内核容易膨胀:
shared/中的公共代码需严格审查 - 上下文边界可能被打破:需要 Code Review 防止直接引用对方领域对象
- 数据库耦合:单 Schema 下容易写跨上下文 JOIN,建议多 Schema
Example 08 — 单体多模块规模(Monolith Multi-Module)
Maven/Gradle 多模块单体。将洋葱各层拆分为独立 Maven 模块(domain/app/infra/api/composition),通过模块依赖强制执行分层规则。适合 5-12 人团队、需要编译期强制依赖约束。
适用场景
| 条件 | 说明 |
|---|---|
| 团队规模 | 5-12 人 |
| 限界上下文 | 2-3 个 |
| 部署单元 | 1 个 Spring Boot JAR |
| 数据库 | 1 个 RDBMS |
| 模块数 | 5-15 个 Maven 模块 |
目录树
ecommerce/
├── pom.xml # 父 POM(版本管理)
├── ecommerce-domain/ # ★ Module: domain(零框架依赖)
│ ├── pom.xml # 不依赖 Spring Boot
│ └── src/main/java/com/example/ec/domain/
│ ├── model/
│ │ ├── order/
│ │ │ ├── Order.java # 聚合根
│ │ │ ├── OrderId.java
│ │ │ ├── OrderItem.java
│ │ │ └── OrderStatus.java
│ │ ├── product/
│ │ │ ├── Product.java
│ │ │ └── ProductId.java
│ │ └── shared/
│ │ ├── Money.java
│ │ └── ValueObject.java
│ ├── service/
│ │ └── PricingService.java # 领域服务接口
│ ├── repository/
│ │ ├── OrderRepository.java # 仓储接口
│ │ └── ProductRepository.java
│ └── event/
│ ├── DomainEvent.java
│ └── OrderPlacedEvent.java
│
├── ecommerce-application/ # ★ Module: application(编排层)
│ ├── pom.xml # 仅依赖 domain 模块
│ └── src/main/java/com/example/ec/application/
│ ├── service/
│ │ ├── PlaceOrderUseCase.java # 应用服务接口
│ │ └── ProductQueryService.java
│ ├── command/
│ │ └── PlaceOrderCommand.java
│ └── dto/
│ ├── OrderDTO.java
│ └── ProductDTO.java
│
├── ecommerce-infrastructure/ # ★ Module: infrastructure
│ ├── pom.xml # 依赖 domain + application
│ └── src/main/java/com/example/ec/infra/
│ ├── data/
│ │ ├── entity/
│ │ │ ├── OrderPO.java # JPA Entity
│ │ │ └── ProductPO.java
│ │ ├── repository/
│ │ │ ├── OrderRepositoryImpl.java
│ │ │ └── ProductRepositoryImpl.java
│ │ ├── mapper/
│ │ │ └── EntityDomainMapper.java
│ │ └── config/
│ │ └── JpaConfig.java
│ ├── messaging/
│ │ ├── KafkaConfig.java
│ │ └── KafkaEventPublisher.java
│ └── external/
│ └── PaymentGatewayClient.java
│
├── ecommerce-api-rest/ # ★ Module: API(REST 适配器)
│ ├── pom.xml # 依赖 domain + application + infra
│ └── src/main/java/com/example/ec/api/
│ ├── controller/
│ │ ├── OrderController.java
│ │ └── ProductController.java
│ ├── dto/
│ │ ├── request/
│ │ │ └── PlaceOrderRequest.java
│ │ └── response/
│ │ └── OrderResponse.java
│ ├── assembler/
│ │ └── OrderAssembler.java
│ └── advice/
│ └── GlobalExceptionHandler.java
│
├── ecommerce-api-grpc/ # ★ Module: API(gRPC 适配器,可选)
│ ├── pom.xml
│ └── src/main/java/.../
│ └── proto/
│ └── OrderServiceGrpc.java
│
├── ecommerce-api-mq/ # ★ Module: API(MQ 适配器,可选)
│ ├── pom.xml
│ └── src/main/java/.../
│ └── listener/
│ └── OrderEventListener.java
│
└── ecommerce-composition/ # ★ Module: composition(DI 根)
├── pom.xml # 依赖所有模块
└── src/main/java/com/example/ec/
├── CompositionApp.java # Spring Boot 启动类
└── config/
├── OrderModuleConfig.java
├── ProductModuleConfig.java
└── MessagingConfig.java模块依赖图
┌────────────────────────────────────────────┐
│ ecommerce-composition/ │ ← Spring Boot 启动 + DI 组装
└───┬────────┬──────────┬──────────┬─────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐ ┌──────────────┐
│api- │ │api- │ │api- │ │infrastructure│
│rest │ │grpc │ │mq │ │ │
└──┬───┘ └──┬───┘ └──┬───┘ └──────┬───────┘
│ │ │ │
└────────┼────────┴────────────┘
▼
┌──────────────────────┐
│ecommerce-application/│ ← 编排层
└──────────┬───────────┘
│
▼
┌──────────────────────┐
│ ecommerce-domain/ │ ★ 核心(零框架依赖)
└──────────────────────┘关键约束:多模块通过 Maven <dependency> 强制执行:
domain不依赖任何其他模块application仅依赖domaininfrastructure依赖domain,实现其接口api-*依赖domain+application+infrastructurecomposition依赖所有模块
Maven 父 POM 示例
<modules>
<module>ecommerce-domain</module>
<module>ecommerce-application</module>
<module>ecommerce-infrastructure</module>
<module>ecommerce-api-rest</module>
<module>ecommerce-api-grpc</module>
<module>ecommerce-api-mq</module>
<module>ecommerce-composition</module>
</modules>合规检查
| 检查项 | 状态 | 说明 |
|---|---|---|
| domain 模块零 Spring 依赖 | ✅ | pom.xml 中无 spring-boot-starter |
| application 仅依赖 domain | ✅ | Maven 依赖可视 |
| 编译期强制执行分层 | ✅ | 逆向依赖 = 编译失败 |
| API 适配器可插拔 | ✅ | rest/grpc/mq 各自独立模块 |
| DI 集中组装 | ✅ | composition 模块统筹 |
何时选择此结构
- 需要编译期强制执行分层规则(防止团队成员引入逆向依赖)
- 多入口系统:REST + gRPC + MQ 消费者
- 大型团队,需要严格模块边界
- 基础设施可能切换(DB/消息队列/缓存)
风险提示
- 模块过多增加 Maven 构建复杂度
- domain 模块变更 → 需要 rebuild 所有下游模块
- 过度拆分导致每个模块代码量过少
- composition 模块可能成为"大泥球"(所有配置集中)
Example 09 — 微服务简单规模(Microservice Simple)
单个微服务内部应用洋葱架构。每个微服务独立部署,服务间通过 API 调用或消息通信。适合 3-5 人团队负责一个服务。
适用场景
| 条件 | 说明 |
|---|---|
| 团队规模 | 3-5 人(一个微服务团队) |
| 限界上下文 | 1 个 = 1 个微服务 |
| 部署单元 | 独立容器化部署 |
| 数据库 | 每个服务独立 Database |
| 通信方式 | REST/gRPC(同步)+ Kafka(异步) |
目录树
order-service/ # 订单微服务
├── docker-compose.yml # 本地开发环境
├── Dockerfile
├── pom.xml
├── src/main/java/com/example/order/
│ ├── core/
│ │ ├── domain/ # Domain 层
│ │ │ ├── model/
│ │ │ │ ├── Order.java
│ │ │ │ ├── OrderId.java
│ │ │ │ ├── OrderItem.java
│ │ │ │ └── OrderStatus.java
│ │ │ ├── service/
│ │ │ │ └── PricingService.java
│ │ │ ├── repository/
│ │ │ │ └── OrderRepository.java
│ │ │ └── event/
│ │ │ ├── OrderCreatedEvent.java
│ │ │ └── OrderPaidEvent.java
│ │ └── application/ # Application 层
│ │ ├── service/
│ │ │ ├── PlaceOrderUseCase.java
│ │ │ └── impl/PlaceOrderService.java
│ │ ├── command/
│ │ │ └── PlaceOrderCommand.java
│ │ └── dto/
│ │ └── OrderDTO.java
│ ├── infrastructure/ # Infrastructure 层
│ │ ├── data/
│ │ │ ├── entity/OrderPO.java
│ │ │ ├── repository/OrderRepositoryImpl.java
│ │ │ └── mapper/OrderMapper.java
│ │ ├── external/
│ │ │ ├── ProductServiceClient.java # 调用商品服务 API
│ │ │ └── PaymentServiceClient.java # 调用支付服务 API
│ │ ├── messaging/
│ │ │ ├── KafkaConfig.java
│ │ │ ├── OrderEventPublisher.java
│ │ │ └── ProductEventListener.java # 监听商品变更事件
│ │ └── config/
│ │ └── ServiceClientConfig.java
│ ├── api/ # API 层
│ │ ├── controller/
│ │ │ └── OrderController.java
│ │ ├── dto/
│ │ │ ├── request/CreateOrderRequest.java
│ │ │ └── response/OrderResponse.java
│ │ └── assembler/
│ │ └── OrderDTOAssembler.java
│ └── composition/ # Composition 层
│ ├── OrderServiceApplication.java # Spring Boot 入口
│ └── config/
│ └── OrderServiceConfig.java
│
└── src/test/
└── java/com/example/order/
├── core/domain/model/OrderTest.java
├── core/application/PlaceOrderServiceTest.java
└── integration/OrderServiceIT.java微服务间依赖方向
┌──────────────┐ REST/gRPC ┌──────────────┐
│ order-service│ ──────────────────→│product-service│
│ │←─ OrderCreated ─── │ │
└──────┬───────┘ (Kafka) └──────────────┘
│
│ REST/gRPC
▼
┌──────────────┐
│payment-service│
│ │
└──────────────┘
★ 服务间通过 API Contract / Protobuf / Avro Schema 定义接口
★ 服务间仅通过 ID 引用对方的实体
★ 跨服务事务:Saga 模式(编排/协同)服务内部依赖方向
composition → api / infrastructure → application → domain ★
↑
Kafka Event (外部消息) ────────────┘
REST/gRPC Request ─────────────────┘注意:微服务内部的洋葱架构与单体完全相同,外部的分布式通信不影响内部结构。
微服务特有配置
// infrastructure/external/ProductServiceClient.java
@Component
public class ProductServiceClient {
private final RestTemplate restTemplate;
// 调用其他微服务时,Application 层通过 Domain 接口调用
public ProductInfo getProductInfo(ProductId productId) {
return restTemplate.getForObject(
"http://product-service/api/products/{id}",
ProductInfo.class, productId.getValue()
);
}
}
// infrastructure/messaging/ProductEventListener.java
@Component
public class ProductEventListener {
@KafkaListener(topics = "product-events")
public void handleProductEvent(ProductEvent event) {
// 维护本地缓存或投影,避免实时调用
if (event instanceof ProductRemovedEvent) {
// 标记相关订单
}
}
}合规检查
| 检查项 | 状态 | 说明 |
|---|---|---|
| 服务内部洋葱合规 | ✅ | Domain 层零框架依赖 |
| 服务间接口契约 | ✅ | ProductServiceClient 基于 API Contract |
| 数据库独立 | ✅ | 每个服务独立 Database |
| 跨服务事务 Saga | ⚠️ | 需要实现补偿逻辑 |
| 服务发现与负载均衡 | ⚠️ | 基础设施层处理 |
何时选择此结构
- 微服务数量 < 10,每服务团队 < 5 人
- 每服务业务独立性强,变化频率不同
- 需要独立部署和扩缩容
- 团队已有容器化和服务治理经验
Example 10 — 微服务复杂规模(Microservice Complex)
一个微服务内部包含多个限界上下文,每个上下文在服务内独立应用洋葱分层。适合业务关联紧密无法拆分的场景。
适用场景
| 条件 | 说明 |
|---|---|
| 团队规模 | 6-12 人(一个服务团队) |
| 限界上下文 | 2-4 个(同一服务内) |
| 部署单元 | 独立容器化部署 |
| 数据库 | 每个上下文独立 Schema 或 Table Prefix |
| 通信方式 | 内部直接方法调用,外部 REST/gRPC + Kafka |
目录树
fulfillment-service/ # 履约微服务(含订单、库存、物流)
├── pom.xml
├── Dockerfile
├── src/main/java/com/example/fulfillment/
│ ├── shared/ # 共享内核(同服务内)
│ │ └── domain/
│ │ ├── Money.java
│ │ ├── DomainEvent.java
│ │ └── Address.java
│ │
│ ├── order/ # 上下文 1: 订单
│ │ ├── core/
│ │ │ ├── domain/
│ │ │ │ ├── model/
│ │ │ │ │ ├── Order.java
│ │ │ │ │ ├── OrderId.java
│ │ │ │ │ └── OrderStatus.java
│ │ │ │ ├── repository/
│ │ │ │ │ └── OrderRepository.java
│ │ │ │ └── event/
│ │ │ │ └── OrderPaidEvent.java
│ │ │ └── application/
│ │ │ └── service/
│ │ │ ├── PlaceOrderUseCase.java
│ │ │ └── impl/PlaceOrderService.java
│ │ ├── infrastructure/data/
│ │ │ ├── entity/OrderPO.java
│ │ │ └── repository/OrderRepositoryImpl.java
│ │ ├── api/controller/OrderController.java
│ │ └── composition/config/OrderContextConfig.java
│ │
│ ├── inventory/ # 上下文 2: 库存
│ │ ├── core/
│ │ │ ├── domain/
│ │ │ │ ├── model/
│ │ │ │ │ ├── Inventory.java
│ │ │ │ │ └── StockLevel.java
│ │ │ │ ├── repository/
│ │ │ │ │ └── InventoryRepository.java
│ │ │ │ └── event/
│ │ │ │ └── StockReservedEvent.java
│ │ │ └── application/
│ │ │ └── service/
│ │ │ └── impl/ReserveStockService.java
│ │ ├── infrastructure/data/
│ │ │ └── repository/InventoryRepositoryImpl.java
│ │ ├── api/controller/InventoryController.java
│ │ └── composition/config/InventoryContextConfig.java
│ │
│ └── logistics/ # 上下文 3: 物流
│ ├── core/
│ │ ├── domain/
│ │ │ ├── model/
│ │ │ │ ├── Shipment.java
│ │ │ │ └── TrackingNumber.java
│ │ │ ├── repository/
│ │ │ │ └── ShipmentRepository.java
│ │ │ └── event/
│ │ │ └── ShipmentCreatedEvent.java
│ │ └── application/
│ │ └── service/
│ │ └── impl/CreateShipmentService.java
│ ├── infrastructure/data/
│ │ └── repository/ShipmentRepositoryImpl.java
│ ├── api/controller/LogisticsController.java
│ └── composition/config/LogisticsContextConfig.java
│
├── composition/
│ ├── FulfillmentServiceApplication.java # 服务入口
│ └── config/
│ └── ServiceContextConfig.java # 汇总所有上下文
│
└── src/test/
└── java/com/example/fulfillment/
├── order/core/domain/model/OrderTest.java
├── inventory/core/domain/model/InventoryTest.java
├── logistics/core/domain/model/ShipmentTest.java
└── integration/
├── OrderIntegrationIT.java
└── FulfillmentSagaIT.java # 跨上下文 Saga 集成测试服务内上下文依赖方向
┌──────────────────────────────────────────────┐
│ composition/ │ ← 汇总装配
└─────┬─────────┬──────────┬───────────────────┘
│ │ │
┌───▼───┐ ┌───▼────┐ ┌──▼──────┐
│ order │ │inventory│ │logistics│
│ ctx │ │ ctx │ │ ctx │
└───┬───┘ └───┬────┘ └──┬──────┘
│ │ │
│ 同步调用 │
└────────┼───────────┘
│
通过 Application Service 协调
(不可直接调用对方 Domain)规则:
- 同一服务内的上下文通过 Application Service 协调(同步)
- 对外提供统一的 API 入口
- 每个上下文独立 Database Schema
- 跨上下文事务用 Saga(比纯微服务更简单,可本地加锁)
合规检查
| 检查项 | 状态 | 说明 |
|---|---|---|
| 上下文内部洋葱合规 | ✅ | 每个 ctx 独立 domain/app/infra |
| 上下文间仅通过 AppService 协调 | ✅ | 不跨上下文直接访问 Domain |
| 共享内核最小 | ✅ | shared 仅 Money/Address |
| DB 按上下文隔离 | ✅ | Schema per context |
何时选择此结构
- 多个业务模块关联紧密(如订单-库存-履约不可拆分)
- 需要独立部署但拆分为多个微服务会引入过多网络开销
- 过渡期架构:先做内部分层,后续再拆分为独立微服务
风险提示
- 服务内上下文边界容易模糊(比跨服务更难约束)
- 共享数据库可能产生隐式耦合
- 服务过大可能成为新单体
Example 11 — 微服务多模块规模(Microservice Multi-Module)
单个微服务内部采用 Maven 多模块拆分洋葱各层。编译期强制执行分层约束,适合对代码质量要求严格的大团队。
适用场景
| 条件 | 说明 |
|---|---|
| 团队规模 | 4-8 人(一个服务团队) |
| 限界上下文 | 1 个(= 1 个微服务) |
| 部署单元 | 独立容器化部署 |
| 模块数 | 4-7 个 Maven 模块 / 微服务 |
| 代码规范 | 编译期强制分层 |
目录树
order-service/
├── pom.xml # 父 POM
├── order-domain/ # Module 1: 领域层
│ ├── pom.xml
│ └── src/main/java/.../
│ ├── model/
│ │ ├── Order.java
│ │ ├── OrderId.java
│ │ └── OrderItem.java
│ ├── repository/
│ │ └── OrderRepository.java # 仓储接口
│ ├── service/
│ │ └── PricingService.java
│ ├── event/
│ │ ├── OrderCreatedEvent.java
│ │ └── OrderPaidEvent.java
│ └── external/
│ └── ProductService.java # 外部服务接口
│
├── order-application/ # Module 2: 应用层
│ ├── pom.xml # 仅依赖 order-domain
│ └── src/main/java/.../
│ ├── service/
│ │ ├── PlaceOrderUseCase.java
│ │ └── impl/PlaceOrderService.java
│ ├── command/
│ │ └── PlaceOrderCommand.java
│ ├── query/
│ │ └── OrderQueryService.java
│ └── dto/
│ └── OrderDTO.java
│
├── order-infrastructure/ # Module 3: 基础设施层
│ ├── pom.xml # 依赖 domain + application
│ └── src/main/java/.../
│ ├── data/
│ │ ├── entity/OrderPO.java
│ │ ├── repository/OrderRepositoryImpl.java
│ │ ├── mapper/OrderMapper.java
│ │ └── config/JpaConfig.java
│ ├── messaging/
│ │ ├── KafkaConfig.java
│ │ └── KafkaOrderEventPublisher.java
│ └── client/
│ └── ProductServiceClient.java
│
├── order-api-rest/ # Module 4: REST API
│ ├── pom.xml # 依赖 domain + app + infra
│ └── src/main/java/.../
│ ├── controller/
│ │ └── OrderController.java
│ ├── dto/
│ │ ├── request/CreateOrderRequest.java
│ │ └── response/OrderResponse.java
│ ├── assembler/
│ │ └── OrderAssembler.java
│ └── advice/
│ └── OrderExceptionHandler.java
│
├── order-api-grpc/ # Module 5: gRPC API(可选)
│ ├── pom.xml
│ └── src/main/java/.../
│ └── service/
│ └── OrderGrpcService.java
│
├── order-api-mq/ # Module 6: MQ Consumer API(可选)
│ ├── pom.xml
│ └── src/main/java/.../
│ └── listener/
│ └── PaymentEventListener.java
│
└── order-composition/ # Module 7: DI 根
├── pom.xml # 依赖所有模块
└── src/main/java/.../
├── OrderServiceApplication.java # Spring Boot 入口
└── config/
├── DomainConfig.java
├── InfrastructureConfig.java
└── ApiConfig.java模块依赖图
┌────────────────────────────────────────────┐
│ order-composition │ ← 启动入口 + DI 根
└──┬───────┬─────────┬───────────┬───────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌─────┐ ┌─────┐ ┌─────┐ ┌────────────────┐
│rest │ │grpc │ │ mq │ │ infrastructure │
│api │ │api │ │api │ │ │
└──┬──┘ └──┬──┘ └──┬──┘ └───────┬────────┘
│ │ │ │
└───────┴───────┴────────────┘
│
▼
┌────────────────────────────┐
│ order-application │ ← 仅依赖 domain
└─────────────┬──────────────┘
│
▼
┌────────────────────────────┐
│ order-domain │ ★ 零框架依赖
└────────────────────────────┘Maven 依赖约束示例
<!-- order-domain/pom.xml -->
<dependencies>
<!-- 零框架依赖:无 spring-boot-starter -->
</dependencies>
<!-- order-application/pom.xml -->
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>order-domain</artifactId>
</dependency>
<!-- 允许 JSR-330 -->
</dependencies>
<!-- order-infrastructure/pom.xml -->
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>order-domain</artifactId>
</dependency>
<dependency>
<groupId>com.example</groupId>
<artifactId>order-application</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.kafka</groupId>
<artifactId>spring-kafka</artifactId>
</dependency>
</dependencies>合规检查
| 检查项 | 状态 | 说明 |
|---|---|---|
| domain 模块零 Spring 依赖 | ✅ | pom.xml 无 spring-boot-starter |
| 编译期分层强制 | ✅ | 逆向依赖 = 编译失败 |
| API 适配器独立模块 | ✅ | rest/grpc/mq 可独立部署/修改 |
| composition 集中 DI | ✅ | 单一模块装配 |
何时选择此结构
- 微服务代码量大(>100 个类),需要模块拆分
- 多 API 入口(同时支持 REST、gRPC 和 MQ)
- 需要编译期预防分层违规
- 团队需要严格的代码组织约定
风险提示
- Maven 模块过多 → 构建时间增加
- 每个微服务都多模块 → 仓库数量 × 模块数 = 维护成本爆炸
- 仅在代码量足够大的服务中使用(<50 个类的服务不需要)
Example 12 — 微服务复杂多模块规模(Microservice Complex Multi-Module)
最复杂的场景:一个微服务包含多个限界上下文,每个上下文内部采用 Maven 多模块洋葱分层。适合大型企业级微服务,上下文紧密耦合无法拆分。
适用场景
| 条件 | 说明 |
|---|---|
| 团队规模 | 10-25 人(一个大服务团队) |
| 限界上下文 | 3-5 个(同一服务内) |
| 部署单元 | 独立容器化部署 |
| Maven 模块 | 15-30 个 |
| 数据库 | 每个上下文独立 Database,且独立 Schema |
| 代码量 | 500+ 个类 |
目录树
fulfillment-service/
├── pom.xml # 父 POM(聚合所有上下文)
│
├── shared/ # 共享内核(独立模块)
│ ├── pom.xml
│ └── src/main/java/.../
│ ├── domain/
│ │ ├── Money.java
│ │ ├── DomainEvent.java
│ │ └── Address.java
│ └── annotation/
│ └── ValueObject.java
│
├── order/ # 上下文 1: 订单
│ ├── pom.xml # 上下文父 POM
│ ├── order-domain/
│ │ ├── pom.xml
│ │ └── src/main/java/.../
│ │ ├── model/Order.java, OrderId.java, OrderStatus.java
│ │ ├── repository/OrderRepository.java
│ │ ├── service/PricingService.java
│ │ └── event/OrderPlacedEvent.java, OrderPaidEvent.java
│ ├── order-application/
│ │ ├── pom.xml
│ │ └── src/main/java/.../
│ │ ├── service/PlaceOrderUseCase.java, impl/PlaceOrderService.java
│ │ ├── command/PlaceOrderCommand.java
│ │ └── dto/OrderDTO.java
│ ├── order-infrastructure/
│ │ ├── pom.xml
│ │ └── src/main/java/.../
│ │ ├── data/entity/OrderPO.java
│ │ ├── data/repository/OrderRepositoryImpl.java
│ │ ├── data/mapper/OrderMapper.java
│ │ └── messaging/OrderEventPublisher.java
│ ├── order-api-rest/
│ │ ├── pom.xml
│ │ └── src/main/java/.../
│ │ ├── controller/OrderController.java
│ │ ├── dto/request/CreateOrderRequest.java
│ │ ├── dto/response/OrderResponse.java
│ │ └── assembler/OrderAssembler.java
│ └── order-composition/
│ ├── pom.xml
│ └── src/main/java/.../
│ └── config/OrderContextConfig.java
│
├── inventory/ # 上下文 2: 库存(同上结构)
│ ├── pom.xml
│ ├── inventory-domain/
│ ├── inventory-application/
│ ├── inventory-infrastructure/
│ ├── inventory-api-rest/
│ └── inventory-composition/
│
├── logistics/ # 上下文 3: 物流(同上结构)
│ ├── pom.xml
│ ├── logistics-domain/
│ ├── logistics-application/
│ ├── logistics-infrastructure/
│ ├── logistics-api-rest/
│ └── logistics-composition/
│
├── payment/ # 上下文 4: 支付(同上结构)
│ ├── pom.xml
│ ├── payment-domain/
│ ├── payment-application/
│ ├── payment-infrastructure/
│ ├── payment-api-rest/
│ └── payment-composition/
│
└── service-composition/ # 服务级 DI 根
├── pom.xml # 依赖所有上下文
└── src/main/java/.../
├── FulfillmentServiceApplication.java # Spring Boot 入口
└── config/
├── AggregatedServiceConfig.java
└── CrossContextSagaConfig.java完整依赖图
┌──────────────────────────────────────────────────────────────────┐
│ service-composition/ │
│ FulfillmentServiceApplication + DI 根 │
└──┬───────────┬────────────┬────────────┬─────────────┬───────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────────┐
│order │ │inven-│ │logis-│ │paym- │ │ service │
│comp. │ │tory │ │tics │ │ent │ │ shared │
│ │ │comp. │ │comp. │ │comp. │ │ module │
└──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ └────┬─────┘
│ │ │ │ │
│ ←── 每个上下文内部洋葱依赖 ──→ │ │
│ │ │ │ │
▼ ▼ ▼ ▼ │
┌───────────────────────────────────┐ │
│ 各上下文 domain(零框架依赖) │◄───────────────┘
└───────────────────────────────────┘三层依赖隔离: 1. 上下文内:composition → api/infra → application → domain(经典洋葱) 2. 上下文间:仅通过 Application Service + Domain Event(不直接引用对方 Domain) 3. 服务间:通过 Kafka / REST / gRPC(API Gateway 统一对外)
上下文间协调示例
// order-application/service/impl/PlaceOrderService.java
public class PlaceOrderService implements PlaceOrderUseCase {
private final OrderRepository orderRepo;
// 跨上下文仅通过 Application 接口协调
private final ReserveInventoryUseCase inventory; // 库存 Application 接口
private final CreateShipmentUseCase logistics; // 物流 Application 接口
@Override
@Transactional
public OrderDTO execute(PlaceOrderCommand cmd) {
// 1. 订单领域逻辑
Order order = Order.create(...);
orderRepo.save(order);
// 2. 协调库存上下文(通过 Application Service,不跨 Domain)
inventory.reserve(new ReserveCommand(order.getId(), cmd.getItems()));
// 3. 协调物流上下文
logistics.createShipment(new CreateShipmentCommand(order.getId()));
return OrderAssembler.toDTO(order);
}
}Maven 模块统计
| 上下文 | 模块数 | 说明 |
|---|---|---|
| shared | 1 | 共享模型 |
| order | 5 | domain/app/infra/api-rest/composition |
| inventory | 5 | 同上 |
| logistics | 5 | 同上 |
| payment | 5 | 同上 |
| service-composition | 1 | 汇总装配 |
| 总计 | 22 | modules |
合规检查
| 检查项 | 状态 | 说明 |
|---|---|---|
| 所有 domain 模块零 Spring 依赖 | ✅ | 22 个模块中的 4 个 domain 模块 |
| 上下文间间接协调 | ✅ | 仅通过 Application 接口 |
| 编译期分层强制 | ✅ | 逆向依赖 = 编译失败 |
| 服务内 Saga | ✅ | 跨上下文事务本地协调 |
| 测试隔离 | ✅ | 每个上下文独立测试套件 |
何时选择此结构
- 仅当:业务逻辑 500+ 类,3+ 个紧密耦合的上下文,团队 > 10 人
- 企业级微服务,需要极强的代码组织和编译期约束
- 上下文紧密耦合但需要独立演进(不同团队负责不同上下文)
风险提示
- 22 个 Maven 模块 → 构建时间长,CI/CD 复杂度高
- 过度设计风险:大多数项目用 Example 09(简单微服务)或 Example 10(复杂微服务)即可
- 版本管理困难:一个 shared 模块变更需要 rebuild 所有上游
- 运维成本:模块边界需要持续 Code Review 维护
- 强烈建议:先考虑拆分上下文为独立微服务(降低复杂度)
01 — Domain Layer: Domain Model Design
领域层(Domain Layer)是洋葱架构的核心,零外部依赖,承载所有业务规则。
职责
- 定义业务实体、值对象、聚合根
- 实现充血模型的业务方法
- 定义 Repository 接口(纯抽象,无实现)
- 定义领域事件
- 定义领域服务
代码模板
Aggregate Root(聚合根)
// core/domain/model/order/Order.java
public class Order extends AggregateRoot<OrderId> {
private OrderId id;
private CustomerId customerId;
private Money totalAmount;
private List<OrderItem> items;
private OrderStatus status;
private LocalDateTime createdAt;
// 私有构造器,通过工厂方法创建
private Order(OrderId id, CustomerId customerId) {
this.id = id;
this.customerId = customerId;
this.items = new ArrayList<>();
this.status = OrderStatus.DRAFT;
this.createdAt = LocalDateTime.now();
}
// 工厂方法
public static Order create(OrderId id, CustomerId customerId) {
Order order = new Order(id, customerId);
order.addDomainEvent(new OrderCreatedEvent(id, customerId));
return order;
}
// 充血业务方法
public void addItem(ProductId productId, Money price, int quantity) {
if (quantity <= 0) {
throw new DomainException("数量必须大于0");
}
if (this.status != OrderStatus.DRAFT) {
throw new DomainException("只能向草稿订单添加商品");
}
this.items.add(new OrderItem(productId, price, quantity));
this.totalAmount = calculateTotal();
}
public void submit() {
if (this.items.isEmpty()) {
throw new DomainException("不能提交空订单");
}
if (this.status != OrderStatus.DRAFT) {
throw new DomainException("只能提交草稿订单");
}
this.status = OrderStatus.SUBMITTED;
this.addDomainEvent(new OrderSubmittedEvent(this.id));
}
public void pay(PaymentGateway gateway) {
if (this.status != OrderStatus.SUBMITTED) {
throw new DomainException("当前状态不可支付");
}
gateway.charge(this.id, this.totalAmount);
this.status = OrderStatus.PAID;
this.addDomainEvent(new OrderPaidEvent(this.id, this.totalAmount));
}
private Money calculateTotal() {
return items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
// getter — 只读暴露
public OrderId getId() { return id; }
public Money getTotalAmount() { return totalAmount; }
public OrderStatus getStatus() { return status; }
public List<OrderItem> getItems() { return Collections.unmodifiableList(items); }
}Value Object(值对象)
// core/domain/model/shared/Money.java
public final class Money implements ValueObject {
private final BigDecimal amount;
private final Currency currency;
public static final Money ZERO = new Money(BigDecimal.ZERO, Currency.getInstance("CNY"));
private Money(BigDecimal amount, Currency currency) {
this.amount = amount;
this.currency = currency;
}
public static Money of(BigDecimal amount, String currencyCode) {
return new Money(amount, Currency.getInstance(currencyCode));
}
public Money add(Money other) {
if (!this.currency.equals(other.currency)) {
throw new IllegalArgumentException("货币单位不匹配");
}
return new Money(this.amount.add(other.amount), this.currency);
}
public Money multiply(int times) {
return new Money(this.amount.multiply(BigDecimal.valueOf(times)), this.currency);
}
// 值对象必须实现 equals/hashCode
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Money money = (Money) o;
return amount.compareTo(money.amount) == 0 && currency.equals(money.currency);
}
@Override
public int hashCode() {
return Objects.hash(amount.stripTrailingZeros(), currency);
}
}Repository Interface(仓储接口)
// core/domain/repository/OrderRepository.java
public interface OrderRepository {
Optional<Order> findById(OrderId id);
void save(Order order);
void delete(OrderId id);
Page<Order> findByCustomerId(CustomerId customerId, Pageable pageable);
}Domain Service(领域服务)
// core/domain/service/OrderPricingService.java
public class OrderPricingService {
private final DiscountPolicy discountPolicy;
public OrderPricingService(DiscountPolicy discountPolicy) {
this.discountPolicy = discountPolicy;
}
public Money calculateFinalPrice(Order order) {
Money subtotal = order.getTotalAmount();
Discount discount = discountPolicy.applyTo(order);
return subtotal.subtract(discount.getAmount());
}
}Domain Event(领域事件)
// core/domain/event/OrderPaidEvent.java
public class OrderPaidEvent extends DomainEvent {
private final OrderId orderId;
private final Money paidAmount;
private final LocalDateTime paidAt;
public OrderPaidEvent(OrderId orderId, Money paidAmount) {
this.orderId = orderId;
this.paidAmount = paidAmount;
this.paidAt = LocalDateTime.now();
}
public OrderId getOrderId() { return orderId; }
public Money getPaidAmount() { return paidAmount; }
public LocalDateTime getPaidAt() { return paidAt; }
}规范检查清单
- [ ] 无任何框架 import(Spring/JPA/MyBatis)
- [ ] 实体采用充血模型(业务方法在实体内部)
- [ ] 值对象不可变(final 字段,无 setter)
- [ ] Repository 接口定义在 Domain 层
- [ ] 聚合根负责一致性边界
- [ ] 跨聚合通过 ID 引用(非对象引用)
- [ ] 关键业务操作发布领域事件
- [ ] 领域异常使用自定义异常(非 RuntimeException 裸抛)
02 — Application Layer: Interfaces & Orchestration
应用层(Application Layer)位于 Domain 外围,负责用例编排、事务管理、跨聚合协调。
职责
- 定义应用服务接口(契约)
- 实现应用服务(编排领域对象,不包含业务逻辑)
- 管理事务边界(
@Transactional) - 发布领域事件
- 调用外部微服务
核心原则
Domain 层:业务逻辑(if/else,规则校验)
Application 层:编排逻辑(先调 A,再调 B,最后调 C)
— 不包含业务逻辑判断代码模板
Application Service Interface(应用服务接口)
// core/application/service/OrderApplicationService.java
public interface OrderApplicationService {
OrderDTO createOrder(CreateOrderCommand command);
void payOrder(PayOrderCommand command);
void cancelOrder(CancelOrderCommand command);
OrderDTO getOrder(String orderId);
}Application Service Implementation(应用服务实现)
// core/application/service/OrderApplicationServiceImpl.java
public class OrderApplicationServiceImpl implements OrderApplicationService {
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
private final EventPublisher eventPublisher;
private final OrderAssembler orderAssembler;
public OrderApplicationServiceImpl(
OrderRepository orderRepository,
PaymentGateway paymentGateway,
EventPublisher eventPublisher,
OrderAssembler orderAssembler) {
this.orderRepository = orderRepository;
this.paymentGateway = paymentGateway;
this.eventPublisher = eventPublisher;
this.orderAssembler = orderAssembler;
}
@Override
@Transactional
public OrderDTO createOrder(CreateOrderCommand command) {
// 1. 创建领域对象
OrderId orderId = OrderId.generate();
Order order = Order.create(orderId, new CustomerId(command.getCustomerId()));
// 2. 添加商品(调用 Domain 方法)
for (CreateOrderCommand.Item item : command.getItems()) {
order.addItem(
new ProductId(item.getProductId()),
Money.of(item.getUnitPrice(), "CNY"),
item.getQuantity()
);
}
// 3. 提交订单(调用 Domain 方法)
order.submit();
// 4. 持久化
orderRepository.save(order);
// 5. 发布领域事件
order.getDomainEvents().forEach(eventPublisher::publish);
// 6. 返回 DTO(不暴露 Domain 对象)
return orderAssembler.toDTO(order);
}
@Override
@Transactional
public void payOrder(PayOrderCommand command) {
Order order = orderRepository.findById(new OrderId(command.getOrderId()))
.orElseThrow(() -> new OrderNotFoundException(command.getOrderId()));
// 调用 Domain 层的支付方法(含业务规则)
order.pay(paymentGateway);
orderRepository.save(order);
order.getDomainEvents().forEach(eventPublisher::publish);
}
@Override
@Transactional
public void cancelOrder(CancelOrderCommand command) {
Order order = orderRepository.findById(new OrderId(command.getOrderId()))
.orElseThrow(() -> new OrderNotFoundException(command.getOrderId()));
order.cancel();
orderRepository.save(order);
order.getDomainEvents().forEach(eventPublisher::publish);
}
@Override
@Transactional(readOnly = true)
public OrderDTO getOrder(String orderId) {
Order order = orderRepository.findById(new OrderId(orderId))
.orElseThrow(() -> new OrderNotFoundException(orderId));
return orderAssembler.toDTO(order);
}
}Command / DTO(命令对象)
// core/application/dto/CreateOrderCommand.java
public class CreateOrderCommand {
private String customerId;
private List<Item> items;
public static class Item {
private String productId;
private BigDecimal unitPrice;
private int quantity;
// getter/setter...
}
// getter/setter...
}
// core/application/dto/OrderDTO.java
public class OrderDTO {
private String orderId;
private String customerId;
private String status;
private List<OrderItemDTO> items;
private BigDecimal totalAmount;
private LocalDateTime createdAt;
// getter/setter...
}Event Publisher Interface(事件发布接口)
// core/application/event/EventPublisher.java
public interface EventPublisher {
void publish(DomainEvent event);
void publishAll(List<DomainEvent> events);
}规范检查清单
- [ ] Application Service 不含 if/else 业务逻辑
- [ ] 事务注解在 Application 层
- [ ] 返回值是 DTO,不暴露 Domain 实体
- [ ] 所有依赖通过构造器注入
- [ ] 跨聚合操作通过 Application 编排
- [ ] 领域事件在事务提交后发布
03 — Infrastructure Layer: Implementation Details
基础设施层(Infrastructure Layer)是洋葱架构的最外层,实现 Domain 层定义的所有接口。
职责
- 实现 Repository 接口(JPA / MyBatis / JDBC)
- 实现 EventPublisher 接口(Kafka / RabbitMQ / Redis)
- 实现外部 API 客户端(HTTP / gRPC)
- 定义 PO(Persistent Object)和 Mapper
- 基础设施配置(数据源、连接池、MQ 连接)
代码模板
Repository Implementation(仓储实现 — JPA)
// infrastructure/data/repository/OrderRepositoryImpl.java
@Repository
public class OrderRepositoryImpl implements OrderRepository {
private final JpaOrderRepository jpaRepo; // Spring Data JPA
private final OrderMapper mapper; // PO ↔ Domain
public OrderRepositoryImpl(JpaOrderRepository jpaRepo, OrderMapper mapper) {
this.jpaRepo = jpaRepo;
this.mapper = mapper;
}
@Override
public Optional<Order> findById(OrderId id) {
return jpaRepo.findById(id.getValue())
.map(mapper::toDomain);
}
@Override
public void save(Order order) {
OrderPO po = mapper.toPO(order);
jpaRepo.save(po);
}
@Override
public void delete(OrderId id) {
jpaRepo.deleteById(id.getValue());
}
@Override
public Page<Order> findByCustomerId(CustomerId customerId, Pageable pageable) {
return jpaRepo.findByCustomerId(customerId.getValue(), pageable)
.map(mapper::toDomain);
}
}
// Spring Data JPA 接口
// infrastructure/data/repository/JpaOrderRepository.java
public interface JpaOrderRepository extends JpaRepository<OrderPO, String> {
Page<OrderPO> findByCustomerId(String customerId, Pageable pageable);
}PO(Persistent Object)
// infrastructure/data/entity/OrderPO.java
@Entity
@Table(name = "orders")
public class OrderPO {
@Id
private String id;
private String customerId;
@Enumerated(EnumType.STRING)
private OrderStatus status;
private BigDecimal totalAmount;
private String currency;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
@OneToMany(mappedBy = "order", cascade = CascadeType.ALL, orphanRemoval = true)
private List<OrderItemPO> items;
// getter/setter...
}
// infrastructure/data/entity/OrderItemPO.java
@Entity
@Table(name = "order_items")
public class OrderItemPO {
@Id
private String id;
private String orderId;
private String productId;
private BigDecimal unitPrice;
private String currency;
private int quantity;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "orderId", insertable = false, updatable = false)
private OrderPO order;
// getter/setter...
}Mapper(PO ↔ Domain)
// infrastructure/data/mapper/OrderMapper.java
@Component
public class OrderMapper {
public Order toDomain(OrderPO po) {
if (po == null) return null;
Order order = Order.create(
new OrderId(po.getId()),
new CustomerId(po.getCustomerId())
);
// 通过反射或内部方法恢复状态(不含领域事件)
order.setStatus(po.getStatus());
order.setTotalAmount(Money.of(po.getTotalAmount(), po.getCurrency()));
po.getItems().forEach(itemPo ->
order.addItem(
new ProductId(itemPo.getProductId()),
Money.of(itemPo.getUnitPrice(), itemPo.getCurrency()),
itemPo.getQuantity()
)
);
return order;
}
public OrderPO toPO(Order order) {
OrderPO po = new OrderPO();
po.setId(order.getId().getValue());
po.setCustomerId(order.getCustomerId().getValue());
po.setStatus(order.getStatus());
po.setTotalAmount(order.getTotalAmount().getAmount());
po.setCurrency(order.getTotalAmount().getCurrency().getCurrencyCode());
po.setCreatedAt(order.getCreatedAt());
// items mapping...
return po;
}
}Event Publisher 实现(Kafka)
// infrastructure/messaging/KafkaEventPublisher.java
@Component
public class KafkaEventPublisher implements EventPublisher {
private final KafkaTemplate<String, Object> kafkaTemplate;
private final ObjectMapper objectMapper;
public KafkaEventPublisher(KafkaTemplate<String, Object> kafkaTemplate,
ObjectMapper objectMapper) {
this.kafkaTemplate = kafkaTemplate;
this.objectMapper = objectMapper;
}
@Override
public void publish(DomainEvent event) {
String topic = event.getClass().getSimpleName();
String payload = objectMapper.writeValueAsString(event);
kafkaTemplate.send(topic, event.getAggregateId().getValue(), payload);
}
@Override
public void publishAll(List<DomainEvent> events) {
events.forEach(this::publish);
}
}外部 API 客户端
// infrastructure/external/PaymentGatewayImpl.java
@Component
public class PaymentGatewayImpl implements PaymentGateway {
private final RestTemplate restTemplate;
public PaymentGatewayImpl(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@Override
public void charge(OrderId orderId, Money amount) {
PaymentRequest request = new PaymentRequest(
orderId.getValue(),
amount.getAmount(),
amount.getCurrency().getCurrencyCode()
);
ResponseEntity<PaymentResponse> response = restTemplate.postForEntity(
"https://payment-service/api/charges",
request,
PaymentResponse.class
);
if (!response.getStatusCode().is2xxSuccessful()) {
throw new PaymentFailedException("支付调用失败: " + response.getStatusCode());
}
}
}规范检查清单
- [ ] 所有 Repository 实现 Domain 层定义的接口
- [ ] PO 类只放在 Infrastructure,不进 Domain
- [ ] Mapper 负责 PO ↔ Domain 双向转换
- [ ] 外部 API 调用包装在 Gateway 实现中
- [ ] 基础设施配置集中管理
- [ ] 支持多种实现切换(如 JPA ↔ MyBatis)
04 — API Layer: Adapters & Controllers
API 层(Adapter Layer)是系统对外暴露的边界,负责协议转换、数据校验、异常处理。
职责
- REST Controller:接收 HTTP 请求,调用 Application 服务
- DTO:请求/响应数据结构定义
- Assembler:DTO ↔ Domain 数据转换
- Middleware:拦截器、过滤器、全局异常处理
- Swagger:API 文档
代码模板
REST Controller
// api/controller/OrderController.java
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
private final OrderApplicationService orderAppService;
public OrderController(OrderApplicationService orderAppService) {
this.orderAppService = orderAppService;
}
@PostMapping
public ResponseEntity<ApiResponse<OrderResponse>> createOrder(
@Valid @RequestBody CreateOrderRequest request) {
CreateOrderCommand command = CreateOrderRequestAssembler.toCommand(request);
OrderDTO orderDTO = orderAppService.createOrder(command);
OrderResponse response = OrderResponseAssembler.toResponse(orderDTO);
return ResponseEntity.status(HttpStatus.CREATED)
.body(ApiResponse.success(response));
}
@PostMapping("/{orderId}/pay")
public ResponseEntity<ApiResponse<Void>> payOrder(
@PathVariable String orderId) {
orderAppService.payOrder(new PayOrderCommand(orderId));
return ResponseEntity.ok(ApiResponse.success(null));
}
@GetMapping("/{orderId}")
public ResponseEntity<ApiResponse<OrderResponse>> getOrder(
@PathVariable String orderId) {
OrderDTO orderDTO = orderAppService.getOrder(orderId);
OrderResponse response = OrderResponseAssembler.toResponse(orderDTO);
return ResponseEntity.ok(ApiResponse.success(response));
}
@PostMapping("/{orderId}/cancel")
public ResponseEntity<ApiResponse<Void>> cancelOrder(
@PathVariable String orderId) {
orderAppService.cancelOrder(new CancelOrderCommand(orderId));
return ResponseEntity.ok(ApiResponse.success(null));
}
}Request / Response DTO
// api/dto/request/CreateOrderRequest.java
@Data
public class CreateOrderRequest {
@NotBlank(message = "客户ID不能为空")
private String customerId;
@NotEmpty(message = "订单项不能为空")
@Valid
private List<Item> items;
@Data
public static class Item {
@NotBlank
private String productId;
@NotNull
@Positive
private BigDecimal unitPrice;
@Min(1)
private int quantity;
}
}
// api/dto/response/OrderResponse.java
@Data
public class OrderResponse {
private String orderId;
private String customerId;
private String status;
private List<OrderItemResponse> items;
private BigDecimal totalAmount;
private LocalDateTime createdAt;
@Data
public static class OrderItemResponse {
private String productId;
private BigDecimal unitPrice;
private int quantity;
private BigDecimal subtotal;
}
}Assembler(DTO ↔ Command)
// api/assembler/CreateOrderRequestAssembler.java
public class CreateOrderRequestAssembler {
public static CreateOrderCommand toCommand(CreateOrderRequest request) {
CreateOrderCommand command = new CreateOrderCommand();
command.setCustomerId(request.getCustomerId());
command.setItems(request.getItems().stream()
.map(item -> {
CreateOrderCommand.Item cmdItem = new CreateOrderCommand.Item();
cmdItem.setProductId(item.getProductId());
cmdItem.setUnitPrice(item.getUnitPrice());
cmdItem.setQuantity(item.getQuantity());
return cmdItem;
})
.collect(Collectors.toList())
);
return command;
}
}
// api/assembler/OrderResponseAssembler.java
public class OrderResponseAssembler {
public static OrderResponse toResponse(OrderDTO dto) {
OrderResponse response = new OrderResponse();
response.setOrderId(dto.getOrderId());
response.setCustomerId(dto.getCustomerId());
response.setStatus(dto.getStatus());
response.setTotalAmount(dto.getTotalAmount());
response.setCreatedAt(dto.getCreatedAt());
response.setItems(dto.getItems().stream()
.map(itemDto -> {
OrderResponse.OrderItemResponse itemResp = new OrderResponse.OrderItemResponse();
itemResp.setProductId(itemDto.getProductId());
itemResp.setUnitPrice(itemDto.getUnitPrice());
itemResp.setQuantity(itemDto.getQuantity());
itemResp.setSubtotal(itemDto.getSubtotal());
return itemResp;
})
.collect(Collectors.toList())
);
return response;
}
}统一响应格式
// api/dto/response/ApiResponse.java
@Data
@AllArgsConstructor
public class ApiResponse<T> {
private int code;
private String message;
private T data;
public static <T> ApiResponse<T> success(T data) {
return new ApiResponse<>(200, "success", data);
}
public static <T> ApiResponse<T> error(int code, String message) {
return new ApiResponse<>(code, message, null);
}
}全局异常处理
// api/middleware/GlobalExceptionHandler.java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(DomainException.class)
public ResponseEntity<ApiResponse<Void>> handleDomainException(DomainException ex) {
return ResponseEntity.badRequest()
.body(ApiResponse.error(400, ex.getMessage()));
}
@ExceptionHandler(OrderNotFoundException.class)
public ResponseEntity<ApiResponse<Void>> handleNotFound(OrderNotFoundException ex) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.error(404, ex.getMessage()));
}
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ApiResponse<Void>> handleValidation(
ConstraintViolationException ex) {
String msg = ex.getConstraintViolations().stream()
.map(v -> v.getPropertyPath() + ": " + v.getMessage())
.collect(Collectors.joining(", "));
return ResponseEntity.badRequest()
.body(ApiResponse.error(400, msg));
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ApiResponse<Void>> handleGeneral(Exception ex) {
log.error("Unexpected error", ex);
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error(500, "内部服务异常"));
}
}规范检查清单
- [ ] Controller 不含业务逻辑(无 if/else 业务判断)
- [ ] 输入校验在 DTO 上使用
@Valid+ JSR-303 - [ ] 响应格式统一(ApiResponse 包装)
- [ ] Assembler 完成 DTO ↔ Domain 转换
- [ ] 全局异常处理覆盖所有场景
- [ ] Swagger/OpenAPI 文档生成