├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── command ├── src │ └── main │ │ ├── java │ │ └── com │ │ │ └── cqrs │ │ │ └── command │ │ │ ├── dto │ │ │ ├── DepositDTO.java │ │ │ ├── WithdrawalDTO.java │ │ │ ├── AccountDTO.java │ │ │ ├── TransactionDTO.java │ │ │ ├── HolderDTO.java │ │ │ └── TransferDTO.java │ │ │ ├── event │ │ │ └── DepositCompletedEvent.java │ │ │ ├── CommandApplication.java │ │ │ ├── command │ │ │ ├── TransferApprovedCommand.java │ │ │ ├── AccountCreationCommand.java │ │ │ ├── DepositMoneyCommand.java │ │ │ ├── WithdrawMoneyCommand.java │ │ │ ├── HolderCreationCommand.java │ │ │ └── MoneyTransferCommand.java │ │ │ ├── service │ │ │ ├── TransactionService.java │ │ │ └── TransactionServiceImpl.java │ │ │ ├── aggregate │ │ │ ├── HolderAggregate.java │ │ │ └── AccountAggregate.java │ │ │ ├── controller │ │ │ └── TransactionController.java │ │ │ ├── config │ │ │ └── AxonConfig.java │ │ │ └── saga │ │ │ └── TransferManager.java │ │ └── resources │ │ └── application.yml ├── build.gradle └── api.http ├── settings.gradle ├── jejuBank ├── src │ └── main │ │ ├── java │ │ └── com │ │ │ └── cqrs │ │ │ └── jeju │ │ │ ├── service │ │ │ ├── AccountService.java │ │ │ └── AccountServiceImpl.java │ │ │ ├── dto │ │ │ └── AccountDTO.java │ │ │ ├── event │ │ │ └── AccountCreationEvent.java │ │ │ ├── JejuBankApp.java │ │ │ ├── command │ │ │ └── AccountCreationCommand.java │ │ │ ├── controller │ │ │ └── AccountController.java │ │ │ ├── component │ │ │ └── AccountLoanComponent.java │ │ │ └── aggregate │ │ │ └── Account.java │ │ └── resources │ │ └── application.yml └── build.gradle ├── seoulBank ├── src │ └── main │ │ ├── resources │ │ └── application.yml │ │ └── java │ │ └── com │ │ └── cqrs │ │ └── seoul │ │ ├── SeoulBankApp.java │ │ └── component │ │ └── AccountLoanComponent.java └── build.gradle ├── query ├── src │ └── main │ │ ├── java │ │ └── com │ │ │ └── cqrs │ │ │ └── query │ │ │ ├── query │ │ │ └── AccountQuery.java │ │ │ ├── QueryApplication.java │ │ │ ├── repository │ │ │ └── AccountRepository.java │ │ │ ├── controller │ │ │ ├── WebController.java │ │ │ └── HolderAccountController.java │ │ │ ├── service │ │ │ ├── QueryService.java │ │ │ └── QueryServiceImpl.java │ │ │ ├── entity │ │ │ └── HolderAccountSummary.java │ │ │ ├── version │ │ │ └── HolderCreationEventV1.java │ │ │ ├── config │ │ │ └── AxonConfig.java │ │ │ └── projection │ │ │ └── HolderAccountProjection.java │ │ └── resources │ │ ├── application.yml │ │ └── templates │ │ ├── p2p.html │ │ ├── scatter-gather.html │ │ └── subscription.html └── build.gradle ├── common ├── src │ └── main │ │ └── java │ │ └── com │ │ └── cqrs │ │ ├── query │ │ └── loan │ │ │ ├── LoanLimitQuery.java │ │ │ └── LoanLimitResult.java │ │ ├── event │ │ ├── AccountCreationEvent.java │ │ ├── DepositMoneyEvent.java │ │ ├── WithdrawMoneyEvent.java │ │ ├── transfer │ │ │ ├── TransferApprovedEvent.java │ │ │ ├── CompletedCancelTransferEvent.java │ │ │ ├── CompletedCompensationCancelEvent.java │ │ │ ├── TransferDeniedEvent.java │ │ │ └── MoneyTransferEvent.java │ │ └── HolderCreationEvent.java │ │ └── command │ │ └── transfer │ │ ├── JejuBankTransferCommand.java │ │ ├── SeoulBankTransferCommand.java │ │ ├── JejuBankCancelTransferCommand.java │ │ ├── SeoulBankCancelTransferCommand.java │ │ ├── JejuBankCompensationCancelCommand.java │ │ ├── SeoulBankCompensationCancelCommand.java │ │ ├── AbstractTransferCommand.java │ │ ├── AbstractCancelTransferCommand.java │ │ ├── AbstractCompensationCancelCommand.java │ │ └── factory │ │ └── TransferComamndFactory.java └── build.gradle ├── .gitignore ├── gradlew.bat └── gradlew /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cla9/axon-account-example/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /command/src/main/java/com/cqrs/command/dto/DepositDTO.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.dto; 2 | 3 | 4 | public class DepositDTO extends TransactionDTO {} 5 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'demo' 2 | include 'command' 3 | include 'query' 4 | include 'common' 5 | include 'seoulBank' 6 | include 'jejuBank' 7 | -------------------------------------------------------------------------------- /command/src/main/java/com/cqrs/command/dto/WithdrawalDTO.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.dto; 2 | 3 | 4 | public class WithdrawalDTO extends TransactionDTO {} 5 | -------------------------------------------------------------------------------- /jejuBank/src/main/java/com/cqrs/jeju/service/AccountService.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.jeju.service; 2 | 3 | import com.cqrs.jeju.dto.AccountDTO; 4 | 5 | public interface AccountService { 6 | String createAccount(AccountDTO accountDTO); 7 | } 8 | -------------------------------------------------------------------------------- /seoulBank/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 9092 3 | 4 | spring: 5 | application: 6 | name: eventsourcing-cqrs-seoulBank 7 | 8 | axon: 9 | serializer: 10 | general: xstream 11 | axonserver: 12 | servers: localhost:8124 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Dec 24 23:19:55 KST 2019 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-6.0.1-all.zip 7 | -------------------------------------------------------------------------------- /jejuBank/src/main/java/com/cqrs/jeju/dto/AccountDTO.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.jeju.dto; 2 | 3 | import lombok.*; 4 | 5 | @NoArgsConstructor 6 | @AllArgsConstructor 7 | @ToString 8 | @Getter 9 | public class AccountDTO { 10 | private String accountID; 11 | private Long balance; 12 | } 13 | -------------------------------------------------------------------------------- /query/src/main/java/com/cqrs/query/query/AccountQuery.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.query.query; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | @Getter 8 | @ToString 9 | @AllArgsConstructor 10 | public class AccountQuery { 11 | private String holderId; 12 | } 13 | -------------------------------------------------------------------------------- /command/src/main/java/com/cqrs/command/dto/AccountDTO.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Getter 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class AccountDTO { 11 | private String holderID; 12 | } 13 | -------------------------------------------------------------------------------- /common/src/main/java/com/cqrs/query/loan/LoanLimitQuery.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.query.loan; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | @AllArgsConstructor 8 | @ToString 9 | @Getter 10 | public class LoanLimitQuery { 11 | private String holderID; 12 | private Long balance; 13 | } 14 | -------------------------------------------------------------------------------- /common/src/main/java/com/cqrs/event/AccountCreationEvent.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.event; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | @AllArgsConstructor 8 | @ToString 9 | @Getter 10 | public class AccountCreationEvent { 11 | private String holderID; 12 | private String accountID; 13 | } 14 | -------------------------------------------------------------------------------- /seoulBank/build.gradle: -------------------------------------------------------------------------------- 1 | ext{ 2 | axonVersion = "4.2.1" 3 | } 4 | dependencies{ 5 | implementation 'org.springframework.boot:spring-boot-starter-web' 6 | compileOnly 'org.projectlombok:lombok' 7 | annotationProcessor 'org.projectlombok:lombok' 8 | implementation group: 'org.axonframework', name: 'axon-spring-boot-starter', version: "$axonVersion" 9 | } 10 | -------------------------------------------------------------------------------- /command/src/main/java/com/cqrs/command/event/DepositCompletedEvent.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.event; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | @AllArgsConstructor 8 | @ToString 9 | @Getter 10 | public class DepositCompletedEvent { 11 | private String accountID; 12 | private String transferID; 13 | } 14 | -------------------------------------------------------------------------------- /common/src/main/java/com/cqrs/event/DepositMoneyEvent.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.event; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | @AllArgsConstructor 8 | @ToString 9 | @Getter 10 | public class DepositMoneyEvent { 11 | private String holderID; 12 | private String accountID; 13 | private Long amount; 14 | } 15 | -------------------------------------------------------------------------------- /common/src/main/java/com/cqrs/event/WithdrawMoneyEvent.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.event; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | @AllArgsConstructor 8 | @ToString 9 | @Getter 10 | public class WithdrawMoneyEvent { 11 | private String holderID; 12 | private String accountID; 13 | private Long amount; 14 | } 15 | -------------------------------------------------------------------------------- /jejuBank/src/main/java/com/cqrs/jeju/event/AccountCreationEvent.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.jeju.event; 2 | 3 | import lombok.Getter; 4 | import lombok.RequiredArgsConstructor; 5 | import lombok.ToString; 6 | 7 | @ToString 8 | @RequiredArgsConstructor 9 | @Getter 10 | public class AccountCreationEvent { 11 | private final String accountID; 12 | private final Long balance; 13 | } 14 | -------------------------------------------------------------------------------- /jejuBank/src/main/java/com/cqrs/jeju/JejuBankApp.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.jeju; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class JejuBankApp { 8 | public static void main(String[] args) { 9 | SpringApplication.run(JejuBankApp.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /command/src/main/java/com/cqrs/command/dto/TransactionDTO.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Getter 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class TransactionDTO { 11 | private String accountID; 12 | private String holderID; 13 | private Long amount; 14 | } 15 | -------------------------------------------------------------------------------- /seoulBank/src/main/java/com/cqrs/seoul/SeoulBankApp.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.seoul; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class SeoulBankApp { 8 | public static void main(String[] args) { 9 | SpringApplication.run(SeoulBankApp.class, args); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /query/src/main/java/com/cqrs/query/QueryApplication.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.query; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | 7 | @SpringBootApplication 8 | public class QueryApplication { 9 | public static void main(String[] args) { 10 | SpringApplication.run(QueryApplication.class, args); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /command/src/main/java/com/cqrs/command/dto/HolderDTO.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.dto; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.NoArgsConstructor; 6 | 7 | @Getter 8 | @AllArgsConstructor 9 | @NoArgsConstructor 10 | public class HolderDTO { 11 | private String holderName; 12 | private String tel; 13 | private String address; 14 | private String company; 15 | } 16 | -------------------------------------------------------------------------------- /common/src/main/java/com/cqrs/event/transfer/TransferApprovedEvent.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.event.transfer; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | @Builder 8 | @ToString 9 | @Getter 10 | public class TransferApprovedEvent { 11 | private String srcAccountID; 12 | private String dstAccountID; 13 | private String transferID; 14 | private Long amount; 15 | } 16 | -------------------------------------------------------------------------------- /command/src/main/java/com/cqrs/command/CommandApplication.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | @SpringBootApplication 7 | public class CommandApplication { 8 | public static void main(String[] args) { 9 | SpringApplication.run(CommandApplication.class, args); 10 | } 11 | } 12 | 13 | -------------------------------------------------------------------------------- /common/src/main/java/com/cqrs/event/transfer/CompletedCancelTransferEvent.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.event.transfer; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | @Builder 8 | @ToString 9 | @Getter 10 | public class CompletedCancelTransferEvent { 11 | private String srcAccountID; 12 | private String dstAccountID; 13 | private Long amount; 14 | private String transferID; 15 | } 16 | -------------------------------------------------------------------------------- /jejuBank/src/main/java/com/cqrs/jeju/command/AccountCreationCommand.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.jeju.command; 2 | 3 | 4 | import lombok.*; 5 | import org.axonframework.modelling.command.TargetAggregateIdentifier; 6 | 7 | @NoArgsConstructor 8 | @AllArgsConstructor 9 | @ToString 10 | @Getter 11 | public class AccountCreationCommand { 12 | @TargetAggregateIdentifier 13 | private String accountID; 14 | private Long balance; 15 | } 16 | -------------------------------------------------------------------------------- /command/src/main/java/com/cqrs/command/command/TransferApprovedCommand.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.command; 2 | 3 | 4 | import lombok.*; 5 | import org.axonframework.modelling.command.TargetAggregateIdentifier; 6 | 7 | @ToString 8 | @Getter 9 | @Builder 10 | public class TransferApprovedCommand { 11 | @TargetAggregateIdentifier 12 | private String accountID; 13 | private Long amount; 14 | private String transferID; 15 | } 16 | -------------------------------------------------------------------------------- /common/src/main/java/com/cqrs/event/transfer/CompletedCompensationCancelEvent.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.event.transfer; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | @Builder 8 | @ToString 9 | @Getter 10 | public class CompletedCompensationCancelEvent { 11 | private String srcAccountID; 12 | private String dstAccountID; 13 | private Long amount; 14 | private String transferID; 15 | } 16 | -------------------------------------------------------------------------------- /common/src/main/java/com/cqrs/event/transfer/TransferDeniedEvent.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.event.transfer; 2 | 3 | import lombok.Builder; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | 7 | @Getter 8 | @Builder 9 | @ToString 10 | public class TransferDeniedEvent { 11 | private String srcAccountID; 12 | private String dstAccountID; 13 | private String transferID; 14 | private Long amount; 15 | private String description; 16 | } 17 | -------------------------------------------------------------------------------- /common/src/main/java/com/cqrs/query/loan/LoanLimitResult.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.query.loan; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | import lombok.ToString; 7 | 8 | @AllArgsConstructor 9 | @ToString 10 | @Getter 11 | @Builder 12 | public class LoanLimitResult { 13 | private String holderID; 14 | private String bankName; 15 | private Long balance; 16 | private Long loanLimit; 17 | } 18 | -------------------------------------------------------------------------------- /common/build.gradle: -------------------------------------------------------------------------------- 1 | ext{ 2 | axonVersion = "4.2.1" 3 | } 4 | bootJar { 5 | enabled = false 6 | } 7 | jar { 8 | enabled = true 9 | } 10 | dependencies{ 11 | compileOnly 'org.projectlombok:lombok' 12 | annotationProcessor 'org.projectlombok:lombok' 13 | implementation group: 'org.axonframework', name: 'axon-messaging', version: "$axonVersion" 14 | implementation group: 'org.axonframework', name: 'axon-spring-boot-starter', version: "$axonVersion" 15 | } 16 | -------------------------------------------------------------------------------- /command/src/main/java/com/cqrs/command/command/AccountCreationCommand.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.command; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | import org.axonframework.modelling.command.TargetAggregateIdentifier; 7 | 8 | @AllArgsConstructor 9 | @ToString 10 | @Getter 11 | public class AccountCreationCommand { 12 | @TargetAggregateIdentifier 13 | private String accountID; 14 | private String holderID; 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/** 6 | !**/src/test/** 7 | 8 | ### STS ### 9 | .apt_generated 10 | .classpath 11 | .factorypath 12 | .project 13 | .settings 14 | .springBeans 15 | .sts4-cache 16 | 17 | ### IntelliJ IDEA ### 18 | .idea 19 | *.iws 20 | *.iml 21 | *.ipr 22 | out/ 23 | 24 | ### NetBeans ### 25 | /nbproject/private/ 26 | /nbbuild/ 27 | /dist/ 28 | /nbdist/ 29 | /.nb-gradle/ 30 | 31 | ### VS Code ### 32 | .vscode/ 33 | -------------------------------------------------------------------------------- /command/src/main/java/com/cqrs/command/command/DepositMoneyCommand.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.command; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | import org.axonframework.modelling.command.TargetAggregateIdentifier; 7 | 8 | @AllArgsConstructor 9 | @ToString 10 | @Getter 11 | public class DepositMoneyCommand { 12 | @TargetAggregateIdentifier 13 | private String accountID; 14 | private String holderID; 15 | private Long amount; 16 | } 17 | -------------------------------------------------------------------------------- /command/src/main/java/com/cqrs/command/command/WithdrawMoneyCommand.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.command; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | import org.axonframework.modelling.command.TargetAggregateIdentifier; 7 | 8 | @AllArgsConstructor 9 | @ToString 10 | @Getter 11 | public class WithdrawMoneyCommand { 12 | @TargetAggregateIdentifier 13 | private String accountID; 14 | private String holderID; 15 | private Long amount; 16 | } 17 | -------------------------------------------------------------------------------- /common/src/main/java/com/cqrs/event/HolderCreationEvent.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.event; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | import org.axonframework.serialization.Revision; 7 | 8 | @AllArgsConstructor 9 | @ToString 10 | @Getter 11 | @Revision("1.0") 12 | public class HolderCreationEvent { 13 | private String holderID; 14 | private String holderName; 15 | private String tel; 16 | private String address; 17 | private String company; 18 | } 19 | -------------------------------------------------------------------------------- /jejuBank/build.gradle: -------------------------------------------------------------------------------- 1 | ext{ 2 | axonVersion = "4.2.1" 3 | } 4 | dependencies{ 5 | implementation 'org.springframework.boot:spring-boot-starter-web' 6 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 7 | compileOnly 'org.projectlombok:lombok' 8 | annotationProcessor 'org.projectlombok:lombok' 9 | implementation group: 'org.axonframework', name: 'axon-spring-boot-starter', version: "$axonVersion" 10 | implementation group: 'org.postgresql', name: 'postgresql', version: '42.2.6' 11 | } 12 | -------------------------------------------------------------------------------- /query/src/main/java/com/cqrs/query/repository/AccountRepository.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.query.repository; 2 | 3 | import com.cqrs.query.entity.HolderAccountSummary; 4 | import org.springframework.data.jpa.repository.JpaRepository; 5 | import org.springframework.retry.annotation.Retryable; 6 | 7 | import java.sql.SQLException; 8 | import java.util.Optional; 9 | 10 | public interface AccountRepository extends JpaRepository { 11 | Optional findByHolderId(String holderId); 12 | } 13 | -------------------------------------------------------------------------------- /query/src/main/java/com/cqrs/query/controller/WebController.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.query.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | 6 | @Controller 7 | public class WebController { 8 | @GetMapping("/p2p") 9 | public void pointToPointQueryView(){} 10 | 11 | @GetMapping("/subscription") 12 | public void subscriptionQueryView(){} 13 | 14 | @GetMapping("/scatter-gather") 15 | public void scatterGatherQueryView(){} 16 | } 17 | -------------------------------------------------------------------------------- /common/src/main/java/com/cqrs/event/transfer/MoneyTransferEvent.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.event.transfer; 2 | 3 | import com.cqrs.command.transfer.factory.TransferComamndFactory; 4 | import lombok.Builder; 5 | import lombok.Getter; 6 | import lombok.ToString; 7 | 8 | @Builder 9 | @ToString 10 | @Getter 11 | public class MoneyTransferEvent { 12 | private String dstAccountID; 13 | private String srcAccountID; 14 | private Long amount; 15 | private String transferID; 16 | private TransferComamndFactory comamndFactory; 17 | } 18 | -------------------------------------------------------------------------------- /jejuBank/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 9091 3 | 4 | spring: 5 | application: 6 | name: eventsourcing-cqrs-jejuBank 7 | datasource: 8 | platform: postgres 9 | url: jdbc:postgresql://localhost:5432/jeju 10 | username: jeju 11 | password: jeju 12 | driverClassName: org.postgresql.Driver 13 | jpa: 14 | hibernate: 15 | ddl-auto: update 16 | 17 | axon: 18 | serializer: 19 | general: xstream 20 | axonserver: 21 | servers: localhost:8124 22 | 23 | logging.level.com.cqrs.jeju : debug -------------------------------------------------------------------------------- /command/src/main/java/com/cqrs/command/dto/TransferDTO.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.dto; 2 | 3 | import com.cqrs.command.command.MoneyTransferCommand; 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.ToString; 8 | 9 | @Getter 10 | @ToString 11 | @AllArgsConstructor 12 | @NoArgsConstructor 13 | public class TransferDTO { 14 | private String srcAccountID; 15 | private String dstAccountID; 16 | private Long amount; 17 | private MoneyTransferCommand.BankType bankType; 18 | } 19 | -------------------------------------------------------------------------------- /common/src/main/java/com/cqrs/command/transfer/JejuBankTransferCommand.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.transfer; 2 | 3 | public class JejuBankTransferCommand extends AbstractTransferCommand { 4 | @Override 5 | public String toString() { 6 | return "JejuBankTransferCommand{" + 7 | "srcAccountID='" + srcAccountID + '\'' + 8 | ", dstAccountID='" + dstAccountID + '\'' + 9 | ", amount=" + amount + 10 | ", transferID='" + transferID + '\'' + 11 | '}'; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /common/src/main/java/com/cqrs/command/transfer/SeoulBankTransferCommand.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.transfer; 2 | 3 | 4 | public class SeoulBankTransferCommand extends AbstractTransferCommand { 5 | @Override 6 | public String toString() { 7 | return "SeoulBankTransferCommand{" + 8 | "srcAccountID='" + srcAccountID + '\'' + 9 | ", dstAccountID='" + dstAccountID + '\'' + 10 | ", amount=" + amount + 11 | ", transferID='" + transferID + '\'' + 12 | '}'; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /query/src/main/java/com/cqrs/query/service/QueryService.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.query.service; 2 | 3 | 4 | import com.cqrs.query.entity.HolderAccountSummary; 5 | import com.cqrs.query.loan.LoanLimitResult; 6 | import reactor.core.publisher.Flux; 7 | 8 | import java.util.List; 9 | 10 | public interface QueryService { 11 | void reset(); 12 | HolderAccountSummary getAccountInfo(String holderId); 13 | Flux getAccountInfoSubscription(String holderId); 14 | List getAccountInfoScatterGather(String holderId); 15 | } 16 | -------------------------------------------------------------------------------- /command/src/main/java/com/cqrs/command/command/HolderCreationCommand.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.command; 2 | 3 | import lombok.AllArgsConstructor; 4 | import lombok.Getter; 5 | import lombok.ToString; 6 | import org.axonframework.modelling.command.TargetAggregateIdentifier; 7 | 8 | @AllArgsConstructor 9 | @ToString 10 | @Getter 11 | public class HolderCreationCommand { 12 | @TargetAggregateIdentifier 13 | private String holderID; 14 | private String holderName; 15 | private String tel; 16 | private String address; 17 | private String company; 18 | } 19 | -------------------------------------------------------------------------------- /common/src/main/java/com/cqrs/command/transfer/JejuBankCancelTransferCommand.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.transfer; 2 | 3 | public class JejuBankCancelTransferCommand extends AbstractCancelTransferCommand { 4 | @Override 5 | public String toString() { 6 | return "JejuBankCancelTransferCommand{" + 7 | "srcAccountID='" + srcAccountID + '\'' + 8 | ", dstAccountID='" + dstAccountID + '\'' + 9 | ", amount=" + amount + 10 | ", transferID='" + transferID + '\'' + 11 | '}'; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /common/src/main/java/com/cqrs/command/transfer/SeoulBankCancelTransferCommand.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.transfer; 2 | 3 | public class SeoulBankCancelTransferCommand extends AbstractCancelTransferCommand { 4 | @Override 5 | public String toString() { 6 | return "SeoulBankCancelTransferCommand{" + 7 | "srcAccountID='" + srcAccountID + '\'' + 8 | ", dstAccountID='" + dstAccountID + '\'' + 9 | ", amount=" + amount + 10 | ", transferID='" + transferID + '\'' + 11 | '}'; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /common/src/main/java/com/cqrs/command/transfer/JejuBankCompensationCancelCommand.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.transfer; 2 | 3 | public class JejuBankCompensationCancelCommand extends AbstractCompensationCancelCommand { 4 | @Override 5 | public String toString() { 6 | return "JejuBankCompensationCancelCommand{" + 7 | "srcAccountID='" + srcAccountID + '\'' + 8 | ", dstAccountID='" + dstAccountID + '\'' + 9 | ", amount=" + amount + 10 | ", transferID='" + transferID + '\'' + 11 | '}'; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /query/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 9090 3 | 4 | spring: 5 | application: 6 | name: eventsourcing-cqrs-query 7 | datasource: 8 | platform: postgres 9 | url: jdbc:postgresql://localhost:5432/query 10 | username: query 11 | password: query 12 | driverClassName: org.postgresql.Driver 13 | jpa: 14 | hibernate: 15 | ddl-auto: update 16 | 17 | axon: 18 | serializer: 19 | general: xstream 20 | axonserver: 21 | servers: localhost:8124 22 | 23 | logging.level.com.cqrs.query : debug 24 | logging.level.org.axonframework : debug 25 | -------------------------------------------------------------------------------- /command/src/main/java/com/cqrs/command/service/TransactionService.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.service; 2 | 3 | import com.cqrs.command.dto.*; 4 | 5 | import java.util.concurrent.CompletableFuture; 6 | 7 | public interface TransactionService { 8 | CompletableFuture createHolder(HolderDTO holderDTO); 9 | CompletableFuture createAccount(AccountDTO accountDTO); 10 | CompletableFuture depositMoney(DepositDTO transactionDTO); 11 | CompletableFuture withdrawMoney(WithdrawalDTO transactionDTO); 12 | String transferMoney(TransferDTO transferDTO); 13 | } 14 | -------------------------------------------------------------------------------- /common/src/main/java/com/cqrs/command/transfer/SeoulBankCompensationCancelCommand.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.transfer; 2 | 3 | 4 | public class SeoulBankCompensationCancelCommand extends AbstractCompensationCancelCommand { 5 | @Override 6 | public String toString() { 7 | return "SeoulBankCompensationCancelCommand{" + 8 | "srcAccountID='" + srcAccountID + '\'' + 9 | ", dstAccountID='" + dstAccountID + '\'' + 10 | ", amount=" + amount + 11 | ", transferID='" + transferID + '\'' + 12 | '}'; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /command/src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | server: 2 | port: 8080 3 | 4 | spring: 5 | application: 6 | name: eventsourcing-cqrs-comamndFactory 7 | datasource: 8 | platform: postgres 9 | url: jdbc:postgresql://localhost:5432/command 10 | username: command 11 | password: command 12 | driverClassName: org.postgresql.Driver 13 | jpa: 14 | hibernate: 15 | ddl-auto: update 16 | 17 | axon: 18 | serializer: 19 | general: xstream 20 | axonserver: 21 | servers: localhost:8124 22 | 23 | 24 | logging.level.com.cqrs.command : debug 25 | logging.level.org.axonframework : debug -------------------------------------------------------------------------------- /command/build.gradle: -------------------------------------------------------------------------------- 1 | ext{ 2 | axonVersion = "4.2.1" 3 | } 4 | dependencies{ 5 | implementation 'org.springframework.boot:spring-boot-starter-web' 6 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 7 | implementation group: 'org.postgresql', name: 'postgresql', version: '42.2.6' 8 | compileOnly 'org.projectlombok:lombok' 9 | annotationProcessor 'org.projectlombok:lombok' 10 | implementation (group: 'org.axonframework', name: 'axon-spring-boot-starter', version: "$axonVersion") 11 | implementation group: 'org.axonframework', name: 'axon-configuration', version: "$axonVersion" 12 | } 13 | -------------------------------------------------------------------------------- /jejuBank/src/main/java/com/cqrs/jeju/service/AccountServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.jeju.service; 2 | 3 | import com.cqrs.jeju.command.AccountCreationCommand; 4 | import com.cqrs.jeju.dto.AccountDTO; 5 | import lombok.RequiredArgsConstructor; 6 | import org.axonframework.commandhandling.gateway.CommandGateway; 7 | import org.springframework.stereotype.Service; 8 | 9 | @Service 10 | @RequiredArgsConstructor 11 | public class AccountServiceImpl implements AccountService { 12 | private final CommandGateway commandGateway; 13 | 14 | @Override 15 | public String createAccount(AccountDTO accountDTO) { 16 | return commandGateway.sendAndWait(new AccountCreationCommand(accountDTO.getAccountID(), accountDTO.getBalance())); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /common/src/main/java/com/cqrs/command/transfer/AbstractTransferCommand.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.transfer; 2 | 3 | 4 | import lombok.*; 5 | import org.axonframework.modelling.command.TargetAggregateIdentifier; 6 | 7 | @ToString 8 | @Getter 9 | public abstract class AbstractTransferCommand { 10 | @TargetAggregateIdentifier 11 | protected String srcAccountID; 12 | protected String dstAccountID; 13 | protected Long amount; 14 | protected String transferID; 15 | 16 | public AbstractTransferCommand create(String srcAccountID, String dstAccountID, Long amount, String transferID) { 17 | this.srcAccountID = srcAccountID; 18 | this.dstAccountID = dstAccountID; 19 | this.transferID = transferID; 20 | this.amount = amount; 21 | return this; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /jejuBank/src/main/java/com/cqrs/jeju/controller/AccountController.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.jeju.controller; 2 | 3 | import com.cqrs.jeju.dto.AccountDTO; 4 | import com.cqrs.jeju.service.AccountService; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.PostMapping; 8 | import org.springframework.web.bind.annotation.RequestBody; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | @RestController 12 | @RequiredArgsConstructor 13 | public class AccountController { 14 | private final AccountService accountService; 15 | 16 | @PostMapping("/account") 17 | public ResponseEntity createAccount(@RequestBody AccountDTO accountDTO){ 18 | return ResponseEntity.ok().body(accountService.createAccount(accountDTO)); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /query/build.gradle: -------------------------------------------------------------------------------- 1 | ext{ 2 | axonVersion = "4.2.1" 3 | } 4 | dependencies{ 5 | implementation 'org.springframework.boot:spring-boot-starter-web' 6 | implementation 'org.springframework.boot:spring-boot-starter-data-jpa' 7 | implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' 8 | implementation group: 'org.postgresql', name: 'postgresql', version: '42.2.6' 9 | compileOnly 'org.projectlombok:lombok' 10 | annotationProcessor 'org.projectlombok:lombok' 11 | implementation group: 'org.axonframework', name: 'axon-spring-boot-starter', version: "$axonVersion" 12 | implementation group: 'org.axonframework', name: 'axon-configuration', version: "$axonVersion" 13 | implementation group: 'org.springframework.retry', name: 'spring-retry' 14 | implementation group: 'io.projectreactor', name: 'reactor-core' 15 | } 16 | -------------------------------------------------------------------------------- /seoulBank/src/main/java/com/cqrs/seoul/component/AccountLoanComponent.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.seoul.component; 2 | 3 | import com.cqrs.query.loan.LoanLimitQuery; 4 | import com.cqrs.query.loan.LoanLimitResult; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.axonframework.queryhandling.QueryHandler; 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | @Slf4j 11 | public class AccountLoanComponent { 12 | @QueryHandler 13 | private LoanLimitResult on(LoanLimitQuery query) { 14 | log.debug("handling {}",query); 15 | return LoanLimitResult.builder() 16 | .holderID(query.getHolderID()) 17 | .balance(query.getBalance()) 18 | .bankName("SeoulBank") 19 | .loanLimit(Double.valueOf(query.getBalance() * 1.5).longValue()) 20 | .build(); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /jejuBank/src/main/java/com/cqrs/jeju/component/AccountLoanComponent.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.jeju.component; 2 | 3 | import com.cqrs.query.loan.LoanLimitQuery; 4 | import com.cqrs.query.loan.LoanLimitResult; 5 | import lombok.extern.slf4j.Slf4j; 6 | import org.axonframework.queryhandling.QueryHandler; 7 | import org.springframework.stereotype.Component; 8 | 9 | @Component 10 | @Slf4j 11 | public class AccountLoanComponent { 12 | 13 | @QueryHandler 14 | private LoanLimitResult on(LoanLimitQuery query) { 15 | log.debug("handling {}",query); 16 | return LoanLimitResult.builder() 17 | .holderID(query.getHolderID()) 18 | .balance(query.getBalance()) 19 | .bankName("JejuBank") 20 | .loanLimit(Double.valueOf(query.getBalance() * 1.2).longValue()) 21 | .build(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /query/src/main/java/com/cqrs/query/entity/HolderAccountSummary.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.query.entity; 2 | 3 | import lombok.*; 4 | 5 | import javax.persistence.Column; 6 | import javax.persistence.Entity; 7 | import javax.persistence.Id; 8 | import javax.persistence.Table; 9 | 10 | @Entity 11 | @Table(name = "MV_ACCOUNT") 12 | @NoArgsConstructor 13 | @AllArgsConstructor 14 | @Builder 15 | @Getter @Setter 16 | @ToString 17 | public class HolderAccountSummary { 18 | @Id 19 | @Column(name = "holder_id", nullable = false) 20 | private String holderId; 21 | @Column(nullable = false) 22 | private String name; 23 | @Column(nullable = false) 24 | private String tel; 25 | @Column(nullable = false) 26 | private String address; 27 | @Column(name = "total_balance", nullable = false) 28 | private Long totalBalance; 29 | @Column(name = "account_cnt", nullable = false) 30 | private Long accountCnt; 31 | } 32 | -------------------------------------------------------------------------------- /common/src/main/java/com/cqrs/command/transfer/AbstractCancelTransferCommand.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.transfer; 2 | 3 | 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.ToString; 8 | import org.axonframework.modelling.command.TargetAggregateIdentifier; 9 | 10 | @ToString 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | @Getter 14 | public abstract class AbstractCancelTransferCommand { 15 | @TargetAggregateIdentifier 16 | protected String srcAccountID; 17 | protected String dstAccountID; 18 | protected Long amount; 19 | protected String transferID; 20 | 21 | public AbstractCancelTransferCommand create(String srcAccountID, String dstAccountID, Long amount, String transferID) { 22 | this.srcAccountID = srcAccountID; 23 | this.dstAccountID = dstAccountID; 24 | this.transferID = srcAccountID; 25 | this.amount = amount; 26 | return this; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /common/src/main/java/com/cqrs/command/transfer/AbstractCompensationCancelCommand.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.transfer; 2 | 3 | 4 | import lombok.AllArgsConstructor; 5 | import lombok.Getter; 6 | import lombok.NoArgsConstructor; 7 | import lombok.ToString; 8 | import org.axonframework.modelling.command.TargetAggregateIdentifier; 9 | 10 | @ToString 11 | @NoArgsConstructor 12 | @AllArgsConstructor 13 | @Getter 14 | public abstract class AbstractCompensationCancelCommand { 15 | @TargetAggregateIdentifier 16 | protected String srcAccountID; 17 | protected String dstAccountID; 18 | protected Long amount; 19 | protected String transferID; 20 | public AbstractCompensationCancelCommand create(String srcAccountID, String dstAccountID, Long amount, String transferID) { 21 | this.srcAccountID = srcAccountID; 22 | this.dstAccountID = dstAccountID; 23 | this.transferID = srcAccountID; 24 | this.amount = amount; 25 | return this; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /command/api.http: -------------------------------------------------------------------------------- 1 | POST http://localhost:8080/holder 2 | Content-Type: application/json 3 | 4 | { 5 | "holderName" : "Kevin", 6 | "tel" : "02-2645-5678", 7 | "address" : "OO시 OO구", 8 | "company" : "Korea" 9 | } 10 | 11 | ### 12 | 13 | POST http://localhost:8080/account 14 | Content-Type: application/json 15 | 16 | { 17 | "holderID" : "b01fae84-e8a5-427d-a5f4-baa7376b7163" 18 | } 19 | 20 | ### 21 | 22 | POST http://localhost:8080/deposit 23 | Content-Type: application/json 24 | 25 | { 26 | "accountID" : "190b3376-b396-4cbf-b7bb-13debb726e34", 27 | "holderID" : "a1dca2e7-f3f6-451a-ba64-00c40bad0dfa", 28 | "amount" : 300 29 | } 30 | 31 | ### 32 | 33 | POST http://localhost:8080/withdrawal 34 | Content-Type: application/json 35 | 36 | { 37 | "accountID" : "0945aef6-8000-42b2-9401-2bd69a2114f3", 38 | "holderID" : "f31a8608-f017-4852-9c08-8b217af69d94", 39 | "amount" : 5 40 | } 41 | 42 | ### 43 | POST localhost:9091/account 44 | Content-Type: application/json 45 | 46 | { 47 | "accountID" : "test", 48 | "balance" : 100 49 | } 50 | -------------------------------------------------------------------------------- /query/src/main/java/com/cqrs/query/version/HolderCreationEventV1.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.query.version; 2 | 3 | import com.cqrs.event.HolderCreationEvent; 4 | import org.axonframework.serialization.SimpleSerializedType; 5 | import org.axonframework.serialization.upcasting.event.IntermediateEventRepresentation; 6 | import org.axonframework.serialization.upcasting.event.SingleEventUpcaster; 7 | 8 | public class HolderCreationEventV1 extends SingleEventUpcaster { 9 | private static SimpleSerializedType targetType = new SimpleSerializedType(HolderCreationEvent.class.getTypeName(), null); 10 | 11 | @Override 12 | protected boolean canUpcast(IntermediateEventRepresentation intermediateRepresentation) { 13 | return intermediateRepresentation.getType().equals(targetType); 14 | } 15 | 16 | @Override 17 | protected IntermediateEventRepresentation doUpcast(IntermediateEventRepresentation intermediateRepresentation) { 18 | return intermediateRepresentation.upcastPayload( 19 | new SimpleSerializedType(targetType.getName(), "1.0"), 20 | org.dom4j.Document.class, 21 | document -> { 22 | document.getRootElement() 23 | .addElement("company") 24 | .setText("N/A"); 25 | return document; 26 | } 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /query/src/main/java/com/cqrs/query/config/AxonConfig.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.query.config; 2 | 3 | import com.cqrs.query.version.HolderCreationEventV1; 4 | import org.axonframework.config.EventProcessingConfigurer; 5 | import org.axonframework.eventhandling.TrackingEventProcessorConfiguration; 6 | import org.axonframework.eventhandling.async.SequentialPerAggregatePolicy; 7 | import org.axonframework.serialization.upcasting.event.EventUpcasterChain; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.context.annotation.Bean; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | @Configuration 13 | public class AxonConfig { 14 | @Autowired 15 | public void configure(EventProcessingConfigurer configurer) { 16 | configurer.registerTrackingEventProcessor( 17 | "accounts", 18 | org.axonframework.config.Configuration::eventStore, 19 | c -> TrackingEventProcessorConfiguration.forSingleThreadedProcessing() 20 | .andBatchSize(100) 21 | ); 22 | 23 | configurer.registerSequencingPolicy("accounts", 24 | configuration -> SequentialPerAggregatePolicy.instance()); 25 | } 26 | 27 | @Bean 28 | public EventUpcasterChain eventUpcasterChain(){ 29 | return new EventUpcasterChain( 30 | new HolderCreationEventV1() 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /common/src/main/java/com/cqrs/command/transfer/factory/TransferComamndFactory.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.transfer.factory; 2 | 3 | 4 | import com.cqrs.command.transfer.AbstractCancelTransferCommand; 5 | import com.cqrs.command.transfer.AbstractCompensationCancelCommand; 6 | import com.cqrs.command.transfer.AbstractTransferCommand; 7 | import lombok.RequiredArgsConstructor; 8 | 9 | @RequiredArgsConstructor 10 | public class TransferComamndFactory { 11 | private final AbstractTransferCommand transferCommand; 12 | private final AbstractCancelTransferCommand abortTransferCommand; 13 | private final AbstractCompensationCancelCommand compensationAbortCommand; 14 | 15 | public void create(String srcAccountID, String dstAccountID, Long amount, String transferID){ 16 | transferCommand.create(srcAccountID, dstAccountID, amount, transferID); 17 | abortTransferCommand.create(srcAccountID, dstAccountID, amount, transferID); 18 | compensationAbortCommand.create(srcAccountID, dstAccountID, amount, transferID); 19 | } 20 | 21 | public AbstractTransferCommand getTransferCommand(){ 22 | return this.transferCommand; 23 | } 24 | public AbstractCancelTransferCommand getAbortTransferCommand(){ 25 | return this.abortTransferCommand; 26 | } 27 | public AbstractCompensationCancelCommand getCompensationAbortCommand(){ 28 | return this.compensationAbortCommand; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /command/src/main/java/com/cqrs/command/aggregate/HolderAggregate.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.aggregate; 2 | 3 | import com.cqrs.command.command.HolderCreationCommand; 4 | import com.cqrs.event.HolderCreationEvent; 5 | import lombok.AllArgsConstructor; 6 | import lombok.Getter; 7 | import lombok.NoArgsConstructor; 8 | import lombok.extern.slf4j.Slf4j; 9 | import org.axonframework.commandhandling.CommandHandler; 10 | import org.axonframework.eventsourcing.EventSourcingHandler; 11 | import org.axonframework.modelling.command.AggregateIdentifier; 12 | import org.axonframework.spring.stereotype.Aggregate; 13 | 14 | import static org.axonframework.modelling.command.AggregateLifecycle.apply; 15 | 16 | @AllArgsConstructor 17 | @NoArgsConstructor 18 | @Aggregate 19 | @Slf4j 20 | public class HolderAggregate { 21 | @AggregateIdentifier 22 | @Getter 23 | private String holderID; 24 | private String holderName; 25 | private String tel; 26 | private String address; 27 | 28 | 29 | @CommandHandler 30 | public HolderAggregate(HolderCreationCommand command) { 31 | log.debug("handling {}", command); 32 | apply(new HolderCreationEvent(command.getHolderID(), command.getHolderName(), command.getTel(), command.getAddress(), command.getCompany())); 33 | } 34 | 35 | @EventSourcingHandler 36 | protected void createHolder(HolderCreationEvent event){ 37 | log.debug("applying {}", event); 38 | this.holderID = event.getHolderID(); 39 | this.holderName = event.getHolderName(); 40 | this.tel = event.getTel(); 41 | this.address = event.getAddress(); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /command/src/main/java/com/cqrs/command/controller/TransactionController.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.controller; 2 | 3 | import com.cqrs.command.dto.*; 4 | import com.cqrs.command.service.TransactionService; 5 | import lombok.RequiredArgsConstructor; 6 | import org.springframework.http.ResponseEntity; 7 | import org.springframework.web.bind.annotation.PostMapping; 8 | import org.springframework.web.bind.annotation.RequestBody; 9 | import org.springframework.web.bind.annotation.RestController; 10 | 11 | import java.util.concurrent.CompletableFuture; 12 | 13 | @RestController 14 | @RequiredArgsConstructor 15 | public class TransactionController { 16 | private final TransactionService transactionService; 17 | 18 | @PostMapping("/holder") 19 | public CompletableFuture createHolder(@RequestBody HolderDTO holderDTO){ 20 | return transactionService.createHolder(holderDTO); 21 | } 22 | 23 | @PostMapping("/account") 24 | public CompletableFuture createAccount(@RequestBody AccountDTO accountDTO){ 25 | return transactionService.createAccount(accountDTO); 26 | } 27 | 28 | @PostMapping("/deposit") 29 | public CompletableFuture deposit(@RequestBody DepositDTO transactionDTO){ 30 | return transactionService.depositMoney(transactionDTO); 31 | } 32 | 33 | @PostMapping("/withdrawal") 34 | public CompletableFuture withdraw(@RequestBody WithdrawalDTO transactionDTO){ 35 | return transactionService.withdrawMoney(transactionDTO); 36 | } 37 | 38 | @PostMapping("/transfer") 39 | public ResponseEntity transfer(@RequestBody TransferDTO transferDTO){ 40 | return ResponseEntity.ok().body(transactionService.transferMoney(transferDTO)); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /command/src/main/java/com/cqrs/command/service/TransactionServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.service; 2 | 3 | import com.cqrs.command.command.*; 4 | import com.cqrs.command.dto.*; 5 | import lombok.RequiredArgsConstructor; 6 | import org.axonframework.commandhandling.gateway.CommandGateway; 7 | import org.springframework.stereotype.Service; 8 | 9 | import java.util.UUID; 10 | import java.util.concurrent.CompletableFuture; 11 | 12 | 13 | @Service 14 | @RequiredArgsConstructor 15 | public class TransactionServiceImpl implements TransactionService { 16 | private final CommandGateway commandGateway; 17 | 18 | @Override 19 | public CompletableFuture createHolder(HolderDTO holderDTO) { 20 | return commandGateway.send(new HolderCreationCommand(UUID.randomUUID().toString() 21 | , holderDTO.getHolderName() 22 | , holderDTO.getTel() 23 | , holderDTO.getAddress() 24 | , holderDTO.getCompany()) 25 | ); 26 | } 27 | 28 | @Override 29 | public CompletableFuture createAccount(AccountDTO accountDTO) { 30 | return commandGateway.send(new AccountCreationCommand(UUID.randomUUID().toString(),accountDTO.getHolderID())); 31 | } 32 | 33 | @Override 34 | public CompletableFuture depositMoney(DepositDTO transactionDTO) { 35 | return commandGateway.send(new DepositMoneyCommand(transactionDTO.getAccountID(), transactionDTO.getHolderID(), transactionDTO.getAmount())); 36 | } 37 | 38 | @Override 39 | public CompletableFuture withdrawMoney(WithdrawalDTO transactionDTO) { 40 | return commandGateway.send(new WithdrawMoneyCommand(transactionDTO.getAccountID(), transactionDTO.getHolderID(), transactionDTO.getAmount())); 41 | } 42 | 43 | @Override 44 | public String transferMoney(TransferDTO transferDTO) { 45 | return commandGateway.sendAndWait(MoneyTransferCommand.of(transferDTO)); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /query/src/main/resources/templates/p2p.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PointToPoint Query Example 6 | 7 | 43 | 44 |
45 | 46 |
47 | 48 |
49 | 50 | -------------------------------------------------------------------------------- /query/src/main/java/com/cqrs/query/controller/HolderAccountController.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.query.controller; 2 | 3 | import com.cqrs.query.entity.HolderAccountSummary; 4 | import com.cqrs.query.service.QueryService; 5 | import com.cqrs.query.loan.LoanLimitResult; 6 | import lombok.NonNull; 7 | import lombok.RequiredArgsConstructor; 8 | import org.springframework.http.ResponseEntity; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.PathVariable; 11 | import org.springframework.web.bind.annotation.PostMapping; 12 | import org.springframework.web.bind.annotation.RestController; 13 | import reactor.core.publisher.Flux; 14 | 15 | import javax.validation.constraints.NotBlank; 16 | import java.util.List; 17 | 18 | @RestController 19 | @RequiredArgsConstructor 20 | public class HolderAccountController { 21 | private final QueryService queryService; 22 | 23 | @PostMapping("/reset") 24 | public void reset() { 25 | queryService.reset(); 26 | } 27 | 28 | @GetMapping("/account/info/{id}") 29 | public ResponseEntity getAccountInfo(@PathVariable(value = "id") @NonNull @NotBlank String holderId){ 30 | return ResponseEntity.ok() 31 | .body(queryService.getAccountInfo(holderId)); 32 | } 33 | 34 | @GetMapping("account/info/subscription/{id}") 35 | public ResponseEntity> getAccountInfoSubscription(@PathVariable(value = "id") @NonNull @NotBlank String holderId){ 36 | return ResponseEntity.ok() 37 | .body(queryService.getAccountInfoSubscription(holderId)); 38 | } 39 | 40 | @GetMapping("account/info/scatter/gather/{id}") 41 | public ResponseEntity> getAccountInfoScatterGather(@PathVariable(value = "id") @NonNull @NotBlank String holderId){ 42 | return ResponseEntity.ok() 43 | .body(queryService.getAccountInfoScatterGather(holderId)); 44 | } 45 | 46 | } 47 | -------------------------------------------------------------------------------- /query/src/main/resources/templates/scatter-gather.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scatter-Gather Query Example 6 | 7 | 43 | 44 |
45 | 46 |
47 | 48 |
49 | 50 | -------------------------------------------------------------------------------- /command/src/main/java/com/cqrs/command/command/MoneyTransferCommand.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.command; 2 | 3 | 4 | import com.cqrs.command.dto.TransferDTO; 5 | import com.cqrs.command.transfer.*; 6 | import com.cqrs.command.transfer.factory.TransferComamndFactory; 7 | import lombok.Builder; 8 | import lombok.Getter; 9 | import lombok.ToString; 10 | import org.axonframework.modelling.command.TargetAggregateIdentifier; 11 | 12 | import java.util.UUID; 13 | import java.util.function.Function; 14 | 15 | @Builder 16 | @ToString 17 | @Getter 18 | public class MoneyTransferCommand { 19 | private String srcAccountID; 20 | @TargetAggregateIdentifier 21 | private String dstAccountID; 22 | private Long amount; 23 | private String transferID; 24 | private BankType bankType; 25 | 26 | public enum BankType{ 27 | JEJU(command -> new TransferComamndFactory(new JejuBankTransferCommand(),new JejuBankCancelTransferCommand(), new JejuBankCompensationCancelCommand())), 28 | SEOUL(command -> new TransferComamndFactory(new SeoulBankTransferCommand(), new SeoulBankCancelTransferCommand(), new SeoulBankCompensationCancelCommand())); 29 | 30 | private Function expression; 31 | BankType(Function expression){ this.expression = expression;} 32 | public TransferComamndFactory getCommandFactory(MoneyTransferCommand command){ 33 | TransferComamndFactory factory = this.expression.apply(command); 34 | factory.create(command.getSrcAccountID(), command.getDstAccountID(), command.amount, command.getTransferID()); 35 | return factory; 36 | } 37 | 38 | } 39 | 40 | public static MoneyTransferCommand of(TransferDTO dto){ 41 | return MoneyTransferCommand.builder() 42 | .srcAccountID(dto.getSrcAccountID()) 43 | .dstAccountID(dto.getDstAccountID()) 44 | .amount(dto.getAmount()) 45 | .bankType(dto.getBankType()) 46 | .transferID(UUID.randomUUID().toString()) 47 | .build(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /command/src/main/java/com/cqrs/command/config/AxonConfig.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.config; 2 | 3 | import com.cqrs.command.aggregate.AccountAggregate; 4 | import org.axonframework.commandhandling.SimpleCommandBus; 5 | import org.axonframework.common.caching.Cache; 6 | import org.axonframework.common.caching.WeakReferenceCache; 7 | import org.axonframework.common.transaction.TransactionManager; 8 | import org.axonframework.eventsourcing.*; 9 | import org.axonframework.eventsourcing.eventstore.EventStore; 10 | import org.axonframework.modelling.command.Repository; 11 | import org.axonframework.springboot.autoconfig.AxonAutoConfiguration; 12 | import org.springframework.boot.autoconfigure.AutoConfigureAfter; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.context.annotation.Configuration; 15 | 16 | @Configuration 17 | @AutoConfigureAfter(AxonAutoConfiguration.class) 18 | public class AxonConfig { 19 | // @Bean 20 | // SimpleCommandBus commandBus(TransactionManager transactionManager){ 21 | // return SimpleCommandBus.builder().transactionManager(transactionManager).build(); 22 | // } 23 | @Bean 24 | public AggregateFactory aggregateFactory(){ 25 | return new GenericAggregateFactory<>(AccountAggregate.class); 26 | } 27 | @Bean 28 | public Snapshotter snapshotter(EventStore eventStore, TransactionManager transactionManager){ 29 | return AggregateSnapshotter 30 | .builder() 31 | .eventStore(eventStore) 32 | .aggregateFactories(aggregateFactory()) 33 | .transactionManager(transactionManager) 34 | .build(); 35 | } 36 | @Bean 37 | public SnapshotTriggerDefinition snapshotTriggerDefinition(EventStore eventStore, TransactionManager transactionManager){ 38 | final int SNAPSHOT_TRHRESHOLD = 50; 39 | return new EventCountSnapshotTriggerDefinition(snapshotter(eventStore,transactionManager),SNAPSHOT_TRHRESHOLD); 40 | } 41 | 42 | @Bean 43 | public Cache cache(){ 44 | return new WeakReferenceCache(); 45 | } 46 | 47 | @Bean 48 | public Repository accountAggregateRepository(EventStore eventStore, SnapshotTriggerDefinition snapshotTriggerDefinition, Cache cache){ 49 | return CachingEventSourcingRepository 50 | .builder(AccountAggregate.class) 51 | .eventStore(eventStore) 52 | .snapshotTriggerDefinition(snapshotTriggerDefinition) 53 | .cache(cache) 54 | .build(); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /query/src/main/resources/templates/subscription.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Subscription Query Example 6 | 7 | 62 | 63 |
64 | 65 | 66 |
67 | 68 |
69 | 70 | -------------------------------------------------------------------------------- /query/src/main/java/com/cqrs/query/service/QueryServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.query.service; 2 | 3 | import com.cqrs.query.entity.HolderAccountSummary; 4 | import com.cqrs.query.loan.LoanLimitQuery; 5 | import com.cqrs.query.loan.LoanLimitResult; 6 | import com.cqrs.query.query.AccountQuery; 7 | import com.cqrs.query.repository.AccountRepository; 8 | import lombok.RequiredArgsConstructor; 9 | import lombok.extern.slf4j.Slf4j; 10 | import org.axonframework.config.Configuration; 11 | import org.axonframework.eventhandling.TrackingEventProcessor; 12 | import org.axonframework.messaging.responsetypes.ResponseTypes; 13 | import org.axonframework.queryhandling.QueryGateway; 14 | import org.axonframework.queryhandling.SubscriptionQueryResult; 15 | import org.springframework.stereotype.Service; 16 | import reactor.core.publisher.Flux; 17 | 18 | import java.util.List; 19 | import java.util.concurrent.TimeUnit; 20 | import java.util.stream.Collectors; 21 | 22 | @RequiredArgsConstructor 23 | @Slf4j 24 | @Service 25 | public class QueryServiceImpl implements QueryService { 26 | private final Configuration configuration; 27 | private final QueryGateway queryGateway; 28 | private final AccountRepository repository; 29 | 30 | @Override 31 | public void reset() { 32 | configuration.eventProcessingConfiguration() 33 | .eventProcessorByProcessingGroup("accounts", 34 | TrackingEventProcessor.class) 35 | .ifPresent(trackingEventProcessor -> { 36 | trackingEventProcessor.shutDown(); 37 | trackingEventProcessor.resetTokens(); // (1) 38 | trackingEventProcessor.start(); 39 | }); 40 | } 41 | 42 | @Override 43 | public HolderAccountSummary getAccountInfo(String holderId) { 44 | AccountQuery accountQuery = new AccountQuery(holderId); 45 | log.debug("handling {}", accountQuery); 46 | return queryGateway.query(accountQuery, ResponseTypes.instanceOf(HolderAccountSummary.class)).join(); 47 | } 48 | 49 | @Override 50 | public Flux getAccountInfoSubscription(String holderId) { 51 | AccountQuery accountQuery = new AccountQuery(holderId); 52 | log.debug("handling {}", accountQuery); 53 | 54 | SubscriptionQueryResult queryResult = queryGateway.subscriptionQuery(accountQuery, 55 | ResponseTypes.instanceOf(HolderAccountSummary.class), 56 | ResponseTypes.instanceOf(HolderAccountSummary.class) 57 | ); 58 | 59 | return Flux.create(emitter -> { 60 | queryResult.initialResult().subscribe(emitter::next); 61 | queryResult.updates() 62 | .doOnNext(holder -> { 63 | log.debug("doOnNext : {}, isCanceled {}", holder, emitter.isCancelled()); 64 | if (emitter.isCancelled()) { 65 | queryResult.close(); 66 | } 67 | }) 68 | .doOnComplete(emitter::complete) 69 | .subscribe(emitter::next); 70 | }); 71 | } 72 | 73 | @Override 74 | public List getAccountInfoScatterGather(String holderId) { 75 | HolderAccountSummary accountSummary = repository.findByHolderId(holderId).orElseThrow(); 76 | 77 | return queryGateway.scatterGather(new LoanLimitQuery(accountSummary.getHolderId(), accountSummary.getTotalBalance()), 78 | ResponseTypes.instanceOf(LoanLimitResult.class), 79 | 30, TimeUnit.SECONDS) 80 | .collect(Collectors.toList()); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /command/src/main/java/com/cqrs/command/aggregate/AccountAggregate.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.aggregate; 2 | 3 | 4 | import com.cqrs.command.command.*; 5 | import com.cqrs.command.event.DepositCompletedEvent; 6 | import com.cqrs.event.AccountCreationEvent; 7 | import com.cqrs.event.DepositMoneyEvent; 8 | import com.cqrs.event.WithdrawMoneyEvent; 9 | import com.cqrs.event.transfer.MoneyTransferEvent; 10 | import lombok.AllArgsConstructor; 11 | import lombok.EqualsAndHashCode; 12 | import lombok.NoArgsConstructor; 13 | import lombok.extern.slf4j.Slf4j; 14 | import org.axonframework.commandhandling.CommandHandler; 15 | import org.axonframework.eventsourcing.EventSourcingHandler; 16 | import org.axonframework.modelling.command.AggregateIdentifier; 17 | import org.axonframework.spring.stereotype.Aggregate; 18 | 19 | 20 | import static org.axonframework.modelling.command.AggregateLifecycle.apply; 21 | 22 | @NoArgsConstructor 23 | @AllArgsConstructor 24 | @Slf4j 25 | @Aggregate 26 | @EqualsAndHashCode 27 | public class AccountAggregate { 28 | @AggregateIdentifier 29 | private String accountID; 30 | private String holderID; 31 | private Long balance; 32 | 33 | @CommandHandler 34 | public AccountAggregate(AccountCreationCommand command) { 35 | log.debug("handling {}", command); 36 | apply(new AccountCreationEvent(command.getHolderID(), command.getAccountID())); 37 | } 38 | 39 | @EventSourcingHandler 40 | protected void createAccount(AccountCreationEvent event) { 41 | log.debug("applying {}", event); 42 | this.accountID = event.getAccountID(); 43 | this.holderID = event.getHolderID(); 44 | this.balance = 0L; 45 | } 46 | 47 | @CommandHandler 48 | protected void depositMoney(DepositMoneyCommand command) { 49 | log.debug("handling {}", command); 50 | if (command.getAmount() <= 0) throw new IllegalStateException("amount >= 0"); 51 | apply(new DepositMoneyEvent(command.getHolderID(), command.getAccountID(), command.getAmount())); 52 | } 53 | 54 | @EventSourcingHandler 55 | protected void depositMoney(DepositMoneyEvent event) { 56 | log.debug("applying {}", event); 57 | this.balance += event.getAmount(); 58 | log.debug("balance {}", this.balance); 59 | } 60 | 61 | @CommandHandler 62 | protected void withdrawMoney(WithdrawMoneyCommand command) { 63 | log.debug("handling {}", command); 64 | if (this.balance - command.getAmount() < 0) throw new IllegalStateException("잔고가 부족합니다."); 65 | else if (command.getAmount() <= 0) throw new IllegalStateException("amount >= 0"); 66 | apply(new WithdrawMoneyEvent(command.getHolderID(), command.getAccountID(), command.getAmount())); 67 | } 68 | 69 | @EventSourcingHandler 70 | protected void withdrawMoney(WithdrawMoneyEvent event) { 71 | log.debug("applying {}", event); 72 | this.balance -= event.getAmount(); 73 | log.debug("balance {}", this.balance); 74 | } 75 | 76 | @CommandHandler 77 | protected void transferMoney(MoneyTransferCommand command) { 78 | log.debug("handling {}", command); 79 | apply(MoneyTransferEvent.builder() 80 | .srcAccountID(command.getSrcAccountID()) 81 | .dstAccountID(command.getDstAccountID()) 82 | .amount(command.getAmount()) 83 | .comamndFactory(command.getBankType().getCommandFactory(command)) 84 | .transferID(command.getTransferID()) 85 | .build()); 86 | } 87 | 88 | @CommandHandler 89 | protected void transferMoney(TransferApprovedCommand command) { 90 | log.debug("handling {}", command); 91 | apply(new DepositMoneyEvent(this.holderID, command.getAccountID(), command.getAmount())); 92 | apply(new DepositCompletedEvent(command.getAccountID(), command.getTransferID())); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /command/src/main/java/com/cqrs/command/saga/TransferManager.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.command.saga; 2 | 3 | import com.cqrs.command.command.TransferApprovedCommand; 4 | import com.cqrs.command.event.DepositCompletedEvent; 5 | import com.cqrs.command.transfer.factory.TransferComamndFactory; 6 | import com.cqrs.event.transfer.*; 7 | import lombok.extern.slf4j.Slf4j; 8 | import org.axonframework.commandhandling.CommandExecutionException; 9 | import org.axonframework.commandhandling.gateway.CommandGateway; 10 | import org.axonframework.modelling.saga.EndSaga; 11 | import org.axonframework.modelling.saga.SagaEventHandler; 12 | import org.axonframework.modelling.saga.SagaLifecycle; 13 | import org.axonframework.modelling.saga.StartSaga; 14 | import org.axonframework.spring.stereotype.Saga; 15 | import org.springframework.beans.factory.annotation.Autowired; 16 | 17 | import java.util.concurrent.TimeUnit; 18 | 19 | @Saga 20 | @Slf4j 21 | public class TransferManager { 22 | @Autowired 23 | private transient CommandGateway commandGateway; 24 | private boolean isExecutingCompensation = false; 25 | private boolean isAbortingCompensation = false; 26 | private TransferComamndFactory comamndFactory; 27 | 28 | @StartSaga 29 | @SagaEventHandler(associationProperty = "transferID") 30 | protected void on(MoneyTransferEvent event) { 31 | 32 | log.debug("Created saga instance"); 33 | log.debug("event : {}", event); 34 | comamndFactory = event.getComamndFactory(); 35 | SagaLifecycle.associateWith("srcAccountID", event.getSrcAccountID()); 36 | 37 | try { 38 | log.info("계좌 이체 시작 : {} ", event); 39 | commandGateway.sendAndWait(comamndFactory.getTransferCommand(), 10, TimeUnit.SECONDS); 40 | } catch (CommandExecutionException e) { 41 | log.error("Failed transfer process. Start cancel transaction"); 42 | cancelTransfer(); 43 | } 44 | } 45 | 46 | private void cancelTransfer() { 47 | isExecutingCompensation = true; 48 | log.info("보상 트랜잭션 요청"); 49 | commandGateway.send(comamndFactory.getAbortTransferCommand()); 50 | } 51 | 52 | @SagaEventHandler(associationProperty = "srcAccountID") 53 | protected void on(CompletedCancelTransferEvent event) { 54 | isExecutingCompensation = false; 55 | if (!isAbortingCompensation) { 56 | log.info("계좌 이체 취소 완료 : {} ", event); 57 | SagaLifecycle.end(); 58 | } 59 | } 60 | 61 | @SagaEventHandler(associationProperty = "srcAccountID") 62 | protected void on(TransferDeniedEvent event) { 63 | log.info("계좌 이체 실패 : {}", event); 64 | log.info("실패 사유 : {}", event.getDescription()); 65 | if(isExecutingCompensation){ 66 | isAbortingCompensation = true; 67 | log.info("보상 트랜잭션 취소 요청 : {}", event); 68 | commandGateway.send(comamndFactory.getCompensationAbortCommand()); 69 | } 70 | else { 71 | SagaLifecycle.end(); 72 | } 73 | } 74 | 75 | @SagaEventHandler(associationProperty = "srcAccountID") 76 | @EndSaga 77 | protected void on(CompletedCompensationCancelEvent event){ 78 | isAbortingCompensation = false; 79 | log.info("보상 트랜잭션 취소 완료 : {}",event); 80 | } 81 | 82 | @SagaEventHandler(associationProperty = "srcAccountID") 83 | protected void on(TransferApprovedEvent event) { 84 | if (!isExecutingCompensation && !isAbortingCompensation) { 85 | log.info("이체 금액 {} 계좌 반영 요청 : {}",event.getAmount(), event); 86 | SagaLifecycle.associateWith("accountID", event.getDstAccountID()); 87 | commandGateway.send(TransferApprovedCommand.builder() 88 | .accountID(event.getDstAccountID()) 89 | .amount(event.getAmount()) 90 | .transferID(event.getTransferID()) 91 | .build()); 92 | } 93 | } 94 | 95 | @SagaEventHandler(associationProperty = "accountID") 96 | @EndSaga 97 | protected void on(DepositCompletedEvent event){ 98 | log.info("계좌 이체 성공 : {}", event); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /jejuBank/src/main/java/com/cqrs/jeju/aggregate/Account.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.jeju.aggregate; 2 | 3 | import com.cqrs.command.transfer.JejuBankCancelTransferCommand; 4 | import com.cqrs.command.transfer.JejuBankCompensationCancelCommand; 5 | import com.cqrs.command.transfer.JejuBankTransferCommand; 6 | import com.cqrs.event.transfer.CompletedCancelTransferEvent; 7 | import com.cqrs.event.transfer.CompletedCompensationCancelEvent; 8 | import com.cqrs.event.transfer.TransferApprovedEvent; 9 | import com.cqrs.event.transfer.TransferDeniedEvent; 10 | import com.cqrs.jeju.command.AccountCreationCommand; 11 | import com.cqrs.jeju.event.AccountCreationEvent; 12 | import lombok.AllArgsConstructor; 13 | import lombok.NoArgsConstructor; 14 | import lombok.extern.slf4j.Slf4j; 15 | import org.axonframework.commandhandling.CommandHandler; 16 | import org.axonframework.eventsourcing.EventSourcingHandler; 17 | import org.axonframework.modelling.command.AggregateIdentifier; 18 | import org.axonframework.spring.stereotype.Aggregate; 19 | 20 | import javax.persistence.Entity; 21 | import javax.persistence.Id; 22 | import java.util.Random; 23 | import java.util.concurrent.TimeUnit; 24 | 25 | import static org.axonframework.modelling.command.AggregateLifecycle.apply; 26 | 27 | @Entity 28 | @Aggregate 29 | @NoArgsConstructor 30 | @AllArgsConstructor 31 | @Slf4j 32 | public class Account { 33 | @AggregateIdentifier 34 | @Id 35 | private String accountID; 36 | private Long balance; 37 | private final transient Random random = new Random(); 38 | 39 | @CommandHandler 40 | public Account(AccountCreationCommand command) throws IllegalAccessException { 41 | log.debug("handling {}", command); 42 | if (command.getBalance() <= 0) 43 | throw new IllegalAccessException("유효하지 않은 입력입니다."); 44 | apply(new AccountCreationEvent(command.getAccountID(), command.getBalance())); 45 | } 46 | 47 | @EventSourcingHandler 48 | protected void on(AccountCreationEvent event) { 49 | log.debug("event {}", event); 50 | this.accountID = event.getAccountID(); 51 | this.balance = event.getBalance(); 52 | } 53 | 54 | @CommandHandler 55 | protected void on(JejuBankTransferCommand command) throws InterruptedException { 56 | if (random.nextBoolean()) 57 | TimeUnit.SECONDS.sleep(15); 58 | 59 | log.debug("handling {}", command); 60 | if (this.balance < command.getAmount()) { 61 | apply(TransferDeniedEvent.builder() 62 | .srcAccountID(command.getSrcAccountID()) 63 | .dstAccountID(command.getDstAccountID()) 64 | .amount(command.getAmount()) 65 | .description("잔고가 부족합니다.") 66 | .transferID(command.getTransferID()) 67 | .build()); 68 | } else { 69 | apply(TransferApprovedEvent.builder() 70 | .srcAccountID(command.getSrcAccountID()) 71 | .dstAccountID(command.getDstAccountID()) 72 | .transferID(command.getTransferID()) 73 | .amount(command.getAmount()) 74 | .build()); 75 | } 76 | } 77 | 78 | @EventSourcingHandler 79 | protected void on(TransferApprovedEvent event) { 80 | log.debug("event {}", event); 81 | this.balance -= event.getAmount(); 82 | } 83 | 84 | @CommandHandler 85 | protected void on(JejuBankCancelTransferCommand command) { 86 | log.debug("handling {}", command); 87 | apply(CompletedCancelTransferEvent.builder() 88 | .srcAccountID(command.getSrcAccountID()) 89 | .dstAccountID(command.getDstAccountID()) 90 | .transferID(command.getTransferID()) 91 | .amount(command.getAmount()) 92 | .build()); 93 | } 94 | 95 | @EventSourcingHandler 96 | protected void on(CompletedCancelTransferEvent event) { 97 | log.debug("event {}", event); 98 | this.balance += event.getAmount(); 99 | } 100 | 101 | @CommandHandler 102 | protected void on(JejuBankCompensationCancelCommand command) { 103 | log.debug("handling {}", command); 104 | apply(CompletedCompensationCancelEvent.builder() 105 | .srcAccountID(command.getSrcAccountID()) 106 | .dstAccountID(command.getDstAccountID()) 107 | .transferID(command.getTransferID()) 108 | .amount(command.getAmount()) 109 | .build()); 110 | } 111 | 112 | @EventSourcingHandler 113 | protected void on(CompletedCompensationCancelEvent event) { 114 | log.debug("event {}", event); 115 | this.balance -= event.getAmount(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /query/src/main/java/com/cqrs/query/projection/HolderAccountProjection.java: -------------------------------------------------------------------------------- 1 | package com.cqrs.query.projection; 2 | 3 | import com.cqrs.event.AccountCreationEvent; 4 | import com.cqrs.event.DepositMoneyEvent; 5 | import com.cqrs.event.HolderCreationEvent; 6 | import com.cqrs.event.WithdrawMoneyEvent; 7 | import com.cqrs.query.entity.HolderAccountSummary; 8 | import com.cqrs.query.query.AccountQuery; 9 | import com.cqrs.query.repository.AccountRepository; 10 | import lombok.AllArgsConstructor; 11 | import lombok.extern.slf4j.Slf4j; 12 | import org.axonframework.config.ProcessingGroup; 13 | import org.axonframework.eventhandling.AllowReplay; 14 | import org.axonframework.eventhandling.EventHandler; 15 | import org.axonframework.eventhandling.ResetHandler; 16 | import org.axonframework.eventhandling.Timestamp; 17 | import org.axonframework.queryhandling.QueryHandler; 18 | import org.axonframework.queryhandling.QueryUpdateEmitter; 19 | import org.springframework.retry.annotation.Backoff; 20 | import org.springframework.retry.annotation.EnableRetry; 21 | import org.springframework.retry.annotation.Retryable; 22 | import org.springframework.stereotype.Component; 23 | 24 | import java.time.Instant; 25 | import java.util.NoSuchElementException; 26 | 27 | 28 | @Component 29 | @EnableRetry 30 | @AllArgsConstructor 31 | @Slf4j 32 | @ProcessingGroup("accounts") 33 | public class HolderAccountProjection { 34 | private final AccountRepository repository; 35 | private final QueryUpdateEmitter queryUpdateEmitter; 36 | 37 | @EventHandler 38 | @Retryable(value = {NoSuchElementException.class}, maxAttempts = 5, backoff = @Backoff(delay = 1000)) 39 | @AllowReplay 40 | protected void on(HolderCreationEvent event, @Timestamp Instant instant) { 41 | log.debug("projecting {} , timestamp : {}", event, instant.toString()); 42 | HolderAccountSummary accountSummary = HolderAccountSummary.builder() 43 | .holderId(event.getHolderID()) 44 | .name(event.getHolderName()) 45 | .address(event.getAddress()) 46 | .tel(event.getTel()) 47 | .totalBalance(0L) 48 | .accountCnt(0L) 49 | .build(); 50 | 51 | 52 | 53 | repository.save(accountSummary); 54 | } 55 | @EventHandler 56 | @Retryable(value = {NoSuchElementException.class}, maxAttempts = 5, backoff = @Backoff(delay = 1000)) 57 | @AllowReplay 58 | protected void on(AccountCreationEvent event, @Timestamp Instant instant) { 59 | log.debug("projecting {} , timestamp : {}", event, instant.toString()); 60 | HolderAccountSummary holderAccount = getHolderAccountSummary(event.getHolderID()); 61 | holderAccount.setAccountCnt(holderAccount.getAccountCnt()+1); 62 | repository.save(holderAccount); 63 | } 64 | 65 | @EventHandler 66 | @Retryable(value = {NoSuchElementException.class}, maxAttempts = 5, backoff = @Backoff(delay = 1000)) 67 | @AllowReplay 68 | protected void on(DepositMoneyEvent event, @Timestamp Instant instant){ 69 | log.debug("projecting {} , timestamp : {}", event, instant.toString()); 70 | HolderAccountSummary holderAccount = getHolderAccountSummary(event.getHolderID()); 71 | holderAccount.setTotalBalance(holderAccount.getTotalBalance() + event.getAmount()); 72 | 73 | queryUpdateEmitter.emit(AccountQuery.class, 74 | query -> query.getHolderId().equals(event.getHolderID()), 75 | holderAccount); 76 | 77 | repository.save(holderAccount); 78 | } 79 | 80 | @EventHandler 81 | @Retryable(value = {NoSuchElementException.class}, maxAttempts = 5, backoff = @Backoff(delay = 1000)) 82 | @AllowReplay 83 | protected void on(WithdrawMoneyEvent event, @Timestamp Instant instant){ 84 | log.debug("projecting {} , timestamp : {}", event, instant.toString()); 85 | HolderAccountSummary holderAccount = getHolderAccountSummary(event.getHolderID()); 86 | holderAccount.setTotalBalance(holderAccount.getTotalBalance() - event.getAmount()); 87 | 88 | queryUpdateEmitter.emit(AccountQuery.class, 89 | query -> query.getHolderId().equals(event.getHolderID()), 90 | holderAccount); 91 | 92 | repository.save(holderAccount); 93 | } 94 | 95 | private HolderAccountSummary getHolderAccountSummary(String holderID) { 96 | log.debug("getHolder : {} ",holderID); 97 | return repository.findByHolderId(holderID) 98 | .orElseThrow(() -> new NoSuchElementException("소유주가 존재하지 않습니다." + holderID)); 99 | } 100 | 101 | @ResetHandler 102 | private void resetHolderAccountInfo(){ 103 | log.debug("reset triggered"); 104 | repository.deleteAll(); 105 | } 106 | 107 | @QueryHandler 108 | public HolderAccountSummary on(AccountQuery query){ 109 | log.debug("handling {}", query); 110 | return repository.findByHolderId(query.getHolderId()).orElse(null); 111 | } 112 | 113 | } 114 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the com.cqrs.command.application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape com.cqrs.command.application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | --------------------------------------------------------------------------------