DDD领域驱动设计实践_保险

项目背景:安康保险核心系统重构

公司原有的保险核心系统是一个庞大的单体架构,新保险产品上线慢、核保逻辑错综复杂、理赔处理效率低下。我们决定采用DDD对核心系统进行重构。

第一步:战略设计 - 理解保险关键业务场景

1. 事件风暴与统一语言

我们与精算师、核保员、理赔员、销售团队进行多次工作坊,提炼出核心领域事件

  • 保险产品已创建

  • 投保申请已提交

  • 投保申请已核保(可能通过、拒保、加费)

  • 保单已生效

  • 保费已支付

  • 理赔申请已提交

  • 理赔案件已调查

  • 理赔已结算(赔付或拒赔)

  • 保单已终止

统一语言是关键:

从核心领域事件中,提炼出统一语言。

  • 投保单(Proposal): 客户的投保申请,尚未成为正式合同。

  • 保单(Policy): 生效的保险合同,是核心实体。

  • 被保险人(Insured Person): 保障对象,注意与投保人(Policyholder)可能不同。

  • 核保(Underwriting): 风险评估过程,决定是否承保及以什么条件承保。

  • 理赔(Claim): 保险事故发生后,客户提出的赔付请求。

  • 保险责任(Coverage): 保单提供的具体保障范围,如住院医疗、门诊手术等。一个保单可以有多个责任。

2. 划定限界上下文(Bounded Context)

通过分析,我们识别出几个核心的上下文边界:

  • 产品上下文(Product Context) : 负责定义和配置保险产品。如"安康百万医疗险2024版"。包含责任定义、保费计算规则、等待期、免赔额等。它关心"卖什么"。

  • 投保与保单上下文(Proposal & Policy Context) : 核心领域。处理从投保、核保到保单生效、批改的全生命周期。它关心"合同是什么状态"。

  • 理赔上下文(Claim Context) : 处理理赔申请、调查、理算和结算。它关心"出了事怎么办"。

  • 支付上下文(Payment Context) : 负责保费的收取和赔款的支付,与外部支付网关交互。它关心"钱的进出"。

3. 上下文映射(Context Map)
  • 产品上下文 -> 投保与保单上下文: 遵奉者关系。投保上下文需要严格遵循产品上下文定义的规则,产品上下文是权威来源。

  • 投保与保单上下文 -> 理赔上下文: 客户-供应商关系。理赔上下文需要从保单上下文获取保单信息、有效责任来核实理赔资格。

  • 投保与保单上下文/理赔上下文 -> 支付上下文: 客户-供应商关系。它们发起支付和赔付指令。

第二步:战术设计 - 构建解决方案模型(领域模型)

我们聚焦于最复杂的 投保与保单上下文理赔上下文

1. 在 投保与保单上下文 内部:

这是整个系统的核心。

  1. 实体(Entity)

    • Proposal(投保单): 有proposalId,状态如待核保

    • Policy(保单): 聚合根,有policyNo,状态如保障中已终止

    • InsuredPerson(被保险人): 健康信息、职业等信息在此。

  2. 值对象(Value Object)

    • Address(地址)

    • CoverageDetail(责任详情): 从产品上下文复制过来,包括责任类型保额免赔额等。这是一个重要的防腐措施,保证了即使原始产品停售,已生效保单的保障内容也不变。

    • Period(期限): 封装起保日期和终保日期。

  3. 聚合根(Aggregate Root)

    • Policy 是明确的聚合根。所有对保单的修改(如地址变更、受益人修改)都必须通过Policy对象进行。
  4. 领域服务(Domain Service)

    • UnderwritingService(核保服务): 核保是核心领域逻辑,它非常复杂。

      • 输入:Proposal(包含健康告知)、UnderwritingRules(核保规则,来自产品上下文)

      • 输出:UnderwritingResult(核保结果:标准体、加费、除外责任、拒保)

  5. 领域事件(Domain Event)

    • PolicyIssuedEvent(保单已签发事件)

    • PolicyLapsedEvent(保单已失效事件)

java 复制代码
// 聚合根 - 保单
public class Policy {
    private PolicyId id;
    private String policyNo;
    private List<CoverageDetail> coverages;
    private Period validPeriod;
    private PolicyStatus status;

    // 批改:增加保额
    public void increaseSumAssured(CoverageType type, Money newAmount) {
        // 1. 业务规则:只有在保障中的保单才能批改
        if (this.status != PolicyStatus.IN_FORCE) {
            throw new IllegalPolicyOperationException("Only in-force policy can be endorsed.");
        }
        // 2. 找到对应责任并修改
        CoverageDetail coverage = findCoverageByType(type);
        coverage.setSumAssured(newAmount);
        // 3. 可能触发领域事件,如需要重新核保等
    }
}

// 领域服务 - 核保
@Service
public class UnderwritingService {
    public UnderwritingResult underwrite(Proposal proposal, UnderwritingRules rules) {
        // 1. 年龄规则
        if (!rules.isAgeEligible(proposal.getInsuredPerson().getAge())) {
            return UnderwritingResult.rejected("年龄不符合要求");
        }

        // 2. 健康告知规则(复杂逻辑)
        for (HealthDeclaration declaration : proposal.getHealthDeclarations()) {
            UnderwritingRule rule = rules.findRule(declaration.getCondition());
            if (rule != null) {
                // 根据规则进行评估,可能返回加费、除外等结果
                UnderwritingDecision decision = rule.evaluate(declaration);
                if (decision.isRejection()) {
                    return UnderwritingResult.rejected(decision.getReason());
                }
                // 累积加费或除外责任...
            }
        }

        // 3. 组合所有核保决定,生成最终结果
        return UnderwritingResult.approvedWithModifications(... /* 加费比例、除外责任列表 */);
    }
}
2. 在 理赔上下文 内部:
  1. 聚合根(Aggregate Root)

    • Claim(理赔案件): 有claimId,是整个理赔聚合的根。
  2. 实体(Entity)

    • ClaimItem(理赔项): 一个理赔案件可能对应多个医疗费用项。

    • MedicalRecord(医疗记录): 值对象,由客户提供。

  3. 值对象(Value Object)

    • Diagnosis(诊断)

    • Treatment(治疗方式)

  4. 领域服务(Domain Service)

    • ClaimAssessmentService(理赔理算服务): 核心领域逻辑,计算赔付金额。

      • 输入:ClaimPolicyCoverages(从保单上下文获取)

      • 输出:AssessmentResult(赔付金额、拒赔原因)

  5. 规约模式(Specification)

    • ClaimEligibilitySpecification(理赔资格规约): 封装"一个理赔案件是否有效"的复杂业务规则。
java 复制代码
// 聚合根 - 理赔案件
public class Claim {
    private ClaimId id;
    private PolicyId policyId; // 关联的保单ID
    private Diagnosis diagnosis;
    private List<ClaimItem> items;
    private ClaimStatus status;
    private Money assessedAmount;

    // 提交理赔申请
    public void submit() {
        // 验证基本数据...
        this.status = ClaimStatus.SUBMITTED;
    }

    // 核心业务:理算
    public void assess(ClaimAssessmentService service, PolicyCoverages coverages) {
        // 委托给领域服务进行复杂的理算逻辑
        AssessmentResult result = service.assess(this, coverages);
        this.assessedAmount = result.getAmount();
        this.status = result.isApproved() ? ClaimStatus.APPROVED : ClaimStatus.REJECTED;
        // 记录拒赔原因等...
    }
}

// 领域服务 - 理赔理算
@Service
public class ClaimAssessmentService {
    public AssessmentResult assess(Claim claim, PolicyCoverages coverages) {
        Money totalPayable = Money.zero();

        // 1. 检查免责期、等待期等全局规则
        if (!coverages.isWithinCoveragePeriod(claim.getAccidentDate())) {
            return AssessmentResult.rejected("事故日期不在保障期内");
        }

        // 2. 遍历每一个理赔项,计算赔付
        for (ClaimItem item : claim.getItems()) {
            // 找到对应的保险责任
            CoverageDetail coverage = coverages.findCoverageForItem(item);

            // 应用责任规则:免赔额、赔付比例、年度保额上限等
            Money payableForItem = coverage.calculatePayable(
                item.getAmount(),
                coverages.getAccumulatedPaidAmount() // 已累计赔付金额
            );
            totalPayable = totalPayable.add(payableForItem);
        }
        return AssessmentResult.approved(totalPayable);
    }
}

第三步:架构与集成

  • 微服务架构: 每个限界上下文对应一个微服务(Product Service, Policy Service, Claim Service, Payment Service)。

  • 数据持久化: 每个服务拥有独立数据库。Policy Service 存储保单副本,Claim Service 存储理赔数据。

  • 通信方式

    • 同步调用(API) : Claim Service 需要查询保单信息时,通过一个防腐层调用 Policy Service 的API,将保单信息转换为理赔上下文内部的模型,防止污染。

    • 异步事件(Message Queue) : 当 Policy Service 中保单失效时,发布 PolicyLapsedEvent。Claim Service 订阅该事件,并据此拒绝此后该保单的理赔申请。

总结:DDD在健康保险中的价值

  1. 应对极致复杂度: 将核保、理算等极其复杂的金融 和 医学逻辑 封装在 领域服务和聚合根中,代码清晰表达了业务规则。

  2. 精准的业务边界: 产品、保单、理赔等核心概念被严格分离,团队可以专注于自己的领域,并行开发。新产品上线(产品上下文修改)不会影响已有保单的运行。

  3. 高度的可扩展性 : 当需要增加"直付服务"(医院直接与保险公司结算)时,可以创建一个新的直付上下文,它订阅ClaimApprovedEvent,并与Policy Service和医院系统交互,对现有核心系统冲击最小。

  4. 清晰的审计与合规: 每一个状态变更(如核保通过、理赔赔付)都通过聚合根的方法完成,所有业务规则和校验都集中在此,易于追溯和审计,满足 金融监管要求。

通过DDD,我们成功地将一个混乱的"大泥球"保险系统,重构为一个边界清晰、业务逻辑高度内聚、能够快速响应市场变化的现代化核心系统。

追问环节

1. DDD分层架构设计

DDD推荐使用分层架构设计,在健康保险的业务场景下, 上面给到的第二步战术设计强调的是领域层的设计,那么在健康保险的业务场景下, 用户接口层和应用层应该对应哪些内容?

标准的DDD分层架构从外到内是:

  1. **用户接口层:**处理用户交互(包括Web、API、CLI等),展示信息,并解析和传递用户指令。

  2. 应用层

  3. 领域层

  4. 基础设施层

一、用户接口层

在健康保险的Web API场景下,这一层主要由ControllerDTO构成。

1. 在 投保与保单上下文 中:

1. 投保相关接口:

  • ProposalController : 提交投保申请、查询投保单详

2. 保单管理相关接口:

  • PolicyController:查询保单详情、提交保单批改申请、申请保单失
java 复制代码
// 用户接口层 - Controller
@RestController
@RequestMapping("/api/proposals")
public class ProposalController {

    @Autowired
    private ProposalApplicationService proposalAppService;

    @PostMapping
    public ResponseEntity<ProposalSubmissionResponse> submitProposal(@RequestBody SubmitProposalRequest request) {
        // 1. 将用户请求的DTO转换为应用层能理解的Command
        SubmitProposalCommand command = new SubmitProposalCommand(
            request.getApplicantInfo(),
            request.getInsuredInfo(),
            request.getHealthDeclarations(),
            request.getSelectedPlanId()
        );

        // 2. 调用应用服务
        String proposalId = proposalAppService.submitProposal(command);

        // 3. 将结果包装成DTO返回给前端
        ProposalSubmissionResponse response = new ProposalSubmissionResponse(proposalId, "SUBMITTED");
        return ResponseEntity.ok(response);
    }
}

// 用户接口层 - DTO (Input)
public class SubmitProposalRequest {
    private ApplicantInfoDto applicantInfo;
    private InsuredInfoDto insuredInfo;
    private List<HealthDeclarationDto> healthDeclarations;
    private String selectedPlanId;
    // ... getters and setters
}

// 用户接口层 - DTO (Output)
public class ProposalSubmissionResponse {
    private String proposalId;
    private String status;
    // ... constructor and getters
}
二、应用层

核心职责:协调任务,编排领域对象 来完成一个完整的用户用例。它不包含任何核心业务逻辑,而是"指挥家"。

  • 它接收来自用户接口层的"命令"或"查询"对象。

  • 它依赖领域层(聚合根、领域服务、仓储接口)和基础设施层(如其他微服务调用)来完成任务。

  • 它负责事务控制、权限校验(初步的、用例级的)、发布领域事件等。

1. 在 投保与保单上下文 中:

1. 投保应用服务:

  • ProposalApplicationService
java 复制代码
// 应用层 - 应用服务
@Service
@Transactional
public class ProposalApplicationService {

    @Autowired
    private ProposalRepository proposalRepository;
    @Autowired
    private UnderwritingService underwritingService; // 领域服务
    @Autowired
    private ProductService productService; // 防腐层接口,用于获取产品信息
    @Autowired
    private DomainEventPublisher eventPublisher;

    public String submitProposal(SubmitProposalCommand command) {
        // 【步骤1:获取外部信息(通过防腐层)】
        ProductInfo productInfo = productService.getProductInfo(command.getSelectedPlanId());

        // 【步骤2:调用领域层创建领域对象】
        // 使用Factory模式创建投保单聚合根
        Proposal proposal = ProposalFactory.createProposal(
            command.getApplicantInfo(),
            command.getInsuredInfo(),
            command.getHealthDeclarations(),
            productInfo
        );

        // 【步骤3:调用领域服务执行核心业务逻辑】
        UnderwritingResult result = underwritingService.underwrite(proposal, productInfo.getUnderwritingRules());
        proposal.setUnderwritingResult(result);

        // 【步骤4:持久化领域对象】
        proposalRepository.save(proposal);

        // 【步骤5:发布领域事件(如果需要)】
        if (result.isApproved()) {
            eventPublisher.publish(new ProposalUnderwrittenEvent(proposal.getId(), ...));
        }

        // 【步骤6:返回结果(通常是ID或状态)】
        return proposal.getId().toString();
    }
}
2. 在 理赔上下文 中:

2. 理赔应用服务:

  • ClaimApplicationService
java 复制代码
@Service
@Transactional
public class ClaimApplicationService {

    @Autowired
    private ClaimRepository claimRepository;
    @Autowired
    private PolicyService policyService; // 防腐层接口,用于从保单上下文获取保单详情
    @Autowired
    private ClaimAssessmentService assessmentService; // 领域服务

    public String submitClaim(SubmitClaimCommand command) {
        // 1. 通过防腐层,从"保单上下文"获取有效的保单和责任信息
        PolicyCoverages policyCoverages = policyService.getValidPolicyCoverages(command.getPolicyNo());

        // 2. 创建理赔聚合根
        Claim claim = new Claim(
            ClaimId.generate(),
            command.getPolicyNo(),
            command.getAccidentDetails(),
            command.getMedicalRecords()
        );

        // 3. 调用领域服务进行理算
        claim.assess(assessmentService, policyCoverages);

        // 4. 持久化
        claimRepository.save(claim);

        // 5. 如果理赔通过,可能触发支付指令(通过发布事件或调用支付应用服务)
        if (claim.getStatus() == ClaimStatus.APPROVED) {
            // 例如:eventPublisher.publish(new ClaimApprovedEvent(claim.getId(), claim.getAssessedAmount()));
        }

        return claim.getId().toString();
    }
}
3. 查询应用服务 - 专门用于CQRS中的Query:

对于复杂的查询(如保单列表、理赔进度看板),应用层可以绕过领域层,直接调用基础设施层的查询接口(如MyBatis Mapper、数据库客户端)返回DTO。

总结:健康保险场景下的分层职责
分层 在健康保险场景下的核心内容 是否包含业务逻辑
用户接口层 - ProposalController, PolicyController, ClaimController - 各种Request/Response DTO(如SubmitProposalRequest, PolicyDetailsResponse
应用层 - ProposalApplicationService, ClaimApplicationService(处理写操作) - PolicyQueryService(处理读操作) - SubmitProposalCommand, SubmitClaimCommand(应用层内部指令) ,它包含工作流逻辑 ,但不包含核心业务规则
领域层 - Policy, Proposal, Claim(聚合根) - UnderwritingService, ClaimAssessmentService(领域服务) - CoverageDetail, Money(值对象) - PolicyIssuedEvent(领域事件) 核心业务逻辑所在地
基础设施层 - PolicyRepositoryImpl, ClaimRepositoryImpl(持久化实现) - ProductServiceClient(调用产品上下文的防腐层实现) - DomainEventPublisherImpl(事件发布实现) - PolicyQueryRepositoryImpl(查询数据库实现)
  • 领域层可以专注于复杂的核保、理算规则;
  • 应用层 则像一个导演,协调仓储获取数据,调用领域服务进行核保,再通过防腐层获取外部产品信息,最后 命令仓储保存数据 并 发布事件
  • 用户接口层则只关心如何接收和展示数据。这样确保了系统的关注点分离,架构清晰,易于维护和演化。

2. 存储数据应该哪一层来调用

问题:基础设施层存储数据, 是应用层去调用基础设施层存储数据,还是领域层调用基础设施层存储数据?

正确答案是:由应用层调用基础设施层来存储数据,而不是领域层。

领域层应该对如何存储数据一无所知,它只关心业务逻辑和状态变化。

为什么必须由应用层调用?关键区别对比

方面 应用层调用存储(正确) 领域层调用存储(错误)
架构纯洁性 领域层保持纯洁,不依赖任何技术实现 领域层被基础设施细节污染,变成"贫血模型"的反面------"充血过度"
事务控制 应用层可以方便地使用@Transactional管理 整个用例的事务 领域层中的保存操作难以统一管理事务边界,容易导致部分更新
性能优化 应用层可以批量加载和保存多个聚合,优化数据库交互 领域逻辑中的分散保存会导致多次数据库往返,性能低下
测试便利性 领域层可以完全脱离仓储进行单元测试,测试纯粹的业务逻辑 测试领域逻辑必须mock仓储,测试变得复杂且缓慢
用例完整性 应用层确保一个用例中的 所有领域操作和存储操作作为一个完整单元 存储操作分散在领域各处,难以保证业务操作的原子性
依赖方向 依赖方向正确:领域层 ← 应用层 → 基础设施层 依赖方向错误:领域层 → 基础设施层,违反依赖倒置原则
相关推荐
Scott9999HH3 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔3 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社4 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海4 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖5 小时前
JDK 26 新特性详解
java·开发语言
马优晨5 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区7 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大8 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai8 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说8 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json