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仓储,测试变得复杂且缓慢
用例完整性 应用层确保一个用例中的 所有领域操作和存储操作作为一个完整单元 存储操作分散在领域各处,难以保证业务操作的原子性
依赖方向 依赖方向正确:领域层 ← 应用层 → 基础设施层 依赖方向错误:领域层 → 基础设施层,违反依赖倒置原则
相关推荐
serendipity_hky2 小时前
【微服务 - easy视频 | day04】Seata解决分布式事务
java·spring boot·分布式·spring cloud·微服务·架构
程序猿20232 小时前
Python每日一练---第十二天:验证回文串
开发语言·python
wjs20242 小时前
AJAX 实例详解
开发语言
沿着路走到底2 小时前
python 判断与循环
java·前端·python
我要升天!2 小时前
QT -- 初识
开发语言·qt
wjs20242 小时前
Memcached flush_all 命令详解
开发语言
zbhbbedp282793cl3 小时前
unique_ptr和shared_ptr有何区别?
java·开发语言·jvm
珹洺3 小时前
Java-Spring入门指南(二十九)Android交互核心:按钮点击事件与Activity跳转实战
android·java·spring
.NET修仙日记3 小时前
第四章:C# 面向对象编程详解:从类与对象到完整项目实践
开发语言·c#·.net·源码·教程·.net core