Overview
Silky provides distributed transaction support based on the TCC (Try-Confirm-Cancel) pattern — an application-level distributed transaction solution with no intrusion on underlying resources (database, message queue, etc.):
- Try: Reserve resources, perform business checks and resource locking
- Confirm: Commit the operation using the resources reserved in the Try phase
- Cancel: Roll back, releasing all resources reserved during Try
Compared to two-phase commit (2PC), TCC is more suitable for cross-microservice business consistency scenarios.
Usage
Annotate the Try method with [TccTransaction], specifying the Confirm and Cancel method names:
public interface IAccountAppService
{
/// <summary>Try phase: deduct balance (reserve)</summary>
[TccTransaction(ConfirmMethod = "DeductBalanceConfirm", CancelMethod = "DeductBalanceCancel")]
Task<bool> DeductBalance(DeductBalanceInput input);
/// <summary>Confirm phase: commit the deduction</summary>
Task<bool> DeductBalanceConfirm(DeductBalanceInput input);
/// <summary>Cancel phase: restore the balance (rollback)</summary>
Task<bool> DeductBalanceCancel(DeductBalanceInput input);
}
Roles & Flow
Starter (Initiator)
The Starter is the TCC transaction entry point — typically the business flow orchestrator (e.g., order service). The Starter:
- Calls all participants' Try methods
- Triggers global Confirm after all Try calls succeed
- Triggers global Cancel if any Try call fails
Participant
A Participant is a microservice called remotely by the Starter (e.g., account service, inventory service). Each participant:
- Receives the Try request and reserves resources
- Executes Confirm or Cancel based on the Starter's final decision
Full Sequence
OrderService (Starter) AccountService (Participant) InventoryService (Participant)
│ │ │
│── Try: DeductBalance ─────────────────▶ │
│── Try: ReduceStock ───────────────────────────────────────────────────▶
│ │ │
│ All Try succeed │ │
│ │ │
│── Confirm: DeductBalanceConfirm ──────▶ │
│── Confirm: ReduceStockConfirm ────────────────────────────────────────▶
│
│ [If any Try fails]
│
│── Cancel: DeductBalanceCancel ────────▶
│── Cancel: ReduceStockCancel ──────────────────────────────────────────▶
Transaction Context Propagation
The TCC transaction context is automatically propagated across microservice boundaries through RpcContext.Attachments:
- Starter side: Before calling a participant's Try method, the transaction ID and phase are set in
RpcContext.Context.SetAttachments() - Transport layer: Attachments are serialized into
RemoteInvokeMessage.Attachments - Participant side: The server-side
DefaultServerMessageReceivedHandlerrestores attachments into the server-sideRpcContext
This ensures participants know they are inside a distributed transaction without explicit parameter threading.
StarterTccTransactionHandler
Handles the initiator role:
- PreTry: Creates a global transaction record in Redis with status
Trying - Try execution: Calls all participant Try methods (RPC calls)
- On all success → Confirm: Updates transaction status to
Confirming, calls all participant Confirm methods - On any failure → Cancel: Updates transaction status to
Canceling, calls all participant Cancel methods
ParticipantTccTransactionHandler
Handles the participant role:
- Receives Try / Confirm / Cancel calls
- Creates a participant transaction record for each phase
- Executes the corresponding business method (Try / Confirm / Cancel)
- Updates participant record status after execution
Redis Persistence
Transaction records are persisted to Redis by TccTransactionRepository:
Key format: silky:transaction:{transactionId}
Value: Serialized TccTransaction object
TTL: StoreDays configuration (default 3 days)
Each participant's Try/Confirm/Cancel call also creates a TccParticipant record nested within the transaction.
TccTransactionRecoveryService — Automatic Compensation
A background service that periodically compensates incomplete transactions:
| Config | Default | Description |
|---|---|---|
ScheduledInitDelay | 20s | Delay before first recovery run after startup |
ScheduledRecoveryDelay | 30s | Interval between recovery runs |
RecoverDelayTime | 259200s (3 days) | Transactions older than this are eligible for recovery |
RetryMax | 10 | Max recovery retry attempts per transaction |
Recovery logic:
- Query Redis for all transactions in
Trying/Confirming/Cancelingstate - For transactions exceeding
RecoverDelayTime: re-invoke the appropriate Confirm or Cancel methods - After
RetryMaxretries: mark transaction asFailedand alert (log error)
Important Implementation Notes
- Try method must be idempotent: the recovery service may call it multiple times
- Confirm and Cancel methods must be idempotent: network failures can cause repeated calls
- Cancel must always succeed: if Cancel fails, the recovery service will retry; never throw unrecoverable exceptions from Cancel
- Avoid long-running Try phases: resources are locked during the Try phase; long holds increase contention
