Bài trước, chúng ta đã bàn về lý thuyết cơ bản của Saga Transaction thông qua ví dụ về một hệ thống Microservices với 4 service con.

Trong bài này, chúng ta bàn về cách triển khai Saga cùng với demo với Java core.
Cách triển khai Saga Orchestration
Saga có hai cách triển khai chính: Saga Choreography và Saga Orchestration. Ở đây, tôi sẽ chỉ đi qua nhanh về Choreography do nó thường xuất hiện tại những hệ thống Microservices đơn giản, ít phức tạp.
Sơ lược về Saga Choreography
Trong Saga Choreography, các saga thành phần sẽ lắng nghe (subscribe) các sự kiện (events) từ những thành phần khác và hành xử. Với ví dụ của ta,
- Order Service tạo mới đơn hàng và tạo ra sự kiện OrderCreated.
- Inventory Service lắng nghe sự kiện này và thực hiện đặt chỗ tồn kho. Nếu thành công, nó tạo ra sự kiện InventoryReservationPending.
- Payment Service lắng nghe sự kiện này và thực hiện thanh toán. Nếu thành công, nó tạo ra sự kiện PaymentProcessed. Chú ý rằng đây là một giao dịch chốt (pivot transaction): sự thành công hay thất bại của nó quyết định kết quả của saga.
- Inventory Service tiếp tục lắng nghe được và thực hiện xác nhận trừ tồn kho. Trường hợp này, giao dịch "phải" thành công. Nếu thất bại thì cần cơ chế retry hoặc hủy toàn bộ Saga. Kết quả là tạo ra sự kiện InventoryReservationApproved.
- Order Service lắng nghe được và thực hiện xác nhận Order.
Nhận xét:
- Mỗi saga thành phần tiếp nhận và tạo ra sự kiện để một saga thành phần nào khác trực tiếp tiếp nhận và hành động.
- Điều quan trọng hơn là: Dữ liệu saga sẽ nằm rải rác ở nhiều service thành phần. Do đó, thông thường ta sẽ cần một cơ chế móc nối thông tin như một ID duy nhất cho Saga mà các service tiếp nhận đều có. Đây cũng là một trong những điểm hạn chế nhất của mô hình này.
Về Saga Choreography tạm thời chúng ta chỉ bàn tới đây. Tiếp theo ta đi tới mô hình Orchestration tập trung.
Cơ bản về Saga Orchestration
Saga Orchestration là một cách triển khai Saga tập trung: thay vì các saga thành phần tự giao tiếp với nhau (trực tiếp hay qua messaging) thì chúng được điều phối bởi một thành phần trung tâm: Saga Orchestrator.
Với ví dụ của ta trong Saga Choreography, ta có luồng xử lý sau trong Orchestration:
- Order Service tạo mới đơn hàng và tạo ra sự kiện OrderCreated.
- Saga Orchestrator lắng nghe sự kiện này và gọi tới Inventory Service để đặt chỗ tồn kho. Inventory Service xử lý thành công và tạo ra sự kiện InventoryReservationPending.
- Saga Orchestrator lắng nghe sự kiện này và gọi tới Payment Service để thanh toán. Payment Service xử lý thành công và tạo ra sự kiện PaymentProcessed. Đây là giao dịch chốt.
- Saga Orchestrator lắng nghe sự kiện này và gọi tới Inventory Service để xác nhận tồn kho. Inventory Service xử lý thành công và tạo ra sự kiện InventoryReservationApproved.
- Saga Orchestrator lắng nghe sự kiện và gọi tới Order Service để xác nhận đơn hàng.
Như vậy, ta thấy có một số điểm khác biệt chính sau so với trước:
- Các saga thành phần hoàn toàn được điều phối bởi một thành phần trung tâm Saga Orchestrator.
- Quan trọng hơn, dữ liệu Saga Transaction được tập trung tại nơi đặt Saga Orchestrator. Ta sẽ bàn cụ thể hơn về triển khai vật lý cho phần này sau.
Khi xem về Saga, ta thường nói về một "Máy trạng thái - State Machine".
State Machine
State Machine là một thành phần mà hành vi của nó được xác định bởi trạng thái hiện tại và sự kiện đầu vào. Khi có sự kiện xảy ra, State Machine có thể chuyển đổi sang một trạng thái khác dựa trên logic định nghĩa cho trạng thái hiện tại.
Saga Orchestrator phù hợp với State Machine: Dựa trên trạng thái hiện tại của saga (đang ở giai đoạn thực thi saga thành phần nào) và sự kiện đầu vào (kết quả thực hiện của thành phần đó), saga sẽ thực hiện hành động tiếp theo (thực thi thành phần tiếp theo) và chuyển về trạng thái chờ kết quả này.

Như vậy, ta cần xác định được những trạng thái của State Machine.
Trong ví dụ, ta có các state sau:
OrderCreatedState
. Khi Order Service tạo một Order ở trạng tháiAPPROVAL_PENDING
, một sự kiệnOrderCreated
được tạo ra. Saga Orchestrator lắng nghe và chuyển về trạng tháiOrderCreatedState
. Ở trạng thái này, Saga Orchestrator thực hiện yêu cầu đặt chỗ tồn kho trong Inventory Service và chuyển về trạng thái tiếp theo:InventoryReservationRequestedState
. Như vậy, trạng tháiOrderCreatedState
chỉ là một trạng thái chuyển giao nhanh. Mục đích là thể hiện chi tiết hơn. Các bạn có thể bỏ qua trạng thái này.InventoryReservationRequestedState
. Trạng thái này thể hiện Orchestrator đang chờ kết quả từ Inventory Service. Khi tiếp nhận được input (là kết quả xử lý), nó sẽ xem xét việc chuyển lời gọi sang Payment Service tiếp theo hay tới Order Service tùy theo kết quả thành công hay thất bại trong input này. Nếu thành công, nó thực hiện yêu cầu thanh toán (bằng cách bắn 1 event tới Payment Service) và chuyển sang trạng tháiPaymentProcessRequestedState
để chờ kết quả.PaymentProcessRequestedState
. Tương tự bước trước, nó tiếp nhận input là kết quả xử lý của Payment Service. Tùy theo kết quả thành công hay thất bại mà nó sẽ thực thi hành động xác nhận đặt kho hay bù trừ giao dịch đặt kho. Giả sử thành công, nó sẽ chuyển sangInventoryReservationConfirmationRequestedState
.InventoryReservationConfirmationRequestedState
. Trạng thái này thể hiện Saga đã thành công và đang thực hiện ở "bên kia sườn dốc". Tùy theo yêu cầu, ta có thể thiết lập xử lý xác nhận này có cơ chế retry khi thất bại. Nếu thành công, Saga chuyển về trạng thái yêu cầu xác nhận đơn hàng thành côngOrderApprovalRequestedState
.OrderApprovalRequestedState
. Trạng thái này, tương tự nhưOrderCreatedState
, là một thể hiện chi tiết quá mức. Ta có thể bỏ qua trạng thái này mà từ (4) thành công có thể cập nhật ngay trạng thái Order thànhAPPROVED
. Nhưng ta làm như thế này để tính trước tình huống Saga Orchestrator và Order Service được triển khai trên những process khác nhau.OrderRejectionRequestedState
. Khi saga thành phần thất bại, ta cần yêu cầu hủy đơn và chuyển về trạng thái này.InventoryReservationRejectionRequestedState
. Cụ thể, khi xử lý tại Payment Service thất bại, ta cần yêu cầu hủy giữ chỗ tồn kho và chuyển về trạng thái này.
Các trạng thái được mô tả rõ hơn trong sơ đồ chuyển trạng thái của State Machine như sâu:

Lầm tưởng lớn về Saga Orchestration
Saga Orchestrator không nhất thiết phải là service trung tâm, chuyên trách
Một lầm tưởng thường gặp về Saga Orchestrator là nó là một service trung tâm, các service khác muốn thực hiện các giao dịch Saga thì gọi tới "Saga Orchestration Service" này. Tuy nhiên, điều này không hẳn đúng, và nó chỉ nên là một lựa chọn triển khai mức vật lý mà thôi. Mở rộng hơn, tôi thấy một hướng suy nghĩ thường thấy của người phát triển phần mềm, nhất là trong thời kỳ của Microservices hiên nay là ta suy nghĩ ngay tới service, service và service. Ứng dụng cần thể hiện các chức năng của nó, và các chức năng năng đó nếu bị đưa vào service ngay từ thời điểm đầu là ta đã mất đi khả năng uyển chuyển khi triển khai ứng dụng đó. Service chỉ là nơi thể hiện vật lý của chức năng ứng dụng.
Do đó, Orchestrator có thể được đưa vào từng service trong hệ thống của ta. Tôi thấy cách này "make sense" hơn vì như vậy, tôi sẽ triển khai được các giao dịch saga vào đúng nơi mà service thực sự "owns" nó. Khi triển khai Order Saga, tôi sẽ đưa nó vào Order Service và sẽ rất thuận tiện để thấy được các giao dịch Saga này ở trạng thái nào, thành công hay thất bại, và thất bại ở bước nào. Khi nói service "owns" saga, nghĩa là saga đó sinh ra là thuộc về service đó.
Ngược lại, khi tập trung tất cả saga vào một Saga Orchestration Service trung tâm, ta vô tình giảm đi tính phân tán vốn có của Microservices Architecture. Ngoài ra, team phụ trách Saga sẽ cần biết nghiệp vụ tạo Order của Order Service. Thường thì ta sẽ phải giao nhiệm vụ này cho team Order Service.
Vì vậy, ta cần tăng tính module hóa trong Saga Orchestration
Theo hướng phân tích trên, ta sẽ thấy triển khai theo hướng phân tán cho Saga Orchestration sẽ tốt hơn trên nhiều phương diện. Dù gì thì khi cập nhật một Saga mới, ta cũng phải implement logic xử lý cho nó.
Ta có thể "trì hoãn" quyết định triển khai tập trung hay phân tán cho Saga Orchestration Service bằng cách triển khai module cho nó. "Trì hoãn" quyết định trong thiết kế phần mềm là một trong những điều thường phải làm của một kiến trúc sư phần mềm: Một kiến trúc sư giỏi là người có thể trì hoãn những quyết định liên quan tới triển khai chi tiết càng lâu càng tốt. Đây không phải là điều dễ dàng và cũng ngoài phạm vi của bài viết này. Cơ bản là khi ta đã tạo được module logic nào đó thì việc triển khai logic đó dưới dạng web service, CLI, hay batch job sẽ đơn giản hơn rất nhiều.
Chính vì thế, chúng ta sẽ tạo ra một module SagaOrchestrator
(được ký hiệu là component) đóng vai trò là abstraction, tập hợp những xử lý chung nhất cho Saga Orchestrator cụ thể.
Saga Orchestrator cụ thể, trường hợp này là một module mang tên OrderSagaOrchestrator
sẽ kế thừa SagaOrchestrator
, ở đó, nó cần cài đặt các trạng thái cụ thể và hành vi cụ thể của mỗi trạng thái đó cùng với việc biến đổi giữa các trạng thái đó với nhau.

Trong hình 4, SagaOrchestrator là module chung về Saga Orchestration.
SagaOrchestrator module
Đầu tiên, ta cần một domain entity. Với nghiệp vụ Saga Orchestration, ta sẽ tạo một entity chính: SagaOrchestrator class.
Vai trò của SagaOrchestrator:
- Đóng vai trò wrapper, bọc lấy Saga bên trong.
- Tiếp nhận một input, ta gọi là SagaInput,
- Khởi tạo Saga (cho trường hợp tạo mới, như khi mới tạo một Order) hoặc tái tạo lại Saga từ database,
- Chuyển tiếp SagaInput vào Saga, Saga đóng vai trò chính là một State Machine.
- Lấy kết quả xử lý của State Machine, lưu lại trạng thái đó vào database và thực hiện vai trò messaging: thông báo sự kiện tới service saga thành phần tiếp theo (ở đây, ta thực hiện Outbox Transaction nên chỉ đơn giản là save(events)).

Trong hình 5, SagaOrchestrator
là một abstract class (thể hiện bằng chữ in nghiêng), trong đó có hai phương thức chính:
handle(SagaInput): SagaOutput
, đóng vai trò chính để quản lý thực thi Saga: tiếp nhận đầu vào, tạo mới hay tái tạo dữ liệu Saga, rồi gọi hàm thực thi của Saga. Như đoạn code mô tả dưới đây:
public void handle(SagaInput sagaInput) {
T sagaData = this.sagaRepository.readBy(sagaInput.aggregateId());
if (sagaData == null) {
sagaData = createSagaData(sagaInput);
}
SagaOutput sagaOutput = sagaData.handleInput(sagaInput);
DomainEvent domainEvent = sagaOutput.domainEvents();this.sagaRepository.save(sagaData);
this.eventRepository.save(domainEvent);
}
createSagaData(SagaInput): Saga
: là abstract method mà mỗi SagaOrchestrator cụ thể, trường hợp này là OrderSagaOrchestrator, phải thực hiện nhằm khởi tạo dữ liệu Saga lần đầu tiên.
Saga, là một State Machine, bao gồm nhiều trạng thái khác nhau được thể hiện cụ thể như sau:
- Saga cần có một global id, đặt tên là SagaId. Ta cần sử dụng cơ chế sinh ID đảm bảo tính toàn cục trên toàn hệ thống của nó. Ở đây, tôi sử dụng UUID. Việc tạo ra một class SagaId để chứa giá trị UUID bên trong là thừa nhưng tôi muốn nhấn mạnh thông tin quan trọng qua kiểu của nó. Đây là cách thường gặp khi lập trình theo phương pháp DDD. Về DDD, chúng ta sẽ bàn tới ở nhiều bài viết sau.
- Saga cần sở hữu State, ta cần SagaState. Đây chỉ thuần túy là một abstract class vì state cụ thể còn tùy thuộc vào bài toán triển khai.
- Saga cần được liên kết với dữ liệu mà nó thuộc về, trong trường hợp này là Order. Nhưng, lại một lần nữa sử dụng DDD, ta sẽ khai báo một AggregateId bên trong Saga. AggregateId chính là OrderId. DDD không cho phép tham chiếu trực tiếp giữa các aggregates.
- Bên cạnh trạng thái, ta cũng lưu một thông tin cho thấy Saga đã hoàn thành chưa, ở cột isCompleted.
Hành vi của Saga class:
Saga là một domain entity, ta cần khai báo các hành vi nghiệp vụ cho nó:
- handleInput(SagaInput): SagaOutput. Đây là hành vi chính của Saga, nó thực hiện hai việc nhỏ: 1 - ủy quyền xử lý sang SagaState, là trạng thái mà nó đang có và 2 - chuyển sang trạng thái tiếp theo. Trạng thái tiếp theo này là output của việc xử lý tại trạng thái hiện tại.
- transit(SagaState): void. Thực hiện chuyển sang trạng thái tiếp theo.
public abstract class Saga {
protected SagaId id;
protected AggregateId aggregateId;
protected SagaState state;
protected boolean isCompleted;
protected Date createAt;
protected Date updateAt;
public Saga(AggregateId aggregateId) {
this.id = new SagaId(UUIDUtil.genUUID());
this.aggregateId = aggregateId;
this.createAt = new Date();
this.isCompleted = false;
}
public SagaOutput handleInput(SagaInput sagaInput) {
SagaOutput sagaOutput = this.state.process(sagaInput);
this.transit(sagaOutput.nextState());
return sagaOutput;
}
protected void transit(SagaState sagaState) {
this.state = sagaState;
}
SagaState, đơn giản chỉ là một abstract class thể hiện trạng thái rất tổng quát của một Saga. Trong đó, bao gồm hành vi process(SataInput): SagaOutput để Saga gọi và nó bao gồm 2 thông tin: đối tượng Saga đang sở hữu nó và StateName - tên của trạng thái.
public abstract class SagaState {
protected T saga;
protected StateName stateName;
public SagaState(T saga) {
this.saga = saga;
initName();
}
public abstract SagaOutput process(SagaInput sagaInput);
protected abstract void initName();
Như vậy ta có như sau:

Như vậy, ta có cơ bản một module Saga Orchestrator rồi. Ta đóng gói nó lại trong một thành phần mang tên SagaOrchestrator
. Tiếp theo, ta cần cài đặt một Saga Orchestrator cụ thể: OrderSagaOrchestrator
.
OrderSagaOrchestrator module
Theo hình 4, ta thấy OrderSagaOrchestrator kế thừa từ SagaOrchestrator. Thật vậy, xem lại các class trong SagaOrchestrator, ta thấy nó vẫn khá tổng quát và chứa lại một số class, method cần cụ thể hóa. Đó chính là nơi mà nghiệp vụ cụ thể cần lấp đầy.
OrderSagaOrchestrator
Do abstract class SagaOrchestrator đã thực hiện gần như đầy đủ vai trò của một Orchestrator: tiếp nhận input, tạo mới saga hay khôi phục , gọi xử lý sau đó lưu lại trạng thái vào database cũng như notify các sự kiện. Nên ta cần OrderSagaOrchestrator làm đúng 1 việc đơn giản: Khởi tạo dữ liệu Saga khi bắt đầu một Saga Transaction.
public class OrderSagaOrchestrator extends SagaOrchestrator {
public OrderSagaOrchestrator(SagaRepository sagaRepository, EventRepository eventRepository) {
super(sagaRepository, eventRepository);
}
@Override
protected OrderSaga createSagaData(SagaInput sagaInput) {
SagaInput orderCreatedSagaInput = (SagaInput) sagaInput;
return new OrderSaga(new AggregateId(orderCreatedSagaInput.aggregateId().value()));
}
...
Theo đoạn code trên ta thấy đúng điều đó. OrderCreatedSagaInputContent
là một trong các input param, chính là DTO của SagaOrchestrator
.
OrderSaga - State Machine
OrderSaga cũng không cần làm gì ngoài việc khởi tạo một trạng thái đầu tiên cho nó, cũng là một nghiệp vụ đặc thù:
public class OrderSaga extends Saga {
public OrderSaga(AggregateId aggregateId) {
super(aggregateId);
this.state = new OrderCreatedState(this);
}
}
Ở đây, trạng thái đầu tiên của OrderSaga
là OrderCreatedState
(xem lại các trạng thái trong hình 3).
Các States của OrderSaga
Đây là nơi ta khai báo các class thể hiện trạng thái cụ thể của OrderSaga.
Ví dụ với OrderCreatedState
, theo yêu cầu nghiệp vụ, khi ở trạng thái này, OrderSaga cần thực hiện yêu cầu đặt chỗ tồn kho tại Inventory Service và chuyển sang trạng thái chờ kết quả thực hiện InventoryReservationRequestedState
. Điều này được thực hiện dễ dàng với Outbox Transaction: sinh event và save lại.
public class OrderCreatedState extends SagaState {
public OrderCreatedState(OrderSaga saga) {
super(saga);
}
@Override
public SagaOutput process(SagaInput sagaInput) {
DomainEvent inventoryReservationRequestedEvent = this.generateEvent(sagaInput);
return new SagaOutput(
sagaInput,
this.saga,
inventoryReservationRequestedEvent,
new InventoryReservationRequestedState(this.saga)
);
}
@Override
protected void initName() {
this.stateName = new StateName("OrderCreatedState");
}
private DomainEvent generateEvent(SagaInput sagaInput) {
InventoryReservationRequestedEventContent eventContent = new InventoryReservationRequestedEventContent();
List productReservations = sagaInput.content().order().createProductReservationRequest();
eventContent.setSagaId(this.saga.id());
return new DomainEvent<>(
new AggregateId(this.saga.id().value()),
new EventType("InventoryReservationRequest_Saga"),
new IssuerId("Order Saga"),
new Date(),
eventContent
);
}
}
Tương tự như vậy cho các trạng thái tiếp theo, để tránh code tràn lan gây khó đọc, các bạn xem tiếp trong source code.
Như vậy ta đã xây dựng bức tranh như sau:

Saga Orchestration deployments
Khi đã có các module quản lý về Saga Orchestrator như SagaOrchestrator và OrderSagaOrchestrator, ta có thể triển khai nó tập trung tại một service hay phân tán ra các service owner đều dễ dàng.
Tôi thường sử dụng kiến trúc được khuyến nghị bởi Robert C. Martin: kiến trúc cần thể hiện use cases, còn services là cách triển khai, phân rã hay gom các use cases đó. Do đó, tôi đã tạo ra các module thuần nghiệp vụ là:
- Order Service module: bao gồm các use cases về nghiệp vụ đơn hàng.
- Saga Orchestrator module: bao gồm các domain entities về Saga Orchestration và các State Machine như đã bàn bên trên.
- Order Saga Orchestrator module: bao gồm các hiện thực hóa cho Saga Orchestrator cho nghiệp vụ Order.
- Ngoài ra, tôi còn có các module dùng chung như commons, utilities,...
Module triển khai của tôi sẽ là:
- Order Service module: là một microservice trong kiến trúc chung (lựa chọn 1).
- Saga Orchestration Service module (cho lựa chọn 2): là một microservice chuyên trách thực hiện Saga Orchestration.
Lựa chọn 1: Triển khai Order Saga Orchestrator cùng với Order Service
Ngược lại với cách triển khai 1, cách 2 chỉ là đưa module Order Saga Orchestrator vào trong Order Service. Order Service đóng vai trò là adapter còn các module như Order Business, Order Saga Orchestrator đóng vai trò nghiệp vụ.

Lựa chọn 2: Triển khai một Saga Orchestration Service

Theo mô hình triển khai này, Order Service sẽ chỉ dùng module nghiệp vụ Order Business của nó.
Còn Saga Orchestration Service thì sử dụng Order Saga Orchestrator, ngoài ra Order Saga Orchestrator cần sử dụng Order Business vì nó cần một số entity của nghiệp vụ Order.
Lựa chọn 1 có vẻ là hợp lý hơn lựa chọn 2.
Source code:
Git: https://github.com/pmsbkhn/saga-orchestration-demo
2 modules chính:
- saga-orchestrator: là module khai báo về saga orchestrator
- order-business: là module nghiệp vụ order.
Project này triển khai cách tiếp cận 1: Đưa saga-orchestrator vào trong order-service.
Nhưng project không có order-service nên test sẽ thông qua Unit Tests tại order-business.

Class thực hiện unit tests:
