├── LICENSE
├── README.md
├── account-service
├── pom.xml
└── src
│ └── main
│ ├── java
│ └── io
│ │ └── learning
│ │ └── account
│ │ ├── StartAccountApplication.java
│ │ ├── config
│ │ ├── OpenApiConfig.java
│ │ └── RestConfig.java
│ │ ├── controller
│ │ └── AccountController.java
│ │ ├── devil
│ │ ├── AccountNotFoundException.java
│ │ └── AccountProcessingException.java
│ │ ├── domain
│ │ └── Account.java
│ │ ├── event
│ │ └── AccountTransactionEvent.java
│ │ ├── listener
│ │ ├── AccountTransactionListener.java
│ │ └── DistributedTransactionEventListener.java
│ │ ├── repository
│ │ └── AccountRepository.java
│ │ └── service
│ │ ├── AccountService.java
│ │ └── EventBus.java
│ └── resources
│ ├── application.yml
│ └── schema.sql
├── discovery-server
├── pom.xml
├── src
│ └── main
│ │ └── java
│ │ └── io
│ │ └── learning
│ │ └── discovery
│ │ └── StartDiscoveryServer.java
└── srs
│ └── main
│ └── resources
│ └── application.yml
├── order-service
├── pom.xml
└── src
│ └── main
│ ├── java
│ └── io
│ │ └── learning
│ │ └── order
│ │ ├── StartOrderService.java
│ │ ├── config
│ │ ├── OpenApiConfig.java
│ │ └── RestConfig.java
│ │ ├── controller
│ │ └── OrderController.java
│ │ ├── devil
│ │ ├── InSufficientFundException.java
│ │ └── OrderProcessingException.java
│ │ ├── domain
│ │ └── Order.java
│ │ ├── event
│ │ ├── OrderTransactionEvent.java
│ │ └── listener
│ │ │ ├── DistributedTransactionEventListener.java
│ │ │ └── OrderTransactionListener.java
│ │ ├── repository
│ │ └── OrderRepository.java
│ │ └── service
│ │ ├── EventBus.java
│ │ └── OrderService.java
│ └── resources
│ ├── application.yml
│ └── schema.sql
├── pom.xml
├── product-service
├── pom.xml
└── src
│ └── main
│ ├── java
│ └── io
│ │ └── learning
│ │ └── product
│ │ ├── StartProductApplication.java
│ │ ├── config
│ │ ├── OpenApiConfig.java
│ │ └── RestConfig.java
│ │ ├── controller
│ │ └── ProductController.java
│ │ ├── devil
│ │ ├── ProductNotFoundException.java
│ │ └── ProductProcessingException.java
│ │ ├── domain
│ │ └── ProductEntity.java
│ │ ├── event
│ │ ├── ProductTransactionEvent.java
│ │ └── listener
│ │ │ ├── DistributedTransactionEventListener.java
│ │ │ └── ProductTransactionListener.java
│ │ ├── mapper
│ │ └── ProductMapper.java
│ │ ├── repository
│ │ └── ProductRepository.java
│ │ └── service
│ │ ├── EventBus.java
│ │ └── ProductService.java
│ └── resources
│ ├── application.yml
│ └── schema.sql
├── resources
├── distributed-txn-architecture.drawio
├── distributed-txn-architecture.png
└── distributed-txn-flow.png
├── transaction-server
├── pom.xml
└── src
│ └── main
│ ├── java
│ └── io
│ │ └── learning
│ │ └── transaction
│ │ ├── StartTransactionServerApplication.java
│ │ ├── config
│ │ └── OpenApiConfig.java
│ │ ├── controller
│ │ └── TransactionServerController.java
│ │ └── repo
│ │ └── DistributedTransactionRepo.java
│ └── resources
│ └── application.yml
└── transactional-core
├── pom.xml
└── src
└── main
└── java
└── io
└── learning
└── core
├── domain
├── Account.java
├── DistributedTransaction.java
├── DistributedTransactionParticipant.java
├── DistributedTransactionStatus.java
└── Product.java
└── eventlistener
└── TransactionListener.java
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Anil Jaglan
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Distributed Transactions in Microservices Using 2-Phase Commit (2PC)
2 |
3 | The services in this project are designed with microservice architecture for performing distributed transaction using 2-Phase Commit (2PC).
4 |
5 | Each microservice exposes REST API interfaces that can be accessed through OpenAPI endpoint (/swagger-ui.html)
6 |
7 | ## Tech Stack
8 | 1. SpringBoot
9 | 2. Spring Data JPA
10 | 3. MySQL Database
11 | 4. RabbitMQ
12 |
13 |
14 | ## Pre-Requisites
15 | 1. RabbitMQ
16 | - Start RabbitMQ in Docker with command `docker run -d --hostname my-rabbit --name some-rabbit -p 8080:15672 rabbitmq:3-management` [Learn more](https://hub.docker.com/_/rabbitmq)
17 | 2. MySQL Database
18 | - Start MySQL in Docker with command `docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag` [Learn more](https://hub.docker.com/_/mysql)
19 |
20 | ## Usage
21 | 1. Start `discovery-server`. Default port is 8761.
22 | 2. Start all microservices: `transaction-server`, `account-service`, `order-service`, `product-service`
23 | 3. Add some test data to `account-service` and `product-service`
24 | 4. Send order creation request to `order-service` for testing the flow.
25 |
26 | ## Architecture
27 |
28 | In this microservice-based architecture design, `discovery-server` plays an important role for registering and retrieving the service instances from a centralize location.
29 |
30 | The `transaction-server` is responsible for maintaining transaction status for multiple services for a given transactionId.
31 |
32 | There are three applications: `order-service`, `account-service` and `product-service`.
33 |
34 | The application `order-service` is communicating with `account-service` and `product-service`. All these applications are using MySQL database as a backend store.
35 |
36 | 
37 |
38 |
39 | ## Distributed Transaction Flow
40 |
41 |
42 |
43 | 
--------------------------------------------------------------------------------
/account-service/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 | 4.0.0
7 |
8 | io.learning
9 | spring-boot-distributed-transaction
10 | 0.0.1-SNAPSHOT
11 |
12 |
13 | account-service
14 | account-service
15 |
16 |
17 |
18 |
19 | io.learning
20 | transactional-core
21 |
22 |
23 |
24 |
25 | org.springframework.boot
26 | spring-boot-starter-web
27 |
28 |
29 | org.springframework.boot
30 | spring-boot-starter-actuator
31 |
32 |
33 | org.springframework.boot
34 | spring-boot-starter-data-jpa
35 |
36 |
37 | org.springframework.boot
38 | spring-boot-starter-amqp
39 |
40 |
41 |
42 |
43 | org.springframework.cloud
44 | spring-cloud-starter-netflix-eureka-client
45 |
46 |
47 | org.springframework.cloud
48 | spring-cloud-starter-sleuth
49 |
50 |
51 |
52 |
53 | org.springdoc
54 | springdoc-openapi-ui
55 |
56 |
57 |
58 |
59 | mysql
60 | mysql-connector-java
61 |
62 |
63 |
64 |
65 | org.projectlombok
66 | lombok
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/account-service/src/main/java/io/learning/account/StartAccountApplication.java:
--------------------------------------------------------------------------------
1 | package io.learning.account;
2 |
3 | import org.springframework.amqp.rabbit.annotation.EnableRabbit;
4 | import org.springframework.boot.SpringApplication;
5 | import org.springframework.boot.autoconfigure.SpringBootApplication;
6 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
7 | import org.springframework.scheduling.annotation.EnableAsync;
8 |
9 | @EnableDiscoveryClient
10 | @SpringBootApplication
11 | @EnableAsync
12 | @EnableRabbit
13 | public class StartAccountApplication {
14 |
15 | public static void main(String[] args) {
16 | SpringApplication.run(StartAccountApplication.class, args);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/account-service/src/main/java/io/learning/account/config/OpenApiConfig.java:
--------------------------------------------------------------------------------
1 | package io.learning.account.config;
2 |
3 | import org.springframework.context.annotation.Configuration;
4 |
5 | import io.swagger.v3.oas.annotations.OpenAPIDefinition;
6 | import io.swagger.v3.oas.annotations.info.Info;
7 |
8 | @Configuration
9 | @OpenAPIDefinition(info = @Info(title = "Account Service", description = "REST API for CRUD operation", version = "1.0"))
10 | public class OpenApiConfig {
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/account-service/src/main/java/io/learning/account/config/RestConfig.java:
--------------------------------------------------------------------------------
1 | package io.learning.account.config;
2 |
3 | import org.springframework.cloud.client.loadbalancer.LoadBalanced;
4 | import org.springframework.context.annotation.Bean;
5 | import org.springframework.context.annotation.Configuration;
6 | import org.springframework.web.client.RestTemplate;
7 |
8 | @Configuration
9 | public class RestConfig {
10 |
11 | @Bean
12 | @LoadBalanced
13 | public RestTemplate restTemplate() {
14 | return new RestTemplate();
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/account-service/src/main/java/io/learning/account/controller/AccountController.java:
--------------------------------------------------------------------------------
1 | package io.learning.account.controller;
2 |
3 | import org.springframework.beans.factory.annotation.Autowired;
4 | import org.springframework.web.bind.annotation.GetMapping;
5 | import org.springframework.web.bind.annotation.PathVariable;
6 | import org.springframework.web.bind.annotation.PostMapping;
7 | import org.springframework.web.bind.annotation.PutMapping;
8 | import org.springframework.web.bind.annotation.RequestBody;
9 | import org.springframework.web.bind.annotation.RequestHeader;
10 | import org.springframework.web.bind.annotation.RequestMapping;
11 | import org.springframework.web.bind.annotation.RestController;
12 |
13 | import io.learning.account.domain.Account;
14 | import io.learning.account.service.AccountService;
15 | import io.learning.account.service.EventBus;
16 | import io.swagger.v3.oas.annotations.Operation;
17 | import io.swagger.v3.oas.annotations.tags.Tag;
18 |
19 | /**
20 | *
21 | * Exposes REST API Interface for interacting with AccountService.
22 | *
23 | * @author Anil Jaglan
24 | * @version 1.0
25 | */
26 | @RestController
27 | @RequestMapping("/accounts")
28 | @Tag(name = "Accounts")
29 | public class AccountController {
30 |
31 | @Autowired
32 | private AccountService accountService;
33 |
34 | @Autowired
35 | private EventBus eventBus;
36 |
37 | @PostMapping
38 | @Operation(summary = "Create a new account")
39 | public Account createAccount(@RequestBody Account account) {
40 | return accountService.createAccount(account);
41 | }
42 |
43 | @GetMapping("/customer/{customerId}")
44 | @Operation(summary = "Get account by customer ID")
45 | public Account findByCustomerId(@PathVariable("customerId") Long customerId) {
46 | return accountService.findByCustomerId(customerId);
47 | }
48 |
49 | @PutMapping("/{id}/deposit/{amount}")
50 | @Operation(summary = "Deposit money in customer account")
51 | public Account deposit(@PathVariable("id") Long accountId, @PathVariable("amount") int amount, @RequestHeader("X-Txn-ID") String transactionId) {
52 | accountService.deposit(accountId, amount, transactionId);
53 | return eventBus.receiveEvent(transactionId).getAccount();
54 | }
55 |
56 | @PutMapping("/{id}/withdrawl/{amount}")
57 | @Operation(summary = "Withdraw money from customer account")
58 | public Account withdrawl(@PathVariable("id") Long accountId, @PathVariable("amount") int amount, @RequestHeader("X-Txn-ID") String transactionId) {
59 | accountService.withdrawl(accountId, amount, transactionId);
60 | return eventBus.receiveEvent(transactionId).getAccount();
61 | }
62 |
63 | }
64 |
--------------------------------------------------------------------------------
/account-service/src/main/java/io/learning/account/devil/AccountNotFoundException.java:
--------------------------------------------------------------------------------
1 | package io.learning.account.devil;
2 |
3 | public class AccountNotFoundException extends RuntimeException {
4 |
5 | private static final long serialVersionUID = -1219518716563440469L;
6 |
7 | public AccountNotFoundException(String message) {
8 | super(message);
9 | }
10 |
11 | public AccountNotFoundException(String message, Throwable th) {
12 | super(message, th);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/account-service/src/main/java/io/learning/account/devil/AccountProcessingException.java:
--------------------------------------------------------------------------------
1 | package io.learning.account.devil;
2 |
3 | public class AccountProcessingException extends RuntimeException {
4 |
5 | private static final long serialVersionUID = 8337796345425999746L;
6 |
7 | public AccountProcessingException(String message) {
8 | super(message);
9 | }
10 |
11 | public AccountProcessingException(String message, Throwable th) {
12 | super(message, th);
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/account-service/src/main/java/io/learning/account/domain/Account.java:
--------------------------------------------------------------------------------
1 | package io.learning.account.domain;
2 |
3 | import javax.persistence.Column;
4 | import javax.persistence.Entity;
5 | import javax.persistence.GeneratedValue;
6 | import javax.persistence.GenerationType;
7 | import javax.persistence.Id;
8 | import javax.persistence.Table;
9 |
10 | import lombok.Data;
11 |
12 | @Data
13 | @Entity
14 | @Table(name = "ACCOUNTS")
15 | public class Account {
16 |
17 | @Id
18 | @GeneratedValue(strategy = GenerationType.AUTO)
19 | @Column(name = "ID")
20 | private Long id;
21 |
22 | @Column(name = "CUSTOMER_ID")
23 | private Long customerId;
24 |
25 | @Column(name = "BALANCE")
26 | private Integer balance;
27 |
28 | }
29 |
--------------------------------------------------------------------------------
/account-service/src/main/java/io/learning/account/event/AccountTransactionEvent.java:
--------------------------------------------------------------------------------
1 | package io.learning.account.event;
2 |
3 | import io.learning.account.domain.Account;
4 | import lombok.AllArgsConstructor;
5 | import lombok.Data;
6 |
7 | @Data
8 | @AllArgsConstructor
9 | public class AccountTransactionEvent {
10 |
11 | private String transactionId;
12 |
13 | private Account account;
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/account-service/src/main/java/io/learning/account/listener/AccountTransactionListener.java:
--------------------------------------------------------------------------------
1 | package io.learning.account.listener;
2 |
3 | import static io.learning.core.domain.DistributedTransactionStatus.CONFIRMED;
4 | import static io.learning.core.domain.DistributedTransactionStatus.TO_ROLLBACK;
5 |
6 | import java.util.concurrent.TimeUnit;
7 |
8 | import org.springframework.beans.factory.annotation.Autowired;
9 | import org.springframework.stereotype.Component;
10 | import org.springframework.transaction.event.TransactionPhase;
11 | import org.springframework.transaction.event.TransactionalEventListener;
12 | import org.springframework.web.client.RestTemplate;
13 |
14 | import io.learning.account.devil.AccountProcessingException;
15 | import io.learning.account.event.AccountTransactionEvent;
16 | import io.learning.account.service.EventBus;
17 | import io.learning.core.domain.DistributedTransaction;
18 | import io.learning.core.eventlistener.TransactionListener;
19 | import lombok.extern.slf4j.Slf4j;
20 |
21 | /**
22 | * Class to handle transactional events for {@link AccountTransactionEvent}
23 | * type.
24 | *
25 | * @author Anil Jaglan
26 | * @version 1.0
27 | */
28 | @Component
29 | @Slf4j
30 | public class AccountTransactionListener implements TransactionListener {
31 |
32 | @Autowired
33 | private RestTemplate restTemplate;
34 |
35 | @Autowired
36 | private EventBus eventBus;
37 |
38 | @Override
39 | @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
40 | public void handleEvent(AccountTransactionEvent event) throws AccountProcessingException {
41 | log.debug("Handling event before commit: {}", event);
42 | eventBus.sendEvent(event);
43 |
44 | DistributedTransaction transaction = null;
45 | int count = 3000;
46 | log.info("Waiting for receiving transaction.");
47 | while (count > 0) {
48 | transaction = eventBus.receiveTransaction(event.getTransactionId());
49 | if (transaction == null) {
50 | try {
51 | TimeUnit.MILLISECONDS.sleep(10);
52 | } catch (InterruptedException ex) {
53 | log.error("Error while receiving transaction for: {}. Cause: {}", event.getTransactionId(), ex);
54 | }
55 | --count;
56 | } else {
57 | break;
58 | }
59 | }
60 | if (transaction == null || transaction.getStatus() != CONFIRMED) {
61 | log.info("Transaction received after waiting: {}", transaction);
62 | throw new AccountProcessingException("Distributed transaction wasn't confirmed for txnId: " + event.getTransactionId());
63 | }
64 | }
65 |
66 | @Override
67 | @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
68 | public void handleAfterRollback(AccountTransactionEvent event) {
69 | log.debug("Updating transaction[{}] status to : {} for account-service", event.getTransactionId(), TO_ROLLBACK);
70 | restTemplate.put(
71 | "http://transaction-server/transactions/{transactionId}/participants/{serviceId}/status/{status}",
72 | null,
73 | event.getTransactionId(),
74 | "account-service",
75 | TO_ROLLBACK);
76 | }
77 |
78 | @Override
79 | @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
80 | public void handleAfterCompletion(AccountTransactionEvent event) {
81 | log.debug("Updating transaction[{}] status to: {} for account-service.", event.getTransactionId(), CONFIRMED);
82 | restTemplate.put(
83 | "http://transaction-server/transactions/{transactionId}/participants/{serviceId}/status/{status}",
84 | null,
85 | event.getTransactionId(),
86 | "account-service",
87 | CONFIRMED);
88 | }
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/account-service/src/main/java/io/learning/account/listener/DistributedTransactionEventListener.java:
--------------------------------------------------------------------------------
1 | package io.learning.account.listener;
2 |
3 | import org.springframework.amqp.core.ExchangeTypes;
4 | import org.springframework.amqp.rabbit.annotation.Exchange;
5 | import org.springframework.amqp.rabbit.annotation.Queue;
6 | import org.springframework.amqp.rabbit.annotation.QueueBinding;
7 | import org.springframework.amqp.rabbit.annotation.RabbitListener;
8 | import org.springframework.beans.factory.annotation.Autowired;
9 | import org.springframework.stereotype.Component;
10 |
11 | import io.learning.account.service.EventBus;
12 | import io.learning.core.domain.DistributedTransaction;
13 | import lombok.extern.slf4j.Slf4j;
14 |
15 | @Component
16 | @Slf4j
17 | public class DistributedTransactionEventListener {
18 |
19 | @Autowired
20 | private EventBus eventBus;
21 |
22 | @RabbitListener(bindings = {
23 | @QueueBinding(value = @Queue("txn-events-account"), exchange = @Exchange(type = ExchangeTypes.TOPIC, name = "txn-events"))
24 | })
25 | public void onMessage(DistributedTransaction transaction) {
26 | debug.info("Transaction message received: {}", transaction);
27 | eventBus.sendTransaction(transaction);
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/account-service/src/main/java/io/learning/account/repository/AccountRepository.java:
--------------------------------------------------------------------------------
1 | package io.learning.account.repository;
2 |
3 | import java.util.Optional;
4 |
5 | import org.springframework.data.jpa.repository.JpaRepository;
6 | import org.springframework.stereotype.Repository;
7 |
8 | import io.learning.account.domain.Account;
9 |
10 | @Repository
11 | public interface AccountRepository extends JpaRepository {
12 |
13 | Optional findByCustomerId(Long customerId);
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/account-service/src/main/java/io/learning/account/service/AccountService.java:
--------------------------------------------------------------------------------
1 | package io.learning.account.service;
2 |
3 | import java.util.Optional;
4 |
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import org.springframework.context.ApplicationEventPublisher;
7 | import org.springframework.scheduling.annotation.Async;
8 | import org.springframework.stereotype.Service;
9 | import org.springframework.transaction.annotation.Transactional;
10 |
11 | import io.learning.account.devil.AccountNotFoundException;
12 | import io.learning.account.domain.Account;
13 | import io.learning.account.event.AccountTransactionEvent;
14 | import io.learning.account.repository.AccountRepository;
15 | import lombok.extern.slf4j.Slf4j;
16 |
17 | @Service
18 | @Slf4j
19 | public class AccountService {
20 |
21 | @Autowired
22 | private AccountRepository accountRepository;
23 |
24 | @Autowired
25 | private ApplicationEventPublisher eventPublisher;
26 |
27 | @Transactional
28 | public Account createAccount(Account account) {
29 | return accountRepository.save(account);
30 | }
31 |
32 | public Account findByCustomerId(Long customerId) {
33 | return accountRepository.findByCustomerId(customerId).orElseThrow(() -> new AccountNotFoundException("Account not exists for customer ID: " + customerId));
34 | }
35 |
36 | @Async
37 | @Transactional
38 | public void deposit(Long accountId, int amount, String transactionId) {
39 | transfer(accountId, amount, transactionId);
40 | }
41 |
42 | @Async
43 | @Transactional
44 | public void withdrawl(Long accountId, int amount, String transactionId) {
45 | transfer(accountId, amount * (-1), transactionId);
46 | }
47 |
48 | protected void transfer(Long accountId, int amount, String transactionId) {
49 | log.info("Transferring money to account={}, amount={}, txnId: {}", accountId, amount, transactionId);
50 | Optional accountOpt = accountRepository.findById(accountId);
51 | accountOpt.ifPresent(account -> {
52 | account.setBalance(account.getBalance() + amount);
53 | eventPublisher.publishEvent(new AccountTransactionEvent(transactionId, account));
54 | accountRepository.save(account);
55 | });
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/account-service/src/main/java/io/learning/account/service/EventBus.java:
--------------------------------------------------------------------------------
1 | package io.learning.account.service;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 | import java.util.concurrent.TimeUnit;
6 |
7 | import org.springframework.stereotype.Component;
8 |
9 | import io.learning.account.event.AccountTransactionEvent;
10 | import io.learning.core.domain.DistributedTransaction;
11 | import lombok.extern.slf4j.Slf4j;
12 |
13 | @Component
14 | @Slf4j
15 | public class EventBus {
16 |
17 | private List transactions;
18 |
19 | private List events;
20 |
21 | public EventBus() {
22 | this.transactions = new ArrayList<>();
23 | this.events = new ArrayList<>();
24 | }
25 |
26 | public void sendTransaction(DistributedTransaction transaction) {
27 | transactions.add(transaction);
28 | }
29 |
30 | public DistributedTransaction receiveTransaction(String eventId) {
31 | DistributedTransaction transaction = transactions.stream().filter(tx -> tx.getId().equals(eventId)).findAny().orElse(null);
32 | if (transaction != null) {
33 | transactions.remove(transaction);
34 | }
35 | return transaction;
36 | }
37 |
38 | public void sendEvent(AccountTransactionEvent event) {
39 | events.add(event);
40 | }
41 |
42 | public AccountTransactionEvent receiveEvent(String eventId) {
43 | AccountTransactionEvent event = null;
44 | while (event == null) {
45 | event = events.stream().filter(evnt -> evnt.getTransactionId().equals(eventId)).findAny().orElse(null);
46 | events.remove(event);
47 | if (event != null) {
48 | return event;
49 | }
50 | try {
51 | TimeUnit.MILLISECONDS.sleep(10);
52 | } catch (InterruptedException ex) {
53 | log.error("Error while received event for: {}, Cause:{}", eventId, ex);
54 | }
55 | }
56 | return event;
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/account-service/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | application:
3 | name: account-service
4 | datasource:
5 | url: jdbc:mysql://localhost:3306/distibuted_txn?useSSL=false
6 | username: root
7 | password: root
8 | jpa:
9 | database-platform: org.hibernate.dialect.MySQLDialect
10 | hibernate.ddl-auto: update
11 | rabbitmq:
12 | host: localhost
13 | port: 5672
14 | username: guest
15 | password: guest
16 | server:
17 | port: 8080
18 |
19 | eureka:
20 | client:
21 | service-url:
22 | defaultZone: http://localhost:8761/eureka/
23 | instance:
24 | appname: ${spring.application.name}
25 | prefer-ip-address: true
26 |
27 | springdoc:
28 | swagger-ui.path: /swagger-ui.html
29 |
30 | logging:
31 | level:
32 | org.springframework.data: ERROR
33 | com.netflix: ERROR
34 | pattern:
35 | console: '%d{yyyy-MM-dd HH:mm:ss:SSS} [S=${APP_NAME:-}] [T=%X{traceId}] %-5level %logger{36}.%L %msg%n'
--------------------------------------------------------------------------------
/account-service/src/main/resources/schema.sql:
--------------------------------------------------------------------------------
1 | create table accounts (id bigint not null, balance integer, customer_id bigint, primary key (id));
--------------------------------------------------------------------------------
/discovery-server/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 | 4.0.0
7 |
8 | io.learning
9 | spring-boot-distributed-transaction
10 | 0.0.1-SNAPSHOT
11 |
12 |
13 | discovery-server
14 | discovery-server
15 |
16 |
17 |
18 | org.springframework.cloud
19 | spring-cloud-starter-netflix-eureka-server
20 |
21 |
22 |
23 |
24 |
25 |
26 | org.springframework.boot
27 | spring-boot-maven-plugin
28 | ${springboot.version}
29 |
30 |
31 |
32 | repackage
33 |
34 |
35 |
36 |
37 |
38 | exec
39 |
40 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/discovery-server/src/main/java/io/learning/discovery/StartDiscoveryServer.java:
--------------------------------------------------------------------------------
1 | package io.learning.discovery;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 | import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
6 |
7 | @SpringBootApplication
8 | @EnableEurekaServer
9 | public class StartDiscoveryServer {
10 |
11 | public static void main(String[] args) {
12 | SpringApplication.run(StartDiscoveryServer.class, args);
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/discovery-server/srs/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | application:
3 | name: discovery-server
4 | server:
5 | port: 8761
6 | eureka:
7 | dashboard:
8 | enabled: true
9 | instance:
10 | hostname: localhost
11 | appname: ${spring.application.name}
12 | instanceId: ${spring.application.name}:${vcap.application.instance_id:${spring.application.instance_id:${random.value}}}
13 | client:
14 | registerWithEureka: false
15 | fetchRegistry: false
16 | server:
17 | eviction-interval-timer-in-ms: 30000
--------------------------------------------------------------------------------
/order-service/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 | 4.0.0
7 |
8 | io.learning
9 | spring-boot-distributed-transaction
10 | 0.0.1-SNAPSHOT
11 |
12 |
13 | order-service
14 | order-service
15 |
16 |
17 | io.learning.order.StartOrderService
18 |
19 |
20 |
21 |
22 |
23 | io.learning
24 | transactional-core
25 |
26 |
27 |
28 |
29 | org.springframework.boot
30 | spring-boot-starter-web
31 |
32 |
33 | org.springframework.boot
34 | spring-boot-starter-actuator
35 |
36 |
37 | org.springframework.boot
38 | spring-boot-starter-data-jpa
39 |
40 |
41 | org.springframework.boot
42 | spring-boot-starter-amqp
43 |
44 |
45 |
46 |
47 | org.springframework.cloud
48 | spring-cloud-starter-netflix-eureka-client
49 |
50 |
51 | org.springframework.cloud
52 | spring-cloud-starter-sleuth
53 |
54 |
55 |
56 |
57 | org.springdoc
58 | springdoc-openapi-ui
59 |
60 |
61 |
62 |
63 | mysql
64 | mysql-connector-java
65 |
66 |
67 |
68 |
69 | org.projectlombok
70 | lombok
71 |
72 |
73 |
74 |
75 | ${project.artifactId}
76 |
77 |
78 | org.springframework.boot
79 | spring-boot-maven-plugin
80 |
81 | ${start-class}
82 |
83 | true
84 |
85 |
86 |
87 |
88 |
89 | repackage
90 | build-info
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/order-service/src/main/java/io/learning/order/StartOrderService.java:
--------------------------------------------------------------------------------
1 | package io.learning.order;
2 |
3 | import org.springframework.amqp.rabbit.annotation.EnableRabbit;
4 | import org.springframework.boot.SpringApplication;
5 | import org.springframework.boot.autoconfigure.SpringBootApplication;
6 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
7 |
8 | @SpringBootApplication
9 | @EnableDiscoveryClient
10 | @EnableRabbit
11 | public class StartOrderService {
12 |
13 | public static void main(String[] args) {
14 | SpringApplication.run(StartOrderService.class, args);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/order-service/src/main/java/io/learning/order/config/OpenApiConfig.java:
--------------------------------------------------------------------------------
1 | package io.learning.order.config;
2 |
3 | import org.springframework.context.annotation.Configuration;
4 |
5 | import io.swagger.v3.oas.annotations.OpenAPIDefinition;
6 | import io.swagger.v3.oas.annotations.info.Info;
7 |
8 | @Configuration
9 | @OpenAPIDefinition(info = @Info(title = "Order Service", description = "REST API for CRUD operation", version = "1.0"))
10 | public class OpenApiConfig {
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/order-service/src/main/java/io/learning/order/config/RestConfig.java:
--------------------------------------------------------------------------------
1 | package io.learning.order.config;
2 |
3 | import org.springframework.boot.web.client.RestTemplateBuilder;
4 | import org.springframework.cloud.client.loadbalancer.LoadBalanced;
5 | import org.springframework.context.annotation.Bean;
6 | import org.springframework.context.annotation.Configuration;
7 | import org.springframework.web.client.RestTemplate;
8 |
9 | @Configuration
10 | public class RestConfig {
11 |
12 | @LoadBalanced
13 | @Bean
14 | public RestTemplate restTemplate(RestTemplateBuilder builder) {
15 | return builder.build();
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/order-service/src/main/java/io/learning/order/controller/OrderController.java:
--------------------------------------------------------------------------------
1 | package io.learning.order.controller;
2 |
3 | import org.springframework.beans.factory.annotation.Autowired;
4 | import org.springframework.http.ResponseEntity;
5 | import org.springframework.web.bind.annotation.GetMapping;
6 | import org.springframework.web.bind.annotation.PathVariable;
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 io.learning.order.domain.Order;
12 | import io.learning.order.service.OrderService;
13 | import io.swagger.v3.oas.annotations.Operation;
14 | import io.swagger.v3.oas.annotations.tags.Tag;
15 |
16 | /**
17 | *
18 | * Exposes REST API Interface for interacting with OrderService.
19 | *
20 | * @author Anil Jaglan
21 | * @version 1.0
22 | */
23 | @RestController
24 | @Tag(name = "Orders")
25 | public class OrderController {
26 |
27 | @Autowired
28 | private OrderService orderService;
29 |
30 | @PostMapping("/orders")
31 | @Operation(summary = "Create a new order")
32 | public Order createOrder(@RequestBody Order order) {
33 | return orderService.createOrder(order);
34 | }
35 |
36 | @GetMapping("/orders/{id}")
37 | @Operation(summary = "Get an order")
38 | public ResponseEntity getOrder(@PathVariable("id") Long orderId) {
39 | return orderService.getOrderById(orderId).map(ResponseEntity::ok).orElseGet(() -> ResponseEntity.badRequest().build());
40 | }
41 |
42 | }
43 |
--------------------------------------------------------------------------------
/order-service/src/main/java/io/learning/order/devil/InSufficientFundException.java:
--------------------------------------------------------------------------------
1 | package io.learning.order.devil;
2 |
3 | public class InSufficientFundException extends RuntimeException {
4 |
5 | private static final long serialVersionUID = 5029135185567404773L;
6 |
7 | public InSufficientFundException(String message) {
8 | super(message);
9 | }
10 |
11 | public InSufficientFundException(String message, Throwable th) {
12 | super(message, th);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/order-service/src/main/java/io/learning/order/devil/OrderProcessingException.java:
--------------------------------------------------------------------------------
1 | package io.learning.order.devil;
2 |
3 | public class OrderProcessingException extends RuntimeException {
4 |
5 | private static final long serialVersionUID = -4310868473761742309L;
6 |
7 | public OrderProcessingException(String message) {
8 | super(message);
9 | }
10 |
11 | public OrderProcessingException(String message, Throwable th) {
12 | super(message, th);
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/order-service/src/main/java/io/learning/order/domain/Order.java:
--------------------------------------------------------------------------------
1 | package io.learning.order.domain;
2 |
3 | import javax.persistence.Column;
4 | import javax.persistence.Entity;
5 | import javax.persistence.GeneratedValue;
6 | import javax.persistence.GenerationType;
7 | import javax.persistence.Id;
8 | import javax.persistence.Table;
9 |
10 | import lombok.Data;
11 |
12 | @Entity
13 | @Table(name = "ORDERS")
14 | @Data
15 | public class Order {
16 |
17 | @Id
18 | @GeneratedValue(strategy = GenerationType.AUTO)
19 | @Column(name = "ID")
20 | private Long id;
21 |
22 | @Column(name = "PRODUCT_ID")
23 | private Long productId;
24 |
25 | @Column(name = "QUANTITY")
26 | private Integer quantity;
27 |
28 | @Column(name = "CUSTOMER_ID")
29 | private Long customerId;
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/order-service/src/main/java/io/learning/order/event/OrderTransactionEvent.java:
--------------------------------------------------------------------------------
1 | package io.learning.order.event;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Data;
5 |
6 | @Data
7 | @AllArgsConstructor
8 | public class OrderTransactionEvent {
9 |
10 | private String transactionId;
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/order-service/src/main/java/io/learning/order/event/listener/DistributedTransactionEventListener.java:
--------------------------------------------------------------------------------
1 | package io.learning.order.event.listener;
2 |
3 | import org.springframework.amqp.core.ExchangeTypes;
4 | import org.springframework.amqp.rabbit.annotation.Exchange;
5 | import org.springframework.amqp.rabbit.annotation.Queue;
6 | import org.springframework.amqp.rabbit.annotation.QueueBinding;
7 | import org.springframework.amqp.rabbit.annotation.RabbitListener;
8 | import org.springframework.beans.factory.annotation.Autowired;
9 | import org.springframework.stereotype.Component;
10 |
11 | import io.learning.core.domain.DistributedTransaction;
12 | import io.learning.order.service.EventBus;
13 | import lombok.extern.slf4j.Slf4j;
14 |
15 | @Component
16 | @Slf4j
17 | public class DistributedTransactionEventListener {
18 |
19 | @Autowired
20 | private EventBus eventBus;
21 |
22 | @RabbitListener(bindings = {
23 | @QueueBinding(value = @Queue("txn-events-order"), exchange = @Exchange(type = ExchangeTypes.TOPIC, name = "txn-events"))
24 | })
25 | public void onMessage(DistributedTransaction transaction) {
26 | log.debug("Transaction message received: {}", transaction);
27 | eventBus.sendTransaction(transaction);
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/order-service/src/main/java/io/learning/order/event/listener/OrderTransactionListener.java:
--------------------------------------------------------------------------------
1 | package io.learning.order.event.listener;
2 |
3 | import org.springframework.beans.factory.annotation.Autowired;
4 | import org.springframework.stereotype.Component;
5 | import org.springframework.transaction.event.TransactionPhase;
6 | import org.springframework.transaction.event.TransactionalEventListener;
7 | import org.springframework.web.client.RestTemplate;
8 |
9 | import io.learning.core.domain.DistributedTransactionStatus;
10 | import io.learning.core.eventlistener.TransactionListener;
11 | import io.learning.order.devil.OrderProcessingException;
12 | import io.learning.order.event.OrderTransactionEvent;
13 | import io.learning.order.service.EventBus;
14 | import lombok.extern.slf4j.Slf4j;
15 |
16 | @Component
17 | @Slf4j
18 | public class OrderTransactionListener implements TransactionListener {
19 |
20 | @Autowired
21 | private RestTemplate restTemplate;
22 |
23 | @Autowired
24 | private EventBus eventBus;
25 |
26 | @Override
27 | @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
28 | public void handleEvent(OrderTransactionEvent event) throws OrderProcessingException {
29 | log.debug("Handling event before commit: {}", event);
30 | eventBus.sendEvent(event);
31 | }
32 |
33 | @Override
34 | @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
35 | public void handleAfterRollback(OrderTransactionEvent event) {
36 | log.debug("Handling event after rollback : {}", event);
37 | restTemplate.put(
38 | "http://transaction-server/transactions/{id}/finish/{status}",
39 | null,
40 | event.getTransactionId(),
41 | DistributedTransactionStatus.ROLLBACK);
42 | }
43 |
44 | @Override
45 | @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
46 | public void handleAfterCompletion(OrderTransactionEvent event) {
47 | log.debug("Handling event after completion : {}", event);
48 | restTemplate.put(
49 | "http://transaction-server/transactions/{id}/finish/{status}",
50 | null,
51 | event.getTransactionId(),
52 | DistributedTransactionStatus.CONFIRMED);
53 | }
54 |
55 | }
56 |
--------------------------------------------------------------------------------
/order-service/src/main/java/io/learning/order/repository/OrderRepository.java:
--------------------------------------------------------------------------------
1 | package io.learning.order.repository;
2 |
3 | import org.springframework.data.jpa.repository.JpaRepository;
4 | import org.springframework.stereotype.Repository;
5 |
6 | import io.learning.order.domain.Order;
7 |
8 | @Repository
9 | public interface OrderRepository extends JpaRepository {
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/order-service/src/main/java/io/learning/order/service/EventBus.java:
--------------------------------------------------------------------------------
1 | package io.learning.order.service;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 | import java.util.concurrent.TimeUnit;
6 |
7 | import org.springframework.stereotype.Component;
8 |
9 | import io.learning.core.domain.DistributedTransaction;
10 | import io.learning.order.event.OrderTransactionEvent;
11 | import lombok.extern.slf4j.Slf4j;
12 |
13 | @Component
14 | @Slf4j
15 | public class EventBus {
16 |
17 | private List transactions;
18 |
19 | private List events;
20 |
21 | public EventBus() {
22 | this.transactions = new ArrayList();
23 | this.events = new ArrayList();
24 | }
25 |
26 | public void sendTransaction(DistributedTransaction transaction) {
27 | transactions.add(transaction);
28 | }
29 |
30 | public DistributedTransaction receiveTransaction(String eventId) {
31 | DistributedTransaction transaction = null;
32 | while (transaction == null) {
33 | transaction = transactions.stream().filter(tx -> tx.getId().equals(eventId)).findAny().orElse(null);
34 | transactions.remove(transaction);
35 | if (transaction != null) {
36 | return transaction;
37 | }
38 | try {
39 | TimeUnit.MILLISECONDS.sleep(10);
40 | } catch (InterruptedException ex) {
41 | log.error("Error while received event for: {}, Cause:{}", eventId, ex);
42 | }
43 | }
44 | return transaction;
45 | }
46 |
47 | public void sendEvent(OrderTransactionEvent event) {
48 | events.add(event);
49 | }
50 |
51 | public OrderTransactionEvent receiveEvent(String eventId) {
52 | OrderTransactionEvent event = null;
53 | while (event == null) {
54 | event = events.stream().filter(evnt -> evnt.getTransactionId().equals(eventId)).findAny().orElse(null);
55 | events.remove(event);
56 | if (event != null) {
57 | return event;
58 | }
59 | try {
60 | TimeUnit.MILLISECONDS.sleep(10);
61 | } catch (InterruptedException ex) {
62 | log.error("Error while received event for: {}, Cause:{}", eventId, ex);
63 | }
64 | }
65 | return event;
66 | }
67 |
68 | }
69 |
--------------------------------------------------------------------------------
/order-service/src/main/java/io/learning/order/service/OrderService.java:
--------------------------------------------------------------------------------
1 | package io.learning.order.service;
2 |
3 | import java.util.Optional;
4 | import java.util.Random;
5 |
6 | import org.springframework.beans.factory.annotation.Autowired;
7 | import org.springframework.context.ApplicationEventPublisher;
8 | import org.springframework.http.HttpEntity;
9 | import org.springframework.http.HttpHeaders;
10 | import org.springframework.http.HttpMethod;
11 | import org.springframework.stereotype.Service;
12 | import org.springframework.transaction.annotation.Transactional;
13 | import org.springframework.web.client.RestTemplate;
14 |
15 | import io.learning.core.domain.Account;
16 | import io.learning.core.domain.DistributedTransaction;
17 | import io.learning.core.domain.DistributedTransactionParticipant;
18 | import io.learning.core.domain.DistributedTransactionStatus;
19 | import io.learning.core.domain.Product;
20 | import io.learning.order.devil.InSufficientFundException;
21 | import io.learning.order.devil.OrderProcessingException;
22 | import io.learning.order.domain.Order;
23 | import io.learning.order.event.OrderTransactionEvent;
24 | import io.learning.order.repository.OrderRepository;
25 | import lombok.extern.slf4j.Slf4j;
26 |
27 | @Service
28 | @Slf4j
29 | public class OrderService {
30 |
31 | private static final String TXN_ID_HEADER = "X-Txn-ID";
32 |
33 | private final Random random = new Random();
34 |
35 | @Autowired
36 | private RestTemplate restTemplate;
37 |
38 | @Autowired
39 | private OrderRepository orderRepository;
40 |
41 | @Autowired
42 | private ApplicationEventPublisher eventPublisher;
43 |
44 | public Optional getOrderById(Long orderId) {
45 | return orderRepository.findById(orderId);
46 | }
47 |
48 | @Transactional
49 | public Order createOrder(Order order) {
50 | DistributedTransaction transaction = restTemplate.postForObject("http://transaction-server/transactions", new DistributedTransaction(), DistributedTransaction.class);
51 | log.info("Trasaction created: {}", transaction);
52 | Order savedOrder = orderRepository.save(order);
53 | Product product = updateProduct(transaction.getId(), savedOrder);
54 | log.info("Product updated: {}", product);
55 | int totalAmount = product.getPrice() * order.getQuantity();
56 | Account account = restTemplate.getForObject("http://account-service/accounts/customer/{customerId}", Account.class, order.getCustomerId());
57 | log.info("Account :{}", account);
58 | if (account.getBalance() >= totalAmount) {
59 | log.info("Withdrawing money: {}", totalAmount);
60 | withdraw(transaction.getId(), account.getId(), totalAmount);
61 | } else {
62 | throw new InSufficientFundException("Insufficient funds. Balance: " + account.getBalance() + ", orderAmount: " + totalAmount);
63 | }
64 | eventPublisher.publishEvent(new OrderTransactionEvent(transaction.getId()));
65 | int number = random.nextInt();
66 | if (number % 2 == 0) {
67 | throw new OrderProcessingException("Error while processing your order");
68 | }
69 | return savedOrder;
70 | }
71 |
72 | protected Product updateProduct(String transactionId, Order order) {
73 | addTransactionParticipant(transactionId, "product-service", DistributedTransactionStatus.NEW);
74 | HttpEntity requestEntity = new HttpEntity<>(prepareHeaders(transactionId));
75 | return restTemplate.exchange(
76 | "http://product-service/products/{id}/quantity/{quantity}",
77 | HttpMethod.PUT,
78 | requestEntity,
79 | Product.class,
80 | order.getProductId(),
81 | order.getQuantity()).getBody();
82 | }
83 |
84 | protected Account withdraw(String transactionId, Long accountId, int amount) {
85 | addTransactionParticipant(transactionId, "account-service", DistributedTransactionStatus.NEW);
86 | HttpEntity requestEntity = new HttpEntity<>(prepareHeaders(transactionId));
87 | return restTemplate.exchange("http://account-service/accounts/{id}/withdrawl/{amount}", HttpMethod.PUT, requestEntity, Account.class, accountId, amount).getBody();
88 | }
89 |
90 | protected void addTransactionParticipant(String transactionId, String serviceId, DistributedTransactionStatus status) {
91 | HttpEntity requestEntity = new HttpEntity<>(new DistributedTransactionParticipant(serviceId, status));
92 | restTemplate.exchange("http://transaction-server/transactions/{id}/participants", HttpMethod.PUT, requestEntity, Object.class, transactionId);
93 | }
94 |
95 | private HttpHeaders prepareHeaders(String transactionId) {
96 | HttpHeaders headers = new HttpHeaders();
97 | headers.set(TXN_ID_HEADER, transactionId);
98 | return headers;
99 | }
100 |
101 | }
102 |
--------------------------------------------------------------------------------
/order-service/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | application:
3 | name: order-service
4 | datasource:
5 | url: jdbc:mysql://localhost:3306/distibuted_txn?useSSL=false
6 | username: root
7 | password: root
8 | jpa:
9 | database-platform: org.hibernate.dialect.MySQLDialect
10 | hibernate.ddl-auto: update
11 | rabbitmq:
12 | host: localhost
13 | port: 5672
14 | username: guest
15 | password: guest
16 |
17 | server:
18 | port: 8081
19 |
20 | eureka:
21 | client:
22 | service-url:
23 | defaultZone: http://localhost:8761/eureka/
24 | instance:
25 | appname: ${spring.application.name}
26 | prefer-ip-address: true
27 |
28 | springdoc:
29 | swagger-ui.path: /swagger-ui.html
30 |
31 | logging:
32 | level:
33 | org.springframework.data: ERROR
34 | com.netflix: ERROR
35 | pattern:
36 | console: '%d{yyyy-MM-dd HH:mm:ss:SSS} [S=${APP_NAME:-}] [T=%X{traceId}] %-5level %logger{36}.%L %msg%n'
37 |
--------------------------------------------------------------------------------
/order-service/src/main/resources/schema.sql:
--------------------------------------------------------------------------------
1 | create table orders (id bigint not null, customer_id bigint, product_id bigint, quantity integer, primary key (id));
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 | 4.0.0
6 |
7 | io.learning
8 | spring-boot-distributed-transaction
9 | 0.0.1-SNAPSHOT
10 | pom
11 |
12 | spring-boot-distributed-transaction
13 |
14 |
15 |
16 | anil-jaglan
17 | Anil Jaglan
18 | aniljgln@gmail.com
19 | Asia/Kolkata
20 |
21 |
22 |
23 |
24 | Anil Jaglan
25 |
26 |
27 |
28 |
29 | 1.8
30 | 1.8
31 | 1.8
32 | UTF-8
33 |
34 | 2.3.1.RELEASE
35 | Hoxton.SR5
36 | 1.4.3
37 |
38 |
39 |
40 | transactional-core
41 | discovery-server
42 | order-service
43 | account-service
44 | product-service
45 | transaction-server
46 |
47 |
48 |
49 |
50 |
51 |
52 | io.learning
53 | transactional-core
54 | ${project.version}
55 |
56 |
57 | org.springframework.boot
58 | spring-boot-dependencies
59 | ${springboot.version}
60 | pom
61 | import
62 |
63 |
64 | org.springframework.cloud
65 | spring-cloud-dependencies
66 | ${springcloud.version}
67 | pom
68 | import
69 |
70 |
71 | org.springdoc
72 | springdoc-openapi-ui
73 | ${springdoc.openapi.version}
74 |
75 |
76 |
77 |
--------------------------------------------------------------------------------
/product-service/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 | 4.0.0
7 |
8 | io.learning
9 | spring-boot-distributed-transaction
10 | 0.0.1-SNAPSHOT
11 |
12 |
13 | product-service
14 | product-service
15 |
16 |
17 |
18 |
19 | io.learning
20 | transactional-core
21 |
22 |
23 |
24 |
25 | org.springframework.boot
26 | spring-boot-starter-web
27 |
28 |
29 | org.springframework.boot
30 | spring-boot-starter-actuator
31 |
32 |
33 | org.springframework.boot
34 | spring-boot-starter-data-jpa
35 |
36 |
37 | org.springframework.boot
38 | spring-boot-starter-amqp
39 |
40 |
41 |
42 |
43 | org.springframework.cloud
44 | spring-cloud-starter-netflix-eureka-client
45 |
46 |
47 | org.springframework.cloud
48 | spring-cloud-starter-sleuth
49 |
50 |
51 |
52 |
53 | org.springdoc
54 | springdoc-openapi-ui
55 |
56 |
57 |
58 |
59 | mysql
60 | mysql-connector-java
61 |
62 |
63 |
64 |
65 | org.projectlombok
66 | lombok
67 |
68 |
69 |
70 |
--------------------------------------------------------------------------------
/product-service/src/main/java/io/learning/product/StartProductApplication.java:
--------------------------------------------------------------------------------
1 | package io.learning.product;
2 |
3 | import org.springframework.amqp.rabbit.annotation.EnableRabbit;
4 | import org.springframework.boot.SpringApplication;
5 | import org.springframework.boot.autoconfigure.SpringBootApplication;
6 | import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
7 | import org.springframework.scheduling.annotation.EnableAsync;
8 |
9 | @EnableDiscoveryClient
10 | @SpringBootApplication
11 | @EnableAsync
12 | @EnableRabbit
13 | public class StartProductApplication {
14 |
15 | public static void main(String[] args) {
16 | SpringApplication.run(StartProductApplication.class, args);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/product-service/src/main/java/io/learning/product/config/OpenApiConfig.java:
--------------------------------------------------------------------------------
1 | package io.learning.product.config;
2 |
3 | import org.springframework.context.annotation.Configuration;
4 |
5 | import io.swagger.v3.oas.annotations.OpenAPIDefinition;
6 | import io.swagger.v3.oas.annotations.info.Info;
7 |
8 | @Configuration
9 | @OpenAPIDefinition(info = @Info(title = "Product Service", description = "REST API for CRUD operation", version = "1.0"))
10 | public class OpenApiConfig {
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/product-service/src/main/java/io/learning/product/config/RestConfig.java:
--------------------------------------------------------------------------------
1 | package io.learning.product.config;
2 |
3 | import org.springframework.cloud.client.loadbalancer.LoadBalanced;
4 | import org.springframework.context.annotation.Bean;
5 | import org.springframework.context.annotation.Configuration;
6 | import org.springframework.web.client.RestTemplate;
7 |
8 | @Configuration
9 | public class RestConfig {
10 |
11 | @Bean
12 | @LoadBalanced
13 | public RestTemplate restTemplate() {
14 | return new RestTemplate();
15 | }
16 |
17 | }
18 |
--------------------------------------------------------------------------------
/product-service/src/main/java/io/learning/product/controller/ProductController.java:
--------------------------------------------------------------------------------
1 | package io.learning.product.controller;
2 |
3 | import org.springframework.beans.factory.annotation.Autowired;
4 | import org.springframework.web.bind.annotation.GetMapping;
5 | import org.springframework.web.bind.annotation.PathVariable;
6 | import org.springframework.web.bind.annotation.PostMapping;
7 | import org.springframework.web.bind.annotation.PutMapping;
8 | import org.springframework.web.bind.annotation.RequestBody;
9 | import org.springframework.web.bind.annotation.RequestHeader;
10 | import org.springframework.web.bind.annotation.RequestMapping;
11 | import org.springframework.web.bind.annotation.RestController;
12 |
13 | import io.learning.core.domain.Product;
14 | import io.learning.product.devil.ProductNotFoundException;
15 | import io.learning.product.service.EventBus;
16 | import io.learning.product.service.ProductService;
17 | import io.swagger.v3.oas.annotations.Operation;
18 | import io.swagger.v3.oas.annotations.tags.Tag;
19 |
20 | /**
21 | *
22 | * Exposes REST API Interface for interacting with ProductService.
23 | *
24 | * @author Anil Jaglan
25 | * @version 1.0
26 | */
27 | @RestController
28 | @RequestMapping("/products")
29 | @Tag(name = "Products")
30 | public class ProductController {
31 |
32 | @Autowired
33 | private ProductService productService;
34 |
35 | @Autowired
36 | private EventBus eventBus;
37 |
38 | @PostMapping
39 | @Operation(summary = "Create a new product")
40 | public Product createProduct(@RequestBody Product product) {
41 | return productService.createProduct(product);
42 | }
43 |
44 | @GetMapping("/{id}")
45 | @Operation(summary = "Find product by ID")
46 | public Product findById(@PathVariable("id") Long productId) {
47 | return productService.findById(productId).orElseThrow(() -> new ProductNotFoundException("Product not found for id: " + productId));
48 | }
49 |
50 | @PutMapping("/{id}/quantity/{quantity}")
51 | @Operation(summary = "Update product quantity")
52 | public Product updateQuantity(@RequestHeader("X-Txn-ID") String transactionId, @PathVariable("id") Long productId, @PathVariable("quantity") int quantity) {
53 | productService.updateQuantity(transactionId, productId, quantity);
54 | return eventBus.receiveEvent(transactionId).getProduct();
55 | }
56 |
57 | }
58 |
--------------------------------------------------------------------------------
/product-service/src/main/java/io/learning/product/devil/ProductNotFoundException.java:
--------------------------------------------------------------------------------
1 | package io.learning.product.devil;
2 |
3 | public class ProductNotFoundException extends RuntimeException {
4 |
5 | private static final long serialVersionUID = 1L;
6 |
7 | public ProductNotFoundException(String message) {
8 | super(message);
9 | }
10 |
11 | public ProductNotFoundException(String message, Throwable th) {
12 | super(message, th);
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/product-service/src/main/java/io/learning/product/devil/ProductProcessingException.java:
--------------------------------------------------------------------------------
1 | package io.learning.product.devil;
2 |
3 | public class ProductProcessingException extends RuntimeException {
4 |
5 | private static final long serialVersionUID = -191255860130453309L;
6 |
7 | public ProductProcessingException(String message) {
8 | super(message);
9 | }
10 |
11 | public ProductProcessingException(String message, Throwable th) {
12 | super(message, th);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/product-service/src/main/java/io/learning/product/domain/ProductEntity.java:
--------------------------------------------------------------------------------
1 | package io.learning.product.domain;
2 |
3 | import javax.persistence.Column;
4 | import javax.persistence.Entity;
5 | import javax.persistence.GeneratedValue;
6 | import javax.persistence.GenerationType;
7 | import javax.persistence.Id;
8 | import javax.persistence.Table;
9 |
10 | import lombok.Data;
11 |
12 | @Entity
13 | @Table(name = "PRODUCTS")
14 | @Data
15 | public class ProductEntity {
16 |
17 | @Id
18 | @GeneratedValue(strategy = GenerationType.AUTO)
19 | @Column(name = "ID")
20 | private Long id;
21 |
22 | @Column(name = "NAME")
23 | private String name;
24 |
25 | @Column(name = "QUANTITY")
26 | private Integer quantity;
27 |
28 | @Column(name = "PRICE")
29 | private Integer price;
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/product-service/src/main/java/io/learning/product/event/ProductTransactionEvent.java:
--------------------------------------------------------------------------------
1 | package io.learning.product.event;
2 |
3 | import io.learning.core.domain.Product;
4 | import lombok.AllArgsConstructor;
5 | import lombok.Data;
6 |
7 | @Data
8 | @AllArgsConstructor
9 | public class ProductTransactionEvent {
10 |
11 | private String transactionId;
12 |
13 | private Product product;
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/product-service/src/main/java/io/learning/product/event/listener/DistributedTransactionEventListener.java:
--------------------------------------------------------------------------------
1 | package io.learning.product.event.listener;
2 |
3 | import org.springframework.amqp.core.ExchangeTypes;
4 | import org.springframework.amqp.rabbit.annotation.Exchange;
5 | import org.springframework.amqp.rabbit.annotation.Queue;
6 | import org.springframework.amqp.rabbit.annotation.QueueBinding;
7 | import org.springframework.amqp.rabbit.annotation.RabbitListener;
8 | import org.springframework.beans.factory.annotation.Autowired;
9 | import org.springframework.stereotype.Component;
10 |
11 | import io.learning.core.domain.DistributedTransaction;
12 | import io.learning.product.service.EventBus;
13 | import lombok.extern.slf4j.Slf4j;
14 |
15 | @Component
16 | @Slf4j
17 | public class DistributedTransactionEventListener {
18 |
19 | @Autowired
20 | private EventBus eventBus;
21 |
22 | @RabbitListener(bindings = {
23 | @QueueBinding(value = @Queue("txn-events-product"), exchange = @Exchange(type = ExchangeTypes.TOPIC, name = "txn-events"))
24 | })
25 | public void onMessage(DistributedTransaction transaction) {
26 | log.debug("Transaction message received: {}", transaction);
27 | eventBus.sendTransaction(transaction);
28 | }
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/product-service/src/main/java/io/learning/product/event/listener/ProductTransactionListener.java:
--------------------------------------------------------------------------------
1 | package io.learning.product.event.listener;
2 |
3 | import java.util.concurrent.TimeUnit;
4 |
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import org.springframework.stereotype.Component;
7 | import org.springframework.transaction.event.TransactionPhase;
8 | import org.springframework.transaction.event.TransactionalEventListener;
9 | import org.springframework.web.client.RestTemplate;
10 |
11 | import io.learning.core.domain.DistributedTransaction;
12 | import io.learning.core.domain.DistributedTransactionStatus;
13 | import io.learning.core.eventlistener.TransactionListener;
14 | import io.learning.product.devil.ProductProcessingException;
15 | import io.learning.product.event.ProductTransactionEvent;
16 | import io.learning.product.service.EventBus;
17 | import lombok.extern.slf4j.Slf4j;
18 |
19 | @Component
20 | @Slf4j
21 | public class ProductTransactionListener implements TransactionListener {
22 |
23 | @Autowired
24 | private RestTemplate restTemplate;
25 |
26 | @Autowired
27 | private EventBus eventBus;
28 |
29 | @Override
30 | @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
31 | public void handleEvent(ProductTransactionEvent event) throws ProductProcessingException {
32 | log.debug("Handling event before commit: {}", event);
33 | eventBus.sendEvent(event);
34 |
35 | DistributedTransaction transaction = null;
36 | int count = 3000;
37 | while (count > 0) {
38 | transaction = eventBus.receiveTransaction(event.getTransactionId());
39 | if (transaction == null) {
40 | try {
41 | TimeUnit.MILLISECONDS.sleep(10);
42 | } catch (InterruptedException ex) {
43 | log.error("Error while receiving transaction for: {}. Cause: {}", event.getTransactionId(), ex);
44 | }
45 | --count;
46 | } else {
47 | break;
48 | }
49 | }
50 | if (transaction == null || transaction.getStatus() != DistributedTransactionStatus.CONFIRMED) {
51 | log.info("Transction received after waiting: {}", transaction);
52 | throw new ProductProcessingException("Distributed transaction wasn't confirmed for txnId: " + event.getTransactionId());
53 | }
54 | }
55 |
56 | @Override
57 | @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
58 | public void handleAfterRollback(ProductTransactionEvent event) {
59 | log.debug("Handling event after rollback : {}", event);
60 | restTemplate.put(
61 | "http://transaction-server/transactions/{transactionId}/participants/{serviceId}/status/{status}",
62 | null,
63 | event.getTransactionId(),
64 | "product-service",
65 | DistributedTransactionStatus.TO_ROLLBACK);
66 | }
67 |
68 | @Override
69 | @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
70 | public void handleAfterCompletion(ProductTransactionEvent event) {
71 | log.debug("Handling event after completion : {}", event);
72 | restTemplate.put(
73 | "http://transaction-server/transactions/{transactionId}/participants/{serviceId}/status/{status}",
74 | null,
75 | event.getTransactionId(),
76 | "product-service",
77 | DistributedTransactionStatus.CONFIRMED);
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/product-service/src/main/java/io/learning/product/mapper/ProductMapper.java:
--------------------------------------------------------------------------------
1 | package io.learning.product.mapper;
2 |
3 | import org.springframework.beans.BeanUtils;
4 |
5 | import io.learning.core.domain.Product;
6 | import io.learning.product.domain.ProductEntity;
7 |
8 | public class ProductMapper {
9 |
10 | public static Product map(ProductEntity product) {
11 | Product prod = new Product();
12 | BeanUtils.copyProperties(product, prod);
13 | return prod;
14 | }
15 |
16 | public static ProductEntity map(Product product) {
17 | ProductEntity prod = new ProductEntity();
18 | BeanUtils.copyProperties(product, prod);
19 | return prod;
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/product-service/src/main/java/io/learning/product/repository/ProductRepository.java:
--------------------------------------------------------------------------------
1 | package io.learning.product.repository;
2 |
3 | import org.springframework.data.jpa.repository.JpaRepository;
4 | import org.springframework.stereotype.Repository;
5 |
6 | import io.learning.product.domain.ProductEntity;
7 |
8 | @Repository
9 | public interface ProductRepository extends JpaRepository {
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/product-service/src/main/java/io/learning/product/service/EventBus.java:
--------------------------------------------------------------------------------
1 | package io.learning.product.service;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 | import java.util.concurrent.TimeUnit;
6 |
7 | import org.springframework.stereotype.Component;
8 |
9 | import io.learning.core.domain.DistributedTransaction;
10 | import io.learning.product.event.ProductTransactionEvent;
11 | import lombok.extern.slf4j.Slf4j;
12 |
13 | @Component
14 | @Slf4j
15 | public class EventBus {
16 |
17 | private List transactions;
18 |
19 | private List events;
20 |
21 | public EventBus() {
22 | this.transactions = new ArrayList<>();
23 | this.events = new ArrayList<>();
24 | }
25 |
26 | public void sendTransaction(DistributedTransaction transaction) {
27 | transactions.add(transaction);
28 | }
29 |
30 | public DistributedTransaction receiveTransaction(String eventId) {
31 | DistributedTransaction transaction = transactions.stream().filter(tx -> tx.getId().equals(eventId)).findAny().orElse(null);
32 | if (transaction != null) {
33 | transactions.remove(transaction);
34 | }
35 | return transaction;
36 | }
37 |
38 | public void sendEvent(ProductTransactionEvent event) {
39 | events.add(event);
40 | }
41 |
42 | public ProductTransactionEvent receiveEvent(String eventId) {
43 | ProductTransactionEvent event = null;
44 | while (event == null) {
45 | event = events.stream().filter(evnt -> evnt.getTransactionId().equals(eventId)).findAny().orElse(null);
46 | events.remove(event);
47 | if (event != null) {
48 | return event;
49 | }
50 | try {
51 | TimeUnit.MILLISECONDS.sleep(10);
52 | } catch (InterruptedException ex) {
53 | log.error("Error while received event for: {}, Cause:{}", eventId, ex);
54 | }
55 | }
56 | return event;
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/product-service/src/main/java/io/learning/product/service/ProductService.java:
--------------------------------------------------------------------------------
1 | package io.learning.product.service;
2 |
3 | import java.util.Optional;
4 |
5 | import org.springframework.beans.factory.annotation.Autowired;
6 | import org.springframework.context.ApplicationEventPublisher;
7 | import org.springframework.scheduling.annotation.Async;
8 | import org.springframework.stereotype.Service;
9 | import org.springframework.transaction.annotation.Isolation;
10 | import org.springframework.transaction.annotation.Transactional;
11 |
12 | import io.learning.core.domain.Product;
13 | import io.learning.product.devil.ProductProcessingException;
14 | import io.learning.product.event.ProductTransactionEvent;
15 | import io.learning.product.mapper.ProductMapper;
16 | import io.learning.product.repository.ProductRepository;
17 | import lombok.extern.slf4j.Slf4j;
18 |
19 | @Service
20 | @Slf4j
21 | public class ProductService {
22 |
23 | @Autowired
24 | private ProductRepository productRepository;
25 |
26 | @Autowired
27 | private ApplicationEventPublisher eventPublisher;
28 |
29 | @Transactional
30 | public Product createProduct(Product product) {
31 | return ProductMapper.map(productRepository.save(ProductMapper.map(product)));
32 | }
33 |
34 | public Optional findById(Long productId) {
35 | return productRepository.findById(productId).map(ProductMapper::map);
36 | }
37 |
38 | @Async
39 | @Transactional(isolation = Isolation.SERIALIZABLE)
40 | public void updateQuantity(String transactionId, Long productId, int quantity) {
41 | log.info("Updating product quantity with id: {}, quantity: {}", productId, quantity);
42 | findById(productId).ifPresent(prod -> {
43 | if (prod.getQuantity() < quantity) {
44 | throw new ProductProcessingException("Insufficient product quantity. Available: " + prod.getQuantity() + ", Demand: " + quantity);
45 | }
46 | prod.setQuantity(prod.getQuantity() - quantity);
47 | eventPublisher.publishEvent(new ProductTransactionEvent(transactionId, prod));
48 | productRepository.save(ProductMapper.map(prod));
49 | });
50 | }
51 |
52 | }
53 |
--------------------------------------------------------------------------------
/product-service/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | application:
3 | name: product-service
4 | datasource:
5 | url: jdbc:mysql://localhost:3306/distibuted_txn?useSSL=false
6 | username: root
7 | password: root
8 | jpa:
9 | database-platform: org.hibernate.dialect.MySQLDialect
10 | hibernate.ddl-auto: update
11 | rabbitmq:
12 | host: localhost
13 | port: 5672
14 | username: guest
15 | password: guest
16 |
17 | server:
18 | port: 8082
19 |
20 | eureka:
21 | client:
22 | service-url:
23 | defaultZone: http://localhost:8761/eureka/
24 | instance:
25 | appname: ${spring.application.name}
26 | prefer-ip-address: true
27 |
28 | springdoc:
29 | swagger-ui.path: /swagger-ui.html
30 |
31 | logging:
32 | level:
33 | org.springframework.data: ERROR
34 | com.netflix: ERROR
35 | pattern:
36 | console: '%d{yyyy-MM-dd HH:mm:ss:SSS} [S=${APP_NAME:-}] [T=%X{traceId}] %-5level %logger{36}.%L %msg%n'
--------------------------------------------------------------------------------
/product-service/src/main/resources/schema.sql:
--------------------------------------------------------------------------------
1 | create table products (id bigint not null, name varchar(255), price integer, quantity integer, primary key (id));
--------------------------------------------------------------------------------
/resources/distributed-txn-architecture.drawio:
--------------------------------------------------------------------------------
1 | 7V1bl5s4Ev41frQP98tjX9K7D8lOz3b2JJmXORhoW9MYcYScdufXr2SQQQgMtrmoO+6HBITApr6qT1WlkjzT7za7fyEvWX+BQRjNNCXYzfT7maapjuOQ/2jLW9ZiOkrWsEIgyDsVDU/gV5g3sm5bEIQp1xFDGGGQ8I0+jOPQx1ybhxB85bs9w4j/1MRbhULDk+9FYus3EOB11uqYStH+7xCs1uyTVSW/svFY57whXXsBfC016Z9m+h2CEGdHm91dGFHhMblk9z00XD18MRTGuMsNf/z4C36zvobb7/f/uf0F5i/B9ttcVfLn/PSibf7K+dfFb0wGCG7jIKSPUWf67esa4PAp8Xx69ZWgTtrWeBPll59BFN3BCCJyHsM4pE0wxjmyqkPOAy9dHx5HTx49jEMU71s0xSCt+ZcKEQ53je+rHqRI1C+EmxCjN9Ilv2GuMpDeeJV6LXB0mTauyxjqRt7q5cqzOjy8kC85yEV8griddmGviLSTxtfP1dxbsu7KqWIhJshJRddEsah1YjGHEkrfGthJd5rhESU3mWT0dskQVknoIdjsiex2//9NmmRcqJAWj508gx0V4S2VDCAM99lbhtEjTAEGkNreEmIMN6UONxFY0QsYJuzJ5GyNMeXeG/pq2kOCvBWhOuCDGIcr5GGIFoQCwnTxClGQoDBNFz55qvagKapN/lPISz2kCQLxau5HcBss/klWQ6CWP0XntN1YqFb5zxZ031AXdT3KeDd06R1+W4D/HqQ+JDKi7/cUInIkKAQRH+btIcUIvoRVThZp2svh9om4yYNFPdiAIKAfU2uAhYkqVYrf83/+DdXKeNCZ7M+Avsr/Fg+b5YjEp9WNBw339U+EmgTDg14RmlszPNhjkqDb8/AgqmYvqpiBd3zwGFVuhvyDR/CLME8+PKRk5KB9tAccboiGP6iaoRuq6szzwWJJPOV5BFdwkcSXDRhNULHHVCxAFQxA1xeGKkKpD+YhiS7Sje8Tpcf5OACIrv/WA0ELpAbvBahWN+bXBgO0g2c3NNPPDV7PawOBURlL7UBZUlC9LhnVq+aV60/ESl6utwQs/0ABdfSvTN8BUOmYXgziRmf6qk9v2JMzfYdEmBRMb8vG9B2iod+V6RuwkpbpmSGXsHxEMNj6V6++G6Sycb3mCHCFwSpksoEIr4mix170qWitCLbo8xlSE9sL9J8Q47dcot4WQx7/cAfw9/x2evyDHi/M/Ox+V7p0/8ZOYvK+39kD6EnpLnpa3LY/e2vEXcCZvnEjynlTCrfID49JMmc17KFVeFRD6hUEhZGHwU/+i/SPtz0p3geMf5SutOBdgvgHh3A93lWIOfx7xNt9F3C78sCtnAW3Kol5M/dPbrzZwD8N3ursDDpXZlLSOUvsSI636Eq34z26RZ+GcBVUf4t+DkHgnRF2J0VYPwNhTmSjj9eXwT2JDTdMyo4EcYdU6ND5FlPpkFkftZCECUH2fEuGnkQVOJo4JfEVeXHq+fsMybUK4xhq3cN0vabKZrgwXcywC/gJGbIkRIB8PEXknnwGSNLwsWgqAy0AeCj85CBhdM3gn2m6odn27S1pj2ga7tbzX1Z7PEtdnvd/FfQ0+iFED0C8+roflPQiN3fI0YHNikgrAkvyL1jS3Nwygv6Lv/ZATNNzhVb/vfFicg9apD9XnEpdVlzXoiUupySuoCO6IaoIa+t/FDknsu/LUTjP83/fgX1mgpM5Deck7mSCuy2wHwvuzoH9tHgb58QB50BcwKVdhBfPwRx6RXhhd48vpogLLoY8v/URgn3dExt+KpObulkpz8zeIL+rUJwbhLy3UreEdkibP8c0+Lp5g19uQQ6yJxZaeXjHCxRVgmhGV/kXd2vWUYxc2djBgTttzUpvrrDRIYAZV1Yd6g9+0wnjJqyknTA2xKqJaxnoKZDKNmFsdKjmyOidB6gcCA7J/DzxO/rUxM8eLCPxNyS2p5NVh2V1vyvxN2AlLfGbYvr2WhPaHVDZaN+UoPrfqIRNEpB739X//emXKVvBv3kt+D8VK3nJXYxmr2Wgp0AqG71bpgCXpGWgVYhS7CF8Q3dNoQoSeWkKfNb8AKIirRmwTrlSkZb8es/ZRVOph/7CpOG8mjV0GKOyZ2TpTCFr2FdizxKN/g6FHiaWruSuXY0K7Xl5dny2sdGcUZiCX7nHQFHKE57k4ebtzLyvqINyzGK7uB9HTKPRkpWFrjgWb83Z2bl4vzG8K8YOn5/TcBhkxYSbKoKZzSC3+zDCEM17QJ0RO2FZKCuDY5ZhGCKfuiKdsrb+d8EQXZtRy261s+puFRKTlCd8FormtEz6cOybE2uZehWeegt+Lti3Tif2H1KqVRiGojtMAJmTFobZost8YNyvu/hj8m1mO822TvTUNfnKiwNVXjzEmpXn6mNRsC3G1zUcIi8FWzwitquJDOzUMPBQ+9LY1pQMfFZ9RGVdk91WODsQ9/ZJs11LK6wGmz/Nnk+dHrcrNa+2Ypb1rrW/qqojzKfbonP2uF1GIF2Txk8/w/wVP+BAYLUOBKbKe95z7bJxYASe7zx9dok8W+bItIoeK05NItWqyT8MtsmkpbWLZaJEqt1hlmxUWdkdNiIYL5F63LOoT7Nukwh6weIVvBAHPwDeAiJa6EvPE3pOjn242cA4pdnX9XZDC4DpRow2Ae3hv95yCfCXP//e515pwS/NzTpKspsLly5OzTahz7apqFrSQjXc4s8W92pU2V6mfCC6sIcKIphtl9TlM0hxWB8+fKCVoT1lBqsqPVQdKqPAVmfJaBgXx4lJnZrMkOSO9YkL0t6BX905fWFMqipiTPu/JMjSF39uvRgD/PYxXVfnuANGRglT5WeA2I7S7ydp7IguW02JurwZC83iYweHzcmVBmv2VG4ObjBzEYsJDuayz/YpT9jD2/poT8Jxe8jccPZdHINfQLRwXauF3AfPLHddWtCUWS6iW40nCPMyghiBECZNuY2xVH0gjR7Ko+zsJlycfruM98QE15EA5f37Bq1pLV1x5Dd2MQnxDeB1gLxX0vqF2Ea9azcaG5ywskwyOpiCAZpiyqLIgeXyWabjQg3d1d4woL6KeVfjPXmrqkP1sYyAq4lFDqP6q+xXFqSw8M4ppOy3mkpFDqra5jNeixyGHP7ZTHHJMh9AnE1vfdgiB7ehRrCUIGALKHJz12T3CFxx8U5NUanEDMtWH7HZYDZkTMav56z4n9iDqtQwaB8touoa3U8bUbliJukjR1Tu8Vp7ElGpLgsy+ynRHYFNJUqmdPatLN2acb6Vop2TjpM/rmK7OcjuWomZle4Z5fdPC20xrWaaQxTujxbTuhL8NGvl9wqtmkoi06ypjhlumWHfP9PRXyWR2/5brSPLSkyKXHeK7Ioam0a0Jdsq0hLrfQQAr1tF9r5VZJua6FJtFumKlv+0X9V8S3/bXVPuSbiAwHKLiWSIr8CRwg3yqW36eItkXeRb/fV2nh16yBXolWx4zf5ihqGJgJ5RFEpOEQWl5DqQV15/gQF1zz79Hw==
--------------------------------------------------------------------------------
/resources/distributed-txn-architecture.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anil-jaglan/springboot-microservices-distributed-transaction-2pc/b9e8c6cf1959b31669657c4128c45d13faba206b/resources/distributed-txn-architecture.png
--------------------------------------------------------------------------------
/resources/distributed-txn-flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anil-jaglan/springboot-microservices-distributed-transaction-2pc/b9e8c6cf1959b31669657c4128c45d13faba206b/resources/distributed-txn-flow.png
--------------------------------------------------------------------------------
/transaction-server/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 | 4.0.0
7 |
8 | io.learning
9 | spring-boot-distributed-transaction
10 | 0.0.1-SNAPSHOT
11 |
12 |
13 | transaction-server
14 | transaction-server
15 |
16 |
17 |
18 |
19 | io.learning
20 | transactional-core
21 |
22 |
23 |
24 |
25 | org.springframework.boot
26 | spring-boot-starter-web
27 |
28 |
29 | org.springframework.boot
30 | spring-boot-starter-actuator
31 |
32 |
33 | org.springframework.boot
34 | spring-boot-starter-amqp
35 |
36 |
37 |
38 |
39 | org.springframework.cloud
40 | spring-cloud-starter-netflix-eureka-client
41 |
42 |
43 | org.springframework.cloud
44 | spring-cloud-starter-sleuth
45 |
46 |
47 |
48 |
49 | org.springdoc
50 | springdoc-openapi-ui
51 |
52 |
53 |
54 |
55 | org.projectlombok
56 | lombok
57 |
58 |
59 |
60 |
--------------------------------------------------------------------------------
/transaction-server/src/main/java/io/learning/transaction/StartTransactionServerApplication.java:
--------------------------------------------------------------------------------
1 | package io.learning.transaction;
2 |
3 | import org.springframework.amqp.core.TopicExchange;
4 | import org.springframework.boot.SpringApplication;
5 | import org.springframework.boot.autoconfigure.SpringBootApplication;
6 | import org.springframework.context.annotation.Bean;
7 |
8 | @SpringBootApplication
9 | public class StartTransactionServerApplication {
10 |
11 | public static void main(String[] args) {
12 | SpringApplication.run(StartTransactionServerApplication.class, args);
13 | }
14 |
15 | @Bean
16 | public TopicExchange topic() {
17 | return new TopicExchange("txn-events");
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/transaction-server/src/main/java/io/learning/transaction/config/OpenApiConfig.java:
--------------------------------------------------------------------------------
1 | package io.learning.transaction.config;
2 |
3 | import org.springframework.context.annotation.Configuration;
4 |
5 | import io.swagger.v3.oas.annotations.OpenAPIDefinition;
6 | import io.swagger.v3.oas.annotations.info.Info;
7 |
8 | @Configuration
9 | @OpenAPIDefinition(info = @Info(title = "Transactional Server", description = "REST API for managing transaction", version = "1.0"))
10 | public class OpenApiConfig {
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/transaction-server/src/main/java/io/learning/transaction/controller/TransactionServerController.java:
--------------------------------------------------------------------------------
1 | package io.learning.transaction.controller;
2 |
3 | import java.util.List;
4 | import java.util.Optional;
5 | import java.util.stream.Collectors;
6 |
7 | import org.springframework.amqp.rabbit.core.RabbitTemplate;
8 | import org.springframework.beans.factory.annotation.Autowired;
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.PutMapping;
13 | import org.springframework.web.bind.annotation.RequestBody;
14 | import org.springframework.web.bind.annotation.RequestMapping;
15 | import org.springframework.web.bind.annotation.RestController;
16 |
17 | import io.learning.core.domain.DistributedTransaction;
18 | import io.learning.core.domain.DistributedTransactionParticipant;
19 | import io.learning.core.domain.DistributedTransactionStatus;
20 | import io.learning.transaction.repo.DistributedTransactionRepo;
21 | import io.swagger.v3.oas.annotations.Operation;
22 | import io.swagger.v3.oas.annotations.tags.Tag;
23 | import lombok.extern.slf4j.Slf4j;
24 |
25 | /**
26 | *
27 | * Exposes REST API Interface for interacting with TransactionServer
28 | * Application.
29 | *
30 | * @author Anil Jaglan
31 | * @version 1.0
32 | */
33 | @RestController
34 | @RequestMapping("/transactions")
35 | @Tag(name = "Transactions")
36 | @Slf4j
37 | public class TransactionServerController {
38 |
39 | @Autowired
40 | private DistributedTransactionRepo repository;
41 |
42 | @Autowired
43 | private RabbitTemplate rabbitTemplate;
44 |
45 | @PostMapping
46 | @Operation(summary = "Add a new transaction")
47 | public DistributedTransaction add(@RequestBody DistributedTransaction transaction) {
48 | return repository.save(transaction);
49 | }
50 |
51 | @GetMapping("/{id}")
52 | @Operation(summary = "Get transaction by id")
53 | public Optional findById(@PathVariable("id") String id) {
54 | return repository.findById(id);
55 | }
56 |
57 | @GetMapping
58 | @Operation(summary = "Get all transactions")
59 | public List findAll() {
60 | return repository.findAll();
61 | }
62 |
63 | @PutMapping("/{id}/finish/{status}")
64 | @Operation(summary = "Finish a transaction with status")
65 | public void finish(@PathVariable("id") String id, @PathVariable("status") DistributedTransactionStatus status) {
66 | log.info("Finishing transaction as id: {}, status: {}", id, status);
67 | repository.findById(id).ifPresent(txn -> {
68 | txn.setStatus(status);
69 | repository.update(txn);
70 | log.info("Publishing transaction[{}] finish event with status: {}", id, status);
71 | this.publishEvent(new DistributedTransaction(id, status));
72 | });
73 | }
74 |
75 | @PutMapping("/{id}/participants")
76 | @Operation(summary = "Add participant in a transaction")
77 | public void addParticipant(@PathVariable("id") String id, @RequestBody DistributedTransactionParticipant participant) {
78 | log.info("Adding participant in transaction: {}, participant: {}", id, participant);
79 | repository.findById(id).ifPresent(txn -> {
80 | txn.getParticipants().add(participant);
81 | repository.update(txn);
82 | log.info("All participant: {}", txn.getParticipants());
83 | });
84 | }
85 |
86 | @PutMapping("/{id}/participants/{serviceId}/status/{status}")
87 | @Operation(summary = "Update participant status in a transaction")
88 | public void updateParticipant(@PathVariable("id") String id, @PathVariable("serviceId") String serviceId, @PathVariable("status") DistributedTransactionStatus status) {
89 | log.info("Updating participant id: {}, serviceId: {}, status: {}", id, serviceId, status);
90 | repository.findById(id).ifPresent(txn -> {
91 | List participants = txn.getParticipants().stream().map(p -> {
92 | if (p.getServiceId().equals(serviceId)) {
93 | p.setStatus(status);
94 | }
95 | return p;
96 | }).collect(Collectors.toList());
97 | txn.setParticipants(participants);
98 | repository.update(txn);
99 | this.publishEvent(new DistributedTransaction(id, status));
100 | log.info("Publishing transaction [{}] event with status: {}", id, status);
101 | });
102 | }
103 |
104 | protected void publishEvent(DistributedTransaction transaction) {
105 | rabbitTemplate.convertAndSend("txn-events", "txn-events", transaction);
106 | }
107 |
108 | }
109 |
--------------------------------------------------------------------------------
/transaction-server/src/main/java/io/learning/transaction/repo/DistributedTransactionRepo.java:
--------------------------------------------------------------------------------
1 | package io.learning.transaction.repo;
2 |
3 | import java.util.ArrayList;
4 | import java.util.Collections;
5 | import java.util.List;
6 | import java.util.Optional;
7 | import java.util.UUID;
8 |
9 | import org.springframework.stereotype.Repository;
10 |
11 | import io.learning.core.domain.DistributedTransaction;
12 |
13 | @Repository
14 | public class DistributedTransactionRepo {
15 |
16 | private List transactions = new ArrayList<>();
17 |
18 | public Optional findById(String id) {
19 | return transactions.stream().filter(tx -> tx.getId().equalsIgnoreCase(id)).findAny();
20 | }
21 |
22 | public List findAll() {
23 | return Collections.unmodifiableList(transactions);
24 | }
25 |
26 | public DistributedTransaction save(DistributedTransaction transaction) {
27 | transaction.setId(UUID.randomUUID().toString());
28 | transactions.add(transaction);
29 | return transaction;
30 | }
31 |
32 | public DistributedTransaction update(DistributedTransaction transaction) {
33 | int index = transactions.indexOf(transaction);
34 | if (index >= 0) {
35 | transactions.remove(index);
36 | transactions.add(index, transaction);
37 | }
38 | return transaction;
39 | }
40 |
41 | public boolean deleteById(String id) {
42 | Optional txOpt = findById(id);
43 | if (txOpt.isPresent()) {
44 | transactions.remove(txOpt.get());
45 | return true;
46 | }
47 | return false;
48 | }
49 |
50 | }
51 |
--------------------------------------------------------------------------------
/transaction-server/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | application:
3 | name: transaction-server
4 | rabbitmq:
5 | host: localhost
6 | port: 5672
7 | username: guest
8 | password: guest
9 |
10 | server:
11 | port: 8888
12 |
13 | eureka:
14 | client:
15 | service-url:
16 | defaultZone: http://localhost:8761/eureka/
17 | instance:
18 | appname: ${spring.application.name}
19 | prefer-ip-address: true
20 |
21 | springdoc:
22 | swagger-ui.path: /swagger-ui.html
23 |
24 | logging:
25 | level:
26 | org.springframework.data: ERROR
27 | com.netflix: ERROR
28 | pattern:
29 | console: '%d{yyyy-MM-dd HH:mm:ss:SSS} [S=${APP_NAME:-}] [T=%X{traceId}] %-5level %logger{36}.%L %msg%n'
--------------------------------------------------------------------------------
/transactional-core/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 | 4.0.0
7 |
8 | io.learning
9 | spring-boot-distributed-transaction
10 | 0.0.1-SNAPSHOT
11 |
12 |
13 | transactional-core
14 | transactional-core
15 |
16 |
17 |
18 | org.projectlombok
19 | lombok
20 |
21 |
22 |
23 |
--------------------------------------------------------------------------------
/transactional-core/src/main/java/io/learning/core/domain/Account.java:
--------------------------------------------------------------------------------
1 | package io.learning.core.domain;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Builder;
5 | import lombok.Data;
6 | import lombok.NoArgsConstructor;
7 |
8 | @Data
9 | @Builder
10 | @NoArgsConstructor
11 | @AllArgsConstructor
12 | public class Account {
13 |
14 | private Long id;
15 |
16 | private Long customerId;
17 |
18 | private int balance;
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/transactional-core/src/main/java/io/learning/core/domain/DistributedTransaction.java:
--------------------------------------------------------------------------------
1 | package io.learning.core.domain;
2 |
3 | import static io.learning.core.domain.DistributedTransactionStatus.NEW;
4 |
5 | import java.io.Serializable;
6 | import java.util.Collections;
7 | import java.util.List;
8 |
9 | import lombok.AllArgsConstructor;
10 | import lombok.Builder;
11 | import lombok.Data;
12 | import lombok.NoArgsConstructor;
13 |
14 | @Data
15 | @Builder
16 | @NoArgsConstructor
17 | @AllArgsConstructor
18 | public class DistributedTransaction implements Serializable {
19 |
20 | private static final long serialVersionUID = -8594438501671636539L;
21 |
22 | private String id;
23 |
24 | @Builder.Default
25 | private DistributedTransactionStatus status = NEW;
26 |
27 | @Builder.Default
28 | private transient List participants = Collections.emptyList();
29 |
30 | public DistributedTransaction(String id, DistributedTransactionStatus status) {
31 | super();
32 | this.id = id;
33 | this.status = status;
34 | }
35 |
36 | public DistributedTransaction clone() {
37 | return DistributedTransaction.builder().id(this.id).status(this.status).participants(this.participants).build();
38 | }
39 |
40 | @Override
41 | public boolean equals(Object obj) {
42 | if (this == obj)
43 | return true;
44 | if (obj == null)
45 | return false;
46 | if (getClass() != obj.getClass())
47 | return false;
48 | DistributedTransaction other = (DistributedTransaction) obj;
49 | if (id == null) {
50 | if (other.id != null)
51 | return false;
52 | } else if (!id.equals(other.id))
53 | return false;
54 | return true;
55 | }
56 |
57 | @Override
58 | public int hashCode() {
59 | final int prime = 31;
60 | int result = 1;
61 | result = prime * result + ((id == null) ? 0 : id.hashCode());
62 | return result;
63 | }
64 |
65 | }
66 |
--------------------------------------------------------------------------------
/transactional-core/src/main/java/io/learning/core/domain/DistributedTransactionParticipant.java:
--------------------------------------------------------------------------------
1 | package io.learning.core.domain;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Builder;
5 | import lombok.Data;
6 | import lombok.NoArgsConstructor;
7 |
8 | @Data
9 | @Builder
10 | @NoArgsConstructor
11 | @AllArgsConstructor
12 | public class DistributedTransactionParticipant {
13 |
14 | private String serviceId;
15 |
16 | private DistributedTransactionStatus status;
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/transactional-core/src/main/java/io/learning/core/domain/DistributedTransactionStatus.java:
--------------------------------------------------------------------------------
1 | package io.learning.core.domain;
2 |
3 | public enum DistributedTransactionStatus {
4 | NEW, CONFIRMED, ROLLBACK, TO_ROLLBACK
5 | }
6 |
--------------------------------------------------------------------------------
/transactional-core/src/main/java/io/learning/core/domain/Product.java:
--------------------------------------------------------------------------------
1 | package io.learning.core.domain;
2 |
3 | import lombok.AllArgsConstructor;
4 | import lombok.Builder;
5 | import lombok.Data;
6 | import lombok.NoArgsConstructor;
7 |
8 | @Data
9 | @Builder
10 | @NoArgsConstructor
11 | @AllArgsConstructor
12 | public class Product {
13 |
14 | private Long id;
15 |
16 | private String name;
17 |
18 | private int quantity;
19 |
20 | private int price;
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/transactional-core/src/main/java/io/learning/core/eventlistener/TransactionListener.java:
--------------------------------------------------------------------------------
1 | package io.learning.core.eventlistener;
2 |
3 | public interface TransactionListener {
4 |
5 | void handleEvent(T event);
6 |
7 | void handleAfterRollback(T event);
8 |
9 | void handleAfterCompletion(T event);
10 | }
11 |
--------------------------------------------------------------------------------