Skip to content

Commit 81ebcf0

Browse files
authored
docs: commander docs + refactoring (iluwatar#2878)
1 parent 2228212 commit 81ebcf0

File tree

9 files changed

+166
-30
lines changed

9 files changed

+166
-30
lines changed

commander/README.md

Lines changed: 144 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,162 @@
11
---
22
title: Commander
3-
category: Concurrency
3+
category: Behavioral
44
language: en
55
tag:
6-
- Cloud distributed
6+
- Cloud distributed
7+
- Microservices
8+
- Transactions
79
---
810

11+
## Also known as
12+
13+
* Distributed Transaction Commander
14+
* Transaction Coordinator
15+
916
## Intent
1017

11-
> Used to handle all problems that can be encountered when doing distributed transactions.
18+
The intent of the Commander pattern in the context of distributed transactions is to manage and coordinate complex
19+
transactions across multiple distributed components or services, ensuring consistency and integrity of the overall
20+
transaction. It encapsulates transaction commands and coordination logic, facilitating the implementation of distributed
21+
transaction protocols like two-phase commit or Saga.
22+
23+
## Explanation
24+
25+
Real-world example
26+
27+
> Imagine organizing a large international music festival where various bands from around the world are scheduled to
28+
> perform. Each band's arrival, soundcheck, and performance are like individual transactions in a distributed system. The
29+
> festival organizer acts as the "Commander," coordinating these transactions to ensure that if a band's flight is
30+
> delayed (akin to a transaction failure), there's a backup plan, such as rescheduling or swapping time slots with another
31+
> band (compensating actions), to keep the overall schedule intact. This setup mirrors the Commander pattern in
32+
> distributed transactions, where various components must be coordinated to achieve a successful outcome despite
33+
> individual failures.
34+
35+
In plain words
36+
37+
> The Commander pattern turns a request into a stand-alone object, allowing for the parameterization of commands,
38+
> queueing of actions, and the implementation of undo operations.
39+
40+
**Programmatic Example**
41+
42+
Managing transactions across different services in a distributed system, such as an e-commerce platform with separate
43+
Payment and Shipping microservices, requires careful coordination to avoid issues. When a user places an order but one
44+
service (e.g., Payment) is unavailable while the other (e.g., Shipping) is ready, we need a robust solution to handle
45+
this discrepancy.
46+
47+
A strategy to address this involves using a Commander component that orchestrates the process. Initially, the order is
48+
processed by the available service (Shipping in this case). The commander then attempts to synchronize the order with
49+
the currently unavailable service (Payment) by storing the order details in a database or queueing it for future
50+
processing. This queueing system must also account for possible failures in adding requests to the queue.
51+
52+
The commander repeatedly tries to process the queued orders to ensure both services eventually reflect the same
53+
transaction data. This process involves ensuring idempotence, meaning that even if the same order synchronization
54+
request is made multiple times, it will only be executed once, preventing duplicate transactions. The goal is to achieve
55+
eventual consistency across services, where all systems are synchronized over time despite initial failures or delays.
56+
57+
In the provided code, the Commander pattern is used to handle distributed transactions across multiple services (
58+
PaymentService, ShippingService, MessagingService, EmployeeHandle). Each service has its own database and can throw
59+
exceptions to simulate failures.
60+
61+
The Commander class is the central part of this pattern. It takes instances of all services and their databases, along
62+
with some configuration parameters. The placeOrder method in the Commander class is used to place an order, which
63+
involves interacting with all the services.
64+
65+
```java
66+
public class Commander {
67+
// ... constructor and other methods ...
68+
69+
public void placeOrder(Order order) {
70+
// ... implementation ...
71+
}
72+
}
73+
```
74+
75+
The User and Order classes represent a user and an order respectively. An order is placed by a user.
76+
77+
```java
78+
public class User {
79+
// ... constructor and other methods ...
80+
}
81+
82+
public class Order {
83+
// ... constructor and other methods ...
84+
}
85+
```
86+
87+
Each service (e.g., PaymentService, ShippingService, MessagingService, EmployeeHandle) has its own database and can
88+
throw exceptions to simulate failures. For example, the PaymentService might throw a DatabaseUnavailableException if its
89+
database is unavailable.
90+
91+
```java
92+
public class PaymentService {
93+
// ... constructor and other methods ...
94+
}
95+
```
96+
97+
The DatabaseUnavailableException, ItemUnavailableException, and ShippingNotPossibleException classes represent different
98+
types of exceptions that can occur.
99+
100+
```java
101+
public class DatabaseUnavailableException extends Exception {
102+
// ... constructor and other methods ...
103+
}
104+
105+
public class ItemUnavailableException extends Exception {
106+
// ... constructor and other methods ...
107+
}
108+
109+
public class ShippingNotPossibleException extends Exception {
110+
// ... constructor and other methods ...
111+
}
112+
```
113+
114+
In the main method of each class (AppQueueFailCases, AppShippingFailCases), different scenarios are simulated by
115+
creating instances of the Commander class with different configurations and calling the placeOrder method.
12116

13117
## Class diagram
118+
14119
![alt text](./etc/commander.urm.png "Commander class diagram")
15120

16121
## Applicability
17-
This pattern can be used when we need to make commits into 2 (or more) databases to complete transaction, which cannot be done atomically and can thereby create problems.
18122

19-
## Explanation
20-
Handling distributed transactions can be tricky, but if we choose to not handle it carefully, there could be unwanted consequences. Say, we have an e-commerce website which has a Payment microservice and a Shipping microservice. If the shipping is available currently but payment service is not up, or vice versa, how would we deal with it after having already received the order from the user?
21-
We need a mechanism in place which can handle these kinds of situations. We have to direct the order to either one of the services (in this example, shipping) and then add the order into the database of the other service (in this example, payment), since two databases cannot be updated atomically. If currently unable to do it, there should be a queue where this request can be queued, and there has to be a mechanism which allows for a failure in the queueing as well. All this needs to be done by constant retries while ensuring idempotence (even if the request is made several times, the change should only be applied once) by a commander class, to reach a state of eventual consistency.
123+
Use the Commander pattern for distributed transactions when:
124+
125+
* You need to ensure data consistency across distributed services in the event of partial system failures.
126+
* Transactions span multiple microservices or distributed components requiring coordinated commit or rollback.
127+
* You are implementing long-lived transactions requiring compensating actions for rollback.
128+
129+
## Known Uses
130+
131+
* Two-Phase Commit (2PC) Protocols: Coordinating commit or rollback across distributed databases or services.
132+
* Saga Pattern Implementations: Managing long-lived business processes that span multiple microservices, with each step
133+
having a compensating action for rollback.
134+
* Distributed Transactions in Microservices Architecture: Coordinating complex operations across microservices while
135+
maintaining data integrity and consistency.
136+
137+
## Consequences
138+
139+
Benefits:
140+
141+
* Provides a clear mechanism for managing complex distributed transactions, enhancing system reliability.
142+
* Enables the implementation of compensating transactions, which are crucial for maintaining consistency in long-lived
143+
transactions.
144+
* Facilitates the integration of heterogeneous systems within a transactional context.
145+
146+
Trade-offs:
147+
148+
* Increases complexity, especially in failure scenarios, due to the need for coordinated rollback mechanisms.
149+
* Potentially impacts performance due to the overhead of coordination and consistency checks.
150+
* Saga-based implementations can lead to increased complexity in understanding the overall business process flow.
151+
152+
## Related Patterns
153+
154+
[Saga Pattern](https://java-design-patterns.com/patterns/saga/): Often discussed in tandem with the Commander pattern
155+
for distributed transactions, focusing on long-lived transactions with compensating actions.
22156

23157
## Credits
24158

25159
* [Distributed Transactions: The Icebergs of Microservices](https://www.grahamlea.com/2016/08/distributed-transactions-microservices-icebergs/)
160+
* [Microservices Patterns: With examples in Java](https://amzn.to/4axjnYW)
161+
* [Designing Data-Intensive Applications: The Big Ideas Behind Reliable, Scalable, and Maintainable Systems](https://amzn.to/4axHwOV)
162+
* [Enterprise Integration Patterns: Designing, Building, and Deploying Messaging Solutions](https://amzn.to/4aATcRe)

commander/src/main/java/com/iluwatar/commander/Commander.java

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ void placeOrder(Order order) throws Exception {
121121
sendShippingRequest(order);
122122
}
123123

124-
private void sendShippingRequest(Order order) throws Exception {
124+
private void sendShippingRequest(Order order) {
125125
var list = shippingService.exceptionsList;
126126
Retry.Operation op = (l) -> {
127127
if (!l.isEmpty()) {
@@ -233,7 +233,7 @@ private void sendPaymentRequest(Order order) {
233233
try {
234234
r.perform(list, order);
235235
} catch (Exception e1) {
236-
e1.printStackTrace();
236+
LOG.error("An exception occurred", e1);
237237
}
238238
});
239239
t.start();
@@ -282,7 +282,7 @@ private void updateQueue(QueueTask qt) {
282282
try {
283283
r.perform(list, qt);
284284
} catch (Exception e1) {
285-
e1.printStackTrace();
285+
LOG.error("An exception occurred", e1);
286286
}
287287
});
288288
t.start();
@@ -305,7 +305,7 @@ private void tryDoingTasksInQueue() { //commander controls operations done to qu
305305
try {
306306
r.perform(list, null);
307307
} catch (Exception e1) {
308-
e1.printStackTrace();
308+
LOG.error("An exception occurred", e1);
309309
}
310310
});
311311
t2.start();
@@ -324,12 +324,12 @@ private void tryDequeue() {
324324
};
325325
Retry.HandleErrorIssue<QueueTask> handleError = (o, err) -> {
326326
};
327-
var r = new Retry<QueueTask>(op, handleError, numOfRetries, retryDuration,
327+
var r = new Retry<>(op, handleError, numOfRetries, retryDuration,
328328
e -> DatabaseUnavailableException.class.isAssignableFrom(e.getClass()));
329329
try {
330330
r.perform(list, null);
331331
} catch (Exception e1) {
332-
e1.printStackTrace();
332+
LOG.error("An exception occurred", e1);
333333
}
334334
});
335335
t3.start();
@@ -351,7 +351,7 @@ private void sendSuccessMessage(Order order) {
351351
try {
352352
r.perform(list, order);
353353
} catch (Exception e1) {
354-
e1.printStackTrace();
354+
LOG.error("An exception occurred", e1);
355355
}
356356
});
357357
t.start();
@@ -409,7 +409,7 @@ private void sendPaymentFailureMessage(Order order) {
409409
try {
410410
r.perform(list, order);
411411
} catch (Exception e1) {
412-
e1.printStackTrace();
412+
LOG.error("An exception occurred", e1);
413413
}
414414
});
415415
t.start();
@@ -465,7 +465,7 @@ private void sendPaymentPossibleErrorMsg(Order order) {
465465
try {
466466
r.perform(list, order);
467467
} catch (Exception e1) {
468-
e1.printStackTrace();
468+
LOG.error("An exception occurred", e1);
469469
}
470470
});
471471
t.start();
@@ -537,7 +537,7 @@ private void employeeHandleIssue(Order order) {
537537
try {
538538
r.perform(list, order);
539539
} catch (Exception e1) {
540-
e1.printStackTrace();
540+
LOG.error("An exception occurred", e1);
541541
}
542542
});
543543
t.start();

commander/src/main/java/com/iluwatar/commander/Retry.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public void perform(List<Exception> list, T obj) {
9494
this.errors.add(e);
9595
if (this.attempts.incrementAndGet() >= this.maxAttempts || !this.test.test(e)) {
9696
this.handleError.handleIssue(obj, e);
97-
return; //return here...dont go further
97+
return; //return here... don't go further
9898
}
9999
try {
100100
long testDelay =

commander/src/main/java/com/iluwatar/commander/employeehandle/EmployeeHandle.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ protected String updateDb(Object... parameters) throws DatabaseUnavailableExcept
4747
var o = (Order) parameters[0];
4848
if (database.get(o.id) == null) {
4949
database.add(o);
50-
return o.id; //true rcvd - change addedToEmployeeHandle to true else dont do anything
50+
return o.id; //true rcvd - change addedToEmployeeHandle to true else don't do anything
5151
}
5252
return null;
5353
}

commander/src/main/java/com/iluwatar/commander/messagingservice/MessagingDatabase.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ public class MessagingDatabase extends Database<MessageRequest> {
3838

3939
@Override
4040
public MessageRequest add(MessageRequest r) {
41-
return data.put(r.reqId, r);
41+
return data.put(r.reqId(), r);
4242
}
4343

4444
@Override

commander/src/main/java/com/iluwatar/commander/messagingservice/MessagingService.java

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,7 @@ enum MessageToSend {
4444
PAYMENT_SUCCESSFUL
4545
}
4646

47-
@RequiredArgsConstructor
48-
static class MessageRequest {
49-
final String reqId;
50-
final MessageToSend msg;
51-
}
47+
record MessageRequest(String reqId, MessageToSend msg) {}
5248

5349
public MessagingService(MessagingDatabase db, Exception... exc) {
5450
super(db, exc);

commander/src/main/java/com/iluwatar/commander/paymentservice/PaymentService.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ public PaymentService(PaymentDatabase db, Exception... exc) {
5151
*/
5252

5353
public String receiveRequest(Object... parameters) throws DatabaseUnavailableException {
54-
//it could also be sending a userid, payment details here or something, not added here
54+
//it could also be sending an userid, payment details here or something, not added here
5555
var id = generateId();
5656
var req = new PaymentRequest(id, (float) parameters[0]);
5757
return updateDb(req);

commander/src/main/java/com/iluwatar/commander/queue/QueueDatabase.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
package com.iluwatar.commander.queue;
2626

2727
import com.iluwatar.commander.Database;
28-
import com.iluwatar.commander.exceptions.DatabaseUnavailableException;
2928
import com.iluwatar.commander.exceptions.IsEmptyException;
3029
import java.util.ArrayList;
3130
import java.util.List;

commander/src/test/java/com/iluwatar/commander/RetryTest.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,13 @@
3131
import java.util.ArrayList;
3232
import java.util.List;
3333
import org.junit.jupiter.api.Test;
34+
import org.slf4j.Logger;
35+
import org.slf4j.LoggerFactory;
3436

3537
class RetryTest {
3638

39+
private static final Logger LOG = LoggerFactory.getLogger(RetryTest.class);
40+
3741
@Test
3842
void performTest() {
3943
Retry.Operation op = (l) -> {
@@ -53,16 +57,16 @@ void performTest() {
5357
try {
5458
r1.perform(arr1, order);
5559
} catch (Exception e1) {
56-
e1.printStackTrace();
60+
LOG.error("An exception occurred", e1);
5761
}
5862
var arr2 = new ArrayList<>(List.of(new DatabaseUnavailableException(), new ItemUnavailableException()));
5963
try {
6064
r2.perform(arr2, order);
6165
} catch (Exception e1) {
62-
e1.printStackTrace();
66+
LOG.error("An exception occurred", e1);
6367
}
6468
//r1 stops at ItemUnavailableException, r2 retries because it encounters DatabaseUnavailableException
65-
assertTrue(arr1.size() == 1 && arr2.size() == 0);
69+
assertTrue(arr1.size() == 1 && arr2.isEmpty());
6670
}
6771

6872
}

0 commit comments

Comments
 (0)