DDD分层架构的实践指南:从理论到落地

📅 发布时间:2026/7/8 11:25:03 👁️ 浏览次数:
DDD分层架构的实践指南:从理论到落地
1. 为什么你的三层架构总在“打架”DDD分层来破局做了这么多年开发我见过太多项目在初期跑得飞快一到业务复杂期就陷入泥潭。新加一个字段要改五六个地方改一个业务规则前端、后端、数据库全得动。团队每天都在“救火”代码库像一坨纠缠不清的意大利面没人敢轻易重构。这背后的根源往往是我们习惯的三层架构表现层、业务逻辑层、数据访问层在面对复杂业务时力不从心了。三层架构把技术职责分得很清楚但业务逻辑却像一锅粥被随意泼洒在Service层的各个角落。这时候领域驱动设计DDD的分层架构就登场了。它不是一个凭空创造的新玩意儿而是对传统分层架构的一次“精装修”。核心思想就一句话按业务职责而不是技术职责来分层。这听起来有点抽象我打个比方。传统的三层架构就像按“切菜、炒菜、洗碗”来分工的厨房乍一看没问题。但当你需要做一道复杂的“佛跳墙”时你会发现“炒菜”这个环节里既有人在处理海参的泡发领域知识又有人在控制火候应用流程还有人惦记着用什么锅技术细节全混在一起自然容易乱。DDD分层架构则把厨房重新规划为用户点餐台用户接口层、厨师长指挥台应用层、专业厨师工作站领域层和食材仓库/厨具库基础层。厨师工作站只关心“海参该怎么发才Q弹”、“鲍汁该怎么熬才醇厚”这些核心的烹饪技艺领域逻辑厨师长指挥台负责协调“先发海参再熬高汤最后勾芡”的流程应用流程点餐台只管把客人的需求翻译成订单仓库则提供统一的食材和厨具。这样即使明天客人要点西餐新业务我们只需要调整指挥台的流程并启用西餐厨师新领域模型而不会影响到中餐厨师的日常工作核心领域稳定。所以DDD分层架构的目标非常直接让复杂的业务系统变得有序让核心的业务逻辑独立且稳定让变化各就各位减少不必要的耦合。它特别适合那些业务逻辑复杂、需求变更频繁、需要长期迭代和维护的中大型系统。如果你和你的团队正在为系统的“腐化”速度而头疼感觉每次改动都如履薄冰那么花点时间理解并实践DDD分层很可能是一笔非常划算的投资。2. 庖丁解牛DDD四层架构的职责与协作理解了为什么需要DDD分层我们来看看它具体长什么样。经典的DDD分层架构分为四层从外到内或从上到下依次是用户接口层、应用层、领域层和基础层。注意这个“上下”指的是依赖方向上层可以调用下层下层不应感知上层。接下来我结合一个具体的例子——“电商订单履约系统”——来拆解每一层的职责你就明白它们是怎么干活的了。2.1 用户接口层系统的“前台接待”这一层是系统对外的门面负责和各种各样的“用户”打交道。这里的用户不只是真人还包括其他系统、定时任务、测试脚本等。它的核心工作就两项接收指令和返回结果。接收指令把外部千奇百怪的协议HTTP, RPC, MQ消息CLI命令转换成应用层能理解的参数对象。比如一个创建订单的HTTP请求过来用户接口层的Controller会解析JSON验证基本格式然后组装成一个CreateOrderCommand命令对象丢给应用层。它自己不处理任何业务逻辑就是个翻译官。返回结果把应用层返回的领域对象或DTO转换成外部需要的格式JSON、XML、HTML页面等。同样它不关心数据怎么来的只负责包装和呈现。关键点这一层要薄。避免在这里做业务判断比如“如果用户是VIP就打折”这属于业务规则应该下沉。它的稳定依赖于外部渠道所以变化可能频繁但正因为薄改起来也容易。2.2 应用层业务流程的“指挥家”应用层是我认为最容易被人误解的一层。很多人会不自觉地把业务逻辑写在这里把它又变成了一个“大Service”。切记应用层不应该包含核心业务规则它只是一个协调者和编排者。它的职责包括服务编排一个用户操作用例往往涉及多个领域对象的协作。比如“下单”这个用例应用服务会依次调用库存服务.检查(商品ID)-订单领域服务.创建订单(参数)-支付网关.发起支付(订单)-订单.标记为待支付()-发送订单创建成功领域事件。它像导演一样告诉演员领域对象什么时候该上场但不管演员具体怎么表演。事务控制通常一个用例对应一个应用服务方法这个方法边界就是事务的边界。你可以在这里用Transactional注解来保证这一系列领域操作要么全成功要么全失败。权限校验检查当前用户是否有权限执行这个用例。发布领域事件当领域层发生了一些重要事情如订单已创建应用层负责将这些事件发布到消息中间件通知其他感兴趣的系统。关键点应用层是用例驱动的。每个公开的方法都对应一个明确的用户操作。它很薄主要代码是方法调用序列。它的存在保护了领域层让领域层无需关心“谁在调用我”以及“调用我的前后要做什么”。2.3 领域层业务核心的“发动机”这是DDD的心脏是公司核心竞争力的代码体现。所有重要的业务规则、逻辑、状态都沉淀在这里。领域层主要包括以下几种模型实体有唯一标识和生命周期的对象。比如订单Order、用户User。实体是充血模型它不仅有数据更有行为。例如Order实体应该有calculateTotalAmount()计算总金额、applyCoupon(Coupon coupon)应用优惠券等方法。这些方法封装了属于订单本身的业务规则。值对象没有唯一标识通过其属性值来识别的对象。比如地址Address、价格Money。值对象通常不可变且可以用于简化实体的设计。聚合与聚合根一组关系紧密的实体和值对象的集合聚合根是入口。比如订单Order是聚合根它包含订单项OrderItem实体和收货地址Address值对象。外部只能通过聚合根Order来修改其内部对象这保证了数据变更的一致性。领域服务当某个操作或逻辑不属于任何一个实体/值对象时就需要领域服务。它处理跨多个聚合的业务逻辑。比如资金转账服务TransferService它需要操作“源账户”和“目标账户”两个聚合确保原子性。领域服务是无状态的。领域事件表示领域中发生的、其他部分可能感兴趣的事情。如OrderCreatedEvent。它通常由聚合根在自身状态改变后产生。关键点领域层应该是最纯粹、最稳定的一层。它不依赖任何外部框架Spring、数据库MySQL、中间件Redis。它的代码应该像数学公式一样只表达业务是什么而不关心技术如何实现。这种独立性是通过依赖倒置实现的领域层定义接口如OrderRepository基础层去实现它。2.4 基础层默默付出的“后勤部”基础层为上面所有层提供技术支持是所有层的底层支撑。它包含数据库访问的实现实现领域层定义的Repository接口。外部API客户端如调用支付网关、短信服务。消息队列的生产者/消费者实现。缓存、文件存储等操作的封装。通用的工具类。关键点基础层是变化的“吸收层”。今天用MySQL明天换PostgreSQL今天用RabbitMQ明天换Kafka。这些变化应该只影响基础层的具体实现类而应用层和领域层的代码应该毫不知情。这就是依赖倒置原则的魅力。四层协作流程图 一个典型的“用户下单”请求会这样流动用户通过HTTP发送下单请求。用户接口层的OrderController接收请求解析并验证数据格式组装成CreateOrderCommand。OrderController调用应用层的OrderApplicationService.createOrder(command)方法。应用层服务开始工作 a. 权限校验。 b. 开启事务。 c. 调用领域层的InventoryChecker领域服务检查库存。 d. 调用Order聚合根工厂方法创建订单聚合内部包含业务规则校验如金额计算。 e. 调用OrderRepository.save(order)接口注意是接口持久化订单。 f. 调用PaymentService发起支付这是另一个领域服务或外部适配器。 g. 发布OrderCreatedEvent事件。 h. 提交事务。基础层在幕后提供支持 a.OrderRepositoryImpl在基础层实际执行INSERT INTO orders...SQL语句。 b.PaymentServiceAdapter在基础层实际调用支付宝/微信支付的HTTP API。 c.DomainEventPublisherImpl在基础层将事件发送到RabbitMQ。应用层将保存后的Order实体或一个特化的OrderDTO返回给用户接口层。用户接口层将对象转换为JSON返回给前端。3. 从理论到代码一个订单域的落地实战光说不练假把式我们用一个简化版的“电商订单”子域来演示如何将上述分层转化为实际代码。假设我们有一个核心业务规则创建订单时需要校验库存、计算总价支持优惠券并生成订单号。3.1 领域层实现构建业务内核首先我们定义领域层的核心——Order聚合根。注意这里没有任何Spring注解只有纯粹的Java代码和业务逻辑。// 领域层 - 实体订单项 public class OrderItem { private final String productId; private final String productName; private final Money price; // Money是值对象包含金额和货币 private final Integer quantity; // 构造函数、getter... public Money calculateSubTotal() { return price.multiply(quantity); } } // 领域层 - 聚合根订单 public class Order { private String orderId; // 唯一标识 private String userId; private OrderStatus status; private Money totalAmount; private Address shippingAddress; // 值对象 private ListOrderItem items; private Coupon coupon; // 值对象可能为null // 核心业务逻辑创建订单工厂方法也可以是单独的领域服务 public static Order create(String userId, ListOrderItem items, Coupon coupon, Address address) { // 1. 校验参数基本的业务规则 Objects.requireNonNull(items, 订单项不能为空); if (items.isEmpty()) { throw new IllegalArgumentException(订单项不能为空); } Order order new Order(); order.orderId OrderIdGenerator.generate(); // 领域服务生成唯一ID order.userId userId; order.status OrderStatus.CREATED; order.items new ArrayList(items); order.shippingAddress address; order.coupon coupon; // 2. 计算总金额核心业务逻辑封装在实体内部 order.calculateTotalAmount(); // 3. 应用优惠券如果有 if (coupon ! null) { order.applyCoupon(coupon); } // 4. 生成领域事件 order.registerEvent(new OrderCreatedEvent(order.getOrderId(), order.getTotalAmount())); return order; } // 计算总金额私有方法内部使用 private void calculateTotalAmount() { this.totalAmount this.items.stream() .map(OrderItem::calculateSubTotal) .reduce(Money.ZERO, Money::add); } // 应用优惠券 private void applyCoupon(Coupon coupon) { // 优惠券使用规则校验例如是否在有效期是否满足最低消费 if (!coupon.isValidFor(this.totalAmount)) { throw new CouponNotApplicableException(优惠券不适用); } this.totalAmount coupon.applyDiscount(this.totalAmount); } // 支付成功另一个业务行为 public void markAsPaid() { if (this.status ! OrderStatus.CREATED) { throw new IllegalOrderStateException(只有已创建订单才能支付); } this.status OrderStatus.PAID; this.registerEvent(new OrderPaidEvent(this.orderId)); } // getters... }接下来定义领域层需要的接口。领域层只依赖这些接口不关心实现。// 领域层 - 仓储接口 public interface OrderRepository { Order findById(String orderId); void save(Order order); // ... 其他查询方法注意应使用领域术语避免数据库术语如findByUserIdAndStatus } // 领域层 - 领域服务接口例如库存检查可能涉及商品聚合 public interface InventoryService { boolean isStockSufficient(String productId, Integer quantity); }3.2 应用层实现编排用例流程应用层负责协调。我们创建一个应用服务对应“创建订单”这个用例。// 应用层 - 应用服务 Service // Spring注解在这里是合适的因为应用层是框架的入口点之一 Transactional public class OrderApplicationService { Autowired private OrderRepository orderRepository; // 依赖接口 Autowired private InventoryService inventoryService; // 依赖接口 Autowired private DomainEventPublisher eventPublisher; public OrderDTO createOrder(CreateOrderCommand command) { // 1. 参数校验简单的、与业务无关的校验 // 2. 调用领域服务进行库存检查这是一个跨聚合的业务逻辑 for (OrderItemCommand item : command.getItems()) { if (!inventoryService.isStockSufficient(item.getProductId(), item.getQuantity())) { throw new InsufficientStockException(item.getProductId()); } } // 3. 将Command转换为领域对象这部分可以放在一个OrderFactory中 ListOrderItem orderItems command.getItems().stream() .map(cmd - new OrderItem(cmd.getProductId(), cmd.getProductName(), new Money(cmd.getPrice()), cmd.getQuantity())) .collect(Collectors.toList()); Coupon coupon command.getCouponCode() ! null ? new Coupon(command.getCouponCode()) : null; Address address new Address(command.getProvince(), command.getCity(), command.getDetail()); // 4. 调用领域模型的核心逻辑工厂方法或领域服务 Order newOrder Order.create(command.getUserId(), orderItems, coupon, address); // 5. 持久化聚合 orderRepository.save(newOrder); // 6. 发布领域事件通常在事务提交后异步发布这里简化 newOrder.getDomainEvents().forEach(eventPublisher::publish); newOrder.clearDomainEvents(); // 7. 返回DTO避免将领域实体直接暴露给外层 return OrderDTO.from(newOrder); } }3.3 用户接口层与基础层对接世界用户接口层很简单就是一个Spring MVC的Controller。// 用户接口层 - REST控制器 RestController RequestMapping(/api/orders) public class OrderController { Autowired private OrderApplicationService orderAppService; PostMapping public ResponseEntityOrderDTO createOrder(RequestBody Valid CreateOrderRequest request) { // 1. 将HTTP请求对象转换为应用层Command CreateOrderCommand command new CreateOrderCommand( request.getUserId(), request.getItems().stream() .map(reqItem - new OrderItemCommand(reqItem.getProductId(), reqItem.getProductName(), reqItem.getPrice(), reqItem.getQuantity())) .collect(Collectors.toList()), request.getCouponCode(), request.getShippingAddress() ); // 2. 调用应用服务 OrderDTO orderDTO orderAppService.createOrder(command); // 3. 返回HTTP响应 return ResponseEntity.ok(orderDTO); } }基础层则负责实现领域层定义的接口。这里以OrderRepository的实现为例。// 基础层 - 仓储实现 Repository public class OrderRepositoryImpl implements OrderRepository { Autowired private OrderJpaRepository jpaRepository; // 使用Spring Data JPA Override public Order findById(String orderId) { OrderDO orderDO jpaRepository.findById(orderId).orElseThrow(() - new OrderNotFoundException(orderId)); // 使用一个转换器或Mapper将数据对象DO转换为领域对象 return OrderDataConverter.toEntity(orderDO); } Override public void save(Order order) { OrderDO orderDO OrderDataConverter.toDO(order); jpaRepository.save(orderDO); } }这里OrderDO是一个JPA实体它只关心数据库映射没有任何业务方法。OrderDataConverter负责在领域对象Order和数据对象OrderDO之间进行转换。这层转换非常重要它彻底解耦了领域模型和持久化细节。4. 避坑指南实践中常见的“坑”与解决方案在实际项目中落地DDD分层我踩过不少坑也见过很多团队踩坑。下面分享几个最常见的挑战和我们的应对之策。4.1 坑应用层过“胖”领域层过“瘦”这是最常见的反模式。由于习惯使然开发者容易把所有的业务逻辑都写在应用服务里导致应用服务变成上帝类而领域实体退化成只有getter/setter的“贫血模型”。解决方案追问“这是谁的行为”每当要写一段业务逻辑时问自己这个行为是哪个实体或值对象固有的职责比如“计算订单总价”这显然是Order自己的事就应该写成Order.calculateTotalAmount()。遵循“告诉不要询问”原则应用层应该告诉领域对象“做什么”而不是询问领域对象的数据然后自己计算。错误做法appService.getOrderItems(); appService.calculateTotal(items);。正确做法order.calculateTotal()。设立代码审查关卡在团队内建立规范强制要求领域实体必须包含行为并在CR时重点检查应用服务的“胖瘦”。4.2 坑层与层之间对象随意传递经常看到有人把数据库实体OrderDO直接传到应用层甚至用户接口层或者把前端传来的OrderRequest对象直接传给领域层。这会导致层与层之间紧密耦合一方的变化会直接波及另一方。解决方案严格使用DTO/Command进行层间通信。用户接口层 - 应用层使用XXXCommand、XXXQuery和XXXDTO。Command表示写操作指令Query表示读操作查询DTO是返回的数据传输对象。应用层 - 领域层传递领域对象实体、值对象或原始参数。应用层负责将Command转换为领域对象。领域层 - 基础层通过Repository接口交互基础层负责领域对象与数据对象DO的转换。这样当数据库表结构变化时只需修改基础层的转换器和DO当API接口变化时只需修改用户接口层的Request/Response和对应的Command/DTO。领域层和应用层稳如泰山。4.3 坑领域服务滥用实体变成“哑巴”另一个极端是创建了大量的领域服务而实体内部空空如也。领域服务应该是处理跨聚合或执行复杂操作如调用外部系统的。如果一个逻辑明显属于某个实体就应该放在实体内部。如何区分实体方法操作主要影响该实体自身的状态逻辑只涉及该实体自身的属性。如order.applyCoupon(coupon)。领域服务操作涉及多个不同聚合的协作。如transferService.transfer(fromAccount, toAccount, amount)它需要修改两个Account聚合。或者操作需要依赖外部系统如notificationService.notifyUser(user, message)。4.4 坑基础设施依赖渗透到领域层在领域层里看到Autowired、Entity注解或者直接使用RedisTemplate、JdbcTemplate这都是严重的依赖倒置违规。领域层的编译时依赖里只应该有JDK和你自己的领域模块。解决方案依赖倒置原则DIP领域层定义需要的接口如OrderRepository,MessageQueuePublisher放在领域模块内。基础设施层实现在独立的基础设施模块中提供这些接口的具体实现如OrderRepositoryImpl使用MyBatisMessageQueuePublisherImpl使用RabbitMQ。依赖注入在应用层或通过Spring的配置类将基础设施层的实现实例注入到领域服务或应用服务中。这样领域层只知道接口完全不知道实现细节。4.5 坑聚合设计过大或过小聚合是事务和一致性的边界。设计得太大如把用户、所有订单、购物车都放在一个聚合里会导致并发修改冲突严重性能低下。设计得太小如每个订单项都是一个聚合又无法保证业务规则的一致性比如修改订单项价格必须同步更新订单总价这需要跨聚合变得复杂。设计原则一致性边界聚合内强一致聚合间最终一致。问问自己哪些对象必须同时变更才能保持业务一致把它们放在一个聚合里。高频修改分离如果两个对象虽然相关但经常被独立修改考虑拆分成两个聚合。例如Product商品信息和Inventory库存经常是分开修改的适合作为两个聚合通过产品ID关联。通过ID引用聚合之间通过聚合根的ID来引用而不是直接持有对象引用。这明确了聚合的边界。5. 进阶思考架构的演进与防腐层设计当你和团队熟练掌握了基本的四层架构后可能会遇到一些更复杂的情况比如如何优雅地集成外部系统或遗留系统。这时防腐层的概念就变得至关重要。5.1 什么是防腐层想象一下你的核心领域是一个健康的有机体而外部系统可能携带“病毒”糟糕的数据模型、不稳定的接口、不同的业务语义。直接让核心领域依赖外部系统就相当于让身体直接接触病毒会导致“腐败”核心领域被污染变得复杂和不稳定。防腐层就是在这之间建立的一个隔离层。它的职责是转换将外部系统的模型API响应、消息格式转换为你自己领域上下文内的模型。隔离将外部系统的变化接口升级、服务下线屏蔽在核心领域之外。适配模拟外部系统的行为便于测试和降级。5.2 如何在DDD分层中实现防腐层通常防腐层被实现在应用层与基础层之间更具体地说是基础层的一部分但为应用层服务。场景我们的订单系统需要调用一个外部的“风控系统”来检查订单是否有欺诈风险。风控系统返回一个复杂的JSON包含riskLevel,score,tags等字段。没有防腐层的坏味道应用服务直接调用一个RiskControlClient然后解析JSON判断riskLevel HIGH。这样风控系统的数据结构和业务语义什么是HIGH就泄漏到了我们的核心应用逻辑中。有防腐层的实现在领域层或应用层定义抽象的网关接口使用你的领域语言。// 领域层 - 防腐层接口 public interface RiskControlGateway { RiskAssessment assessOrderRisk(Order order); } // 领域层 - 自己的领域模型 public class RiskAssessment { private final boolean isHighRisk; private final String reason; // ... 构造函数getter }在基础层实现这个接口在这里处理与外部系统的通信和模型转换。// 基础层 - 防腐层实现 Component public class RiskControlGatewayImpl implements RiskControlGateway { Autowired private RiskControlApiClient apiClient; // 一个普通的HTTP客户端 Override public RiskAssessment assessOrderRisk(Order order) { // 1. 将领域对象转换为外部系统需要的请求体 ExternalRiskRequest request convertToExternalRequest(order); // 2. 调用外部系统 ExternalRiskResponse response apiClient.checkRisk(request); // 3. **关键步骤**将外部响应转换为我们自己的领域模型 return convertToRiskAssessment(response); } private RiskAssessment convertToRiskAssessment(ExternalRiskResponse response) { // 在这里封装所有的转换逻辑和业务规则映射 // 例如外部系统返回riskLevel: A我们认为这是高风险 boolean isHighRisk A.equals(response.getRiskLevel()) || response.getScore() 90; String reason isHighRisk ? 风控评分过高 : 正常; return new RiskAssessment(isHighRisk, reason); } }这样一来无论风控系统外部接口如何变化字段名改了、枚举值变了或者甚至未来我们换了一家风控服务商需要修改的只有RiskControlGatewayImpl这个实现类。我们的应用服务和领域模型完全不受影响。这就是防腐层的威力——它保护了核心领域的纯洁性与稳定性。5.3 架构的持续演进最后要记住没有银弹。DDD分层架构是一个优秀的指导思想但不是必须严格遵守的教条。在实际项目中你可以根据团队规模和项目阶段进行调整。比如在项目初期可以适当简化将应用层和领域层合并但依然要保持充血模型快速验证业务。当系统复杂到一定程度再清晰地剥离出应用层进行流程编排。关键不在于形式上的四层而在于你是否通过这种架构思想达到了分离关注点、降低耦合、让核心业务逻辑清晰可见且易于测试的目的。当你发现修改功能时影响的范围是可控的当新人加入时他能很快找到业务逻辑所在当需要替换某个技术组件时你感到信心十足——那么你的DDD实践就已经走在成功的路上了。