从单体到可演进:一套可落地的 Java 架构分层与稳定性设计

从单体到可演进:一套可落地的 Java 架构分层与稳定性设计

很多团队做 Java 项目做到中后期,会遇到同一种“结构性疲劳”:功能还能迭代,但改动越来越慢;线上小问题越来越多;排查越来越依赖“老人经验”;新同学接手需要很久才能摸清调用链。这个阶段,问题往往不在某个具体 bug,而在架构没有把“变化”关进笼子里。

这篇文章用一个可落地的角度讲 Java 架构:分层、边界、稳定性与可演进。不堆概念,重点放在你如何把项目从“能跑”变成“能长期跑”。


1)先定边界:不要让 Controller 直接驱动一切

很多代码长这样:Controller 接参数 → 调 Service → Service 里拼装 DTO、做校验、写库、发消息、调第三方。短期快,长期烂。

更可控的做法是:把系统切成四个层

  • API 层(Controller):只做协议转换、入参校验、鉴权、返回组装
  • Application 层(用例/编排):描述业务流程(下单=锁库存+创建订单+扣券+发消息)
  • Domain 层(领域模型):封装业务规则(订单状态流转、金额计算、校验规则)
  • Infrastructure 层(基础设施):数据库、缓存、MQ、第三方接口

核心价值:变化最频繁的部分(流程编排)隔离在 Application,规则稳定的部分在 Domain,外部依赖在 Infrastructure。这样改需求时,你不必改到数据库实现、也不必把规则散落在各处。


2)用“用例服务”承载流程,把事务边界收紧

下单这种流程,不应该散落在多个 Service 里互相调用。建议建一个 PlaceOrderUseCase 作为应用层入口,把流程统一放在一个地方,同时明确事务边界。

@Service
public class PlaceOrderUseCase {

private final OrderRepository orderRepository;
private final InventoryService inventoryService;
private final PaymentGateway paymentGateway;
private final DomainEventPublisher eventPublisher;

public PlaceOrderUseCase(OrderRepository orderRepository,
                         InventoryService inventoryService,
                         PaymentGateway paymentGateway,
                         DomainEventPublisher eventPublisher) {
    this.orderRepository = orderRepository;
    this.inventoryService = inventoryService;
    this.paymentGateway = paymentGateway;
    this.eventPublisher = eventPublisher;
}

@Transactional
public PlaceOrderResult execute(PlaceOrderCommand cmd) {

    // 1) 领域校验(规则放 Domain)
    Order order = Order.create(cmd.getUserId(), cmd.getItems());

    // 2) 库存锁定(外部依赖)
    inventoryService.reserve(order.getOrderId(), cmd.getItems());

    // 3) 持久化(仓储抽象)
    orderRepository.save(order);

    // 4) 支付编排(也可异步)
    paymentGateway.prePay(order.getOrderId(), order.getPayAmount());

    // 5) 事件发布(保证最终一致性)
    eventPublisher.publish(order.pullDomainEvents());

    return new PlaceOrderResult(order.getOrderId(), "CREATED");
}

}

关键点:

  • @Transactional 放在用例服务上,避免事务跨层漂移
  • 领域规则由 Order.create() 承担,避免散落在 if-else
  • 基础设施依赖用接口抽象(Repository / Gateway / Publisher)


3)领域模型要“有脾气”:状态流转别写在 Service 里

最常见的坏味道:订单状态在 Service 里随意改,缺少规则,导致线上出现“已取消还能支付”“已完成还能退款”等异常。

把状态流转收进领域对象,让它自己拒绝非法状态。

public class Order {

private final Long orderId;
private OrderStatus status;
private Money payAmount;

private Order(Long orderId, OrderStatus status, Money payAmount) {
    this.orderId = orderId;
    this.status = status;
    this.payAmount = payAmount;
}

public static Order create(Long userId, List<OrderItem> items) {
    // 规则:items 不能为空、金额计算等
    if (items == null || items.isEmpty()) {
        throw new IllegalArgumentException("items cannot be empty");
    }
    Money amount = Money.sum(items);
    return new Order(IdGenerator.nextId(), OrderStatus.CREATED, amount);
}

public void markPaid() {
    if (status != OrderStatus.CREATED) {
        throw new IllegalStateException("only CREATED can be PAID");
    }
    this.status = OrderStatus.PAID;
}

public void cancel() {
    if (status == OrderStatus.PAID || status == OrderStatus.COMPLETED) {
        throw new IllegalStateException("paid/completed order cannot cancel directly");
    }
    this.status = OrderStatus.CANCELLED;
}

public Long getOrderId() { return orderId; }
public Money getPayAmount() { return payAmount; }

}

这样做的收益是:所有规则集中、可测试、可复用。Service 不再到处写“状态判断”,减少 bug 密度。


4)稳定性:用“幂等 + 重试 + 事件”对抗不确定性

线上系统不稳定的根源往往不是数据库,而是调用链:支付、库存、物流、短信、消息队列。网络抖动、超时、重复回调都很正常。架构必须假设“不确定性一定会发生”。

幂等:用业务唯一键保护重复请求

public class IdempotencyService {

private final IdempotencyRepository repo;

public IdempotencyService(IdempotencyRepository repo) {
    this.repo = repo;
}

public <T> T run(String key, Supplier<T> action) {
    if (repo.exists(key)) {
        return repo.getResult(key);
    }
    T result = action.get();
    repo.save(key, result);
    return result;
}

}

使用时:把“支付回调ID / 下单请求ID”作为 key,避免重复执行核心逻辑。

Outbox 事件:先落库再发 MQ,避免丢消息

@Entity
public class OutboxEvent {
    @Id
    private Long id;
    private String type;
    private String payload;
    private LocalDateTime createdAt;
    private boolean published;
}

事务内写订单 + 写 outbox;事务外由定时任务/后台线程发布并标记 published。 它的价值在于:保证“写库成功”与“事件发出”之间不会撕裂


5)可观测性:日志要能串起来,不要只会 println

一套能跑的架构,必须能被定位。建议统一三件事:

  • TraceId:每个请求贯穿全链路(日志、MQ、第三方)
  • 结构化日志:关键字段可检索(orderId、userId、costMs)
  • 指标与告警:成功率、P95 延迟、异常类型分布

示例:用 MDC 统一注入 TraceId

@Component
public class TraceIdFilter extends OncePerRequestFilter {

@Override
protected void doFilterInternal(HttpServletRequest request,
                                HttpServletResponse response,
                                FilterChain filterChain) throws IOException, ServletException {
    String traceId = Optional.ofNullable(request.getHeader("X-Trace-Id"))
                             .orElse(UUID.randomUUID().toString());
    MDC.put("traceId", traceId);
    try {
        filterChain.doFilter(request, response);
    } finally {
        MDC.remove("traceId");
    }
}

}

日志格式中带上 %X{traceId},排查问题时你会感谢自己。


结语:架构不是“上微服务”,而是把变化管理起来

很多人把“架构升级”等同于“拆微服务”。但真正的架构能力,是把变化关进可控边界,让团队在不确定的需求下仍能稳定迭代。

你可以从最小的一步开始: 先把“流程编排”抽到应用层,把“规则校验”收进领域对象,把“外部依赖”接口化。再逐步补上幂等、事件、观测。 当这些基础打稳后,你再决定要不要拆服务,会更理性、更安全,也更不容易踩坑。

评论 0