领域驱动设计之银行转账:Wow框架实战
银行账户转账案例
银行账户转账案例是一个经典的领域驱动设计(DDD)应用场景。接下来我们通过一个简单的银行账户转账案例,来了解如何使用 Wow 进行领域驱动设计以及服务开发。
银行转账流程
- 准备转账(Prepare): 用户发起转账请求,触发 Prepare 步骤。这个步骤会向源账户发送准备转账的请求。
- 校验余额(CheckBalance): 源账户在收到准备转账请求后,会执行校验余额的操作,确保账户有足够的余额进行转账。
- 锁定金额(LockAmount): 如果余额足够,源账户会锁定转账金额,防止其他操作干扰。
- 入账(Entry): 接着,转账流程进入到目标账户,执行入账操作。
- 确认转账(Confirm): 如果入账成功,确认转账;否则,执行解锁金额操作。
- 成功路径(Success): 如果一切顺利,完成转账流程。
- 失败路径(Fail): 如果入账失败,执行解锁金额操作,并处理失败情况。
运行案例
- 运行 TransferExampleServer.java
- 查看 Swagger-UI : http://localhost:8080/swagger-ui.html
- 执行 API 测试:Transfer.http
自动生成 API 端点
运行之后,访问 Swagger-UI : http://localhost:8080/swagger-ui.html 。
该 RESTful API 端点是由 Wow 自动生成的,无需手动编写。

模块划分
| 模块 | 说明 |
|---|---|
| example-transfer-api | API 层,定义聚合命令(Command)、领域事件(Domain Event)以及查询视图模型(Query View Model),这个模块充当了各个模块之间通信的“发布语言”。 |
| example-transfer-domain | 领域层,包含聚合根和业务约束的实现。聚合根:领域模型的入口点,负责协调领域对象的操作。业务约束:包括验证规则、领域事件的处理等。 |
| example-transfer-server | 宿主服务,应用程序的启动点。负责整合其他模块,并提供应用程序的入口。涉及配置依赖项、连接数据库、启动 API 服务 |
领域建模
账户聚合根
状态聚合根(AccountState)与命令聚合根(Account)分离设计保证了在执行命令过程中,不会修改状态聚合根的状态。
状态聚合根(AccountState)建模
public class AccountState implements Identifier {
private final String id;
private String name;
/**
* 余额
*/
private long balanceAmount = 0L;
/**
* 已锁定金额
*/
private long lockedAmount = 0L;
/**
* 账号已冻结标记
*/
private boolean frozen = false;
@JsonCreator
public AccountState(@JsonProperty("id") String id) {
this.id = id;
}
@NotNull
@Override
public String getId() {
return id;
}
public String getName() {
return name;
}
public long getBalanceAmount() {
return balanceAmount;
}
public long getLockedAmount() {
return lockedAmount;
}
public boolean isFrozen() {
return frozen;
}
void onSourcing(AccountCreated accountCreated) {
this.name = accountCreated.name();
this.balanceAmount = accountCreated.balance();
}
void onSourcing(AmountLocked amountLocked) {
balanceAmount = balanceAmount - amountLocked.amount();
lockedAmount = lockedAmount + amountLocked.amount();
}
void onSourcing(AmountEntered amountEntered) {
balanceAmount = balanceAmount + amountEntered.amount();
}
void onSourcing(Confirmed confirmed) {
lockedAmount = lockedAmount - confirmed.amount();
}
void onSourcing(AmountUnlocked amountUnlocked) {
lockedAmount = lockedAmount - amountUnlocked.amount();
balanceAmount = balanceAmount + amountUnlocked.amount();
}
void onSourcing(AccountFrozen accountFrozen) {
this.frozen = true;
}
}
命令聚合根(Account)建模
@StaticTenantId
@AggregateRoot
public class Account {
private final AccountState state;
public Account(AccountState state) {
this.state = state;
}
AccountCreated onCommand(CreateAccount createAccount) {
return new AccountCreated(createAccount.name(), createAccount.balance());
}
@OnCommand(returns = {AmountLocked.class, Prepared.class})
List<?> onCommand(Prepare prepare) {
checkBalance(prepare.amount());
return List.of(new AmountLocked(prepare.amount()), new Prepared(prepare.to(), prepare.amount()));
}
private void checkBalance(long amount) {
if (state.isFrozen()) {
throw new IllegalStateException("账号已冻结无法转账.");
}
if (state.getBalanceAmount() < amount) {
throw new IllegalStateException("账号余额不足.");
}
}
Object onCommand(Entry entry) {
if (state.isFrozen()) {
return new EntryFailed(entry.sourceId(), entry.amount());
}
return new AmountEntered(entry.sourceId(), entry.amount());
}
Confirmed onCommand(Confirm confirm) {
return new Confirmed(confirm.amount());
}
AmountUnlocked onCommand(UnlockAmount unlockAmount) {
return new AmountUnlocked(unlockAmount.amount());
}
AccountFrozen onCommand(FreezeAccount freezeAccount) {
return new AccountFrozen(freezeAccount.reason());
}
}
转账流程管理器(TransferSaga)
转账流程管理器(TransferSaga)负责协调处理转账的事件,并生成相应的命令。
onEvent(Prepared): 订阅转账已准备就绪事件(Prepared),并生成入账命令(Entry)。onEvent(AmountEntered): 订阅转账已入账事件(AmountEntered),并生成确认转账命令(Confirm)。onEvent(EntryFailed): 订阅转账入账失败事件(EntryFailed),并生成解锁金额命令(UnlockAmount)。
@StatelessSaga
public class TransferSaga {
Entry onEvent(Prepared prepared, AggregateId aggregateId) {
return new Entry(prepared.to(), aggregateId.getId(), prepared.amount());
}
Confirm onEvent(AmountEntered amountEntered) {
return new Confirm(amountEntered.sourceId(), amountEntered.amount());
}
UnlockAmount onEvent(EntryFailed entryFailed) {
return new UnlockAmount(entryFailed.sourceId(), entryFailed.amount());
}
}
单元测试
internal class AccountKTest {
@Test
fun createAccount() {
aggregateVerifier<Account, AccountState>()
.given()
.`when`(CreateAccount("name", 100))
.expectEventType(AccountCreated::class.java)
.expectState {
assertThat(it.name, equalTo("name"))
assertThat(it.balanceAmount, equalTo(100))
}
.verify()
}
}
领域驱动设计之银行转账:Wow框架实战的更多相关文章
- 基于“事件”驱动的领域驱动设计(DDD)框架分析
摘抄自 从去年10月份开始,学了几个月的领域驱动设计(Domain Driven Design,简称DDD).主要是学习领域驱动设计之父Eric Evans的名著:<Domain-driven ...
- [.NET领域驱动设计实战系列]专题二:结合领域驱动设计的面向服务架构来搭建网上书店
一.前言 在前面专题一中,我已经介绍了我写这系列文章的初衷了.由于dax.net中的DDD框架和Byteart Retail案例并没有对其形成过程做一步步分析,而是把整个DDD的实现案例展现给我们,这 ...
- 【无私分享:ASP.NET CORE 项目实战(第三章)】EntityFramework下领域驱动设计的应用
目录索引 [无私分享:ASP.NET CORE 项目实战]目录索引 简介 在我们 [无私分享:从入门到精通ASP.NET MVC] 系列中,我们其实也是有DDD思想的,但是没有完全的去实现,因为并不是 ...
- [.NET领域驱动设计实战系列]专题十一:.NET 领域驱动设计实战系列总结
一.引用 其实在去年本人已经看过很多关于领域驱动设计的书籍了,包括Microsoft .NET企业级应用框架设计.领域驱动设计C# 2008实现.领域驱动设计:软件核心复杂性应对之道.实现领域驱动设计 ...
- [.NET领域驱动设计实战系列]专题一:前期准备之EF CodeFirst
一.前言 从去年已经接触领域驱动设计(Domain-Driven Design)了,当时就想自己搭建一个DDD框架,所以当时看了很多DDD方面的书,例如领域驱动模式与实战,领域驱动设计:软件核心复杂性 ...
- NET 领域驱动设计实战系列总结
NET 领域驱动设计实战系列总结 一.引用 其实在去年本人已经看过很多关于领域驱动设计的书籍了,包括Microsoft .NET企业级应用框架设计.领域驱动设计C# 2008实现.领域驱动设计:软件核 ...
- [转] DDD领域驱动设计框架分享
从去年10月份开始,学了几个月的领域驱动设计(Domain Driven Design,简称DDD).主要是学习领域驱动设计之父Eric Evans的名著:<Domain-driven desi ...
- 领域驱动设计业务框架DMVP
DMVP,全称DDD-MVP,是基于领域驱动设计(DDD)搭建的业务框架,整体设计符合DDD领域模型的规范,业务上达成了领域模型和代码的一一映射,技术上达成了高内聚低耦合的架构设计,开发人员不需要关注 ...
- 基于事件驱动的DDD领域驱动设计框架分享(附源代码)
原文:基于事件驱动的DDD领域驱动设计框架分享(附源代码) 补充:现在再回过头来看这篇文章,感觉当初自己偏激了,呵呵.不过没有以前的我,怎么会有现在的我和现在的enode框架呢?发现自己进步了真好! ...
- .NET领域驱动设计—初尝(一:疑问、模式、原则、工具、过程、框架、实践)
.NET领域驱动设计—初尝(一:疑问.模式.原则.工具.过程.框架.实践) 2013-04-07 17:35:27 标签:.NET DDD 驱动设计 原创作品,允许转载,转载时请务必以超链接形式标明 ...
随机推荐
- React报错:This is probably not a problem with npm. There is likely additional logging output above.
解决方案 删除node_modules和package-lock.json,之后运行npm cache clear --force,重新安装模块npm install,另外要注意 npm 5.0版本之 ...
- JavaScript中单例模式这样用
如果希望自己的代码更优雅.可维护性更高以及更简洁,往往离不开设计模式这一解决方案. 在JS设计模式中,最核心的思想:封装变化(将变与不变分离,确保变化的部分灵活,不变的部分稳定). 单例模式 那么来说 ...
- 解决 Blazor 中因标签换行导致的行内元素空隙问题
实践过不同前端框架的朋友应该都知道,对于同一个样式,在不同框架上的表现都会有不同,时时需要做"适配",在 Blazor 上也不例外.在做 Ant Design Blazor 时就深 ...
- 耗时6个月,我做了一款干净、免费、开源的AI数据库
一.Chat2DB简介 在消失的这段时间,我和小伙伴们做了一款集成了AI的数据库管理工具Chat2DB. 他是数据库也集成了AIGC的能力,能够将自然语言转换为SQL,也可以将SQL转换为自然语言,还 ...
- [HDCTF2019]Maze 反汇编-花指令处理
这是一道迷宫花指令加upx脱壳处理的题 先介绍花指令 一. 概述 花指令是对抗反汇编的有效手段之一,正常代码添加了花指令之后,可以破坏静态反汇编的过程,使反汇编的结果出现错误.错误的反汇编结果会造成破 ...
- Java安全之Webshell免杀
Java安全之Webshell免杀 当遇到文件上传时,如果网站存在查杀软件,我们上传的一句话木马会被直接秒杀,这时候就需要做一下免杀,绕过查杀软件的检测. 思路 我的想法是先拆分,然后分别检验那些语句 ...
- 组合查询(left_inner_right)与排序(order by _DESC _ASC)在题目中的应用
1,想要让哪一列放在开头或者结尾,只需要将select中的查询位置放在最开始或者结尾即可: 2,组合查询要注意使用 on 加上组合条件: 3,order by 默认升序(ASC),降序使用:order ...
- 【Hexo】配置主流搜索引擎收录流程记录
目录 是否已经被收录 生成站点地图 提交站点地图 Google 注册 Search Console 验证网站所有权 提交站点地图 Bing 从 GSC 导入 手动添加网站 手动请求编入索引 参考资料 ...
- 线程方法接收参数和返回参数,Java的两种线程实现方式对比
The difference beteen two way 总所周知,Java实现多线程有两种方式,分别是继承Thread类和实现Runable接口,那么它们的区别是什么? 继承 Thread 类: ...
- 【译】.NET 8 拦截器(interceptor)
通常情况下,出于多种原因,我不会说我喜欢写关于预览功能的文章.我的大多数帖子旨在帮助人们解决他们可能遇到的问题,而不是找个肥皂盒或打广告.但是我认为我应该介绍这个 .NET 预览特性,因为它是我在 . ...