Multi-Datasource and Transaction Interaction
Goal
This page explains how datasource switching with engine.With(...) interacts with transactions such as Transaction and Tx in Juice, so that cross-database transaction misuse can be avoided.
Key Conclusions
engine.With("name")returns a newEngineview bound to the target datasource.A
juice.Transactiononly covers the connection for the datasource associated with the current engine.NestedTransactiononly decides whether a transaction already exists. It does not provide cross-datasource consistency.Juice does not provide a distributed transaction abstraction such as 2PC or XA. Cross-database consistency should be handled with business-level patterns.
Interaction Matrix
Scenario |
Covered by one transaction? |
Risk |
Recommendation |
|---|---|---|---|
|
Yes |
Low |
Use directly |
Call |
Yes, but only for that slave datasource |
Read/write responsibilities can become unclear |
Use only after defining a clear read/write policy |
Switch to another datasource inside a transaction callback |
No, this usually becomes two separate transaction domains |
Inconsistency and partial commits |
Avoid it; use saga or outbox patterns instead |
Switch between multiple datasources and write in one request |
No |
No atomic commit is possible |
Split the steps and design compensation |
Recommended Patterns
Pattern 1: Choose the datasource first, then open the transaction
base := juice.ContextWithManager(context.Background(), engine)
writeEngine, err := engine.With("master")
if err != nil {
return err
}
ctx := juice.ContextWithManager(base, writeEngine)
return juice.Transaction(ctx, func(ctx context.Context) error {
_, err := repo.CreateOrder(ctx, req)
return err
})
Pattern 2: Under read-write splitting, keep write paths on master
Queries can go to replicas according to policy, typically without transactions or with read-only transactions.
Write paths such as order creation, inventory deduction, and bookkeeping should consistently use a transaction on the primary datasource.
Pattern 3: Use business patterns for cross-datasource consistency
Possible strategies:
outbox with asynchronous delivery
saga with compensating transactions
idempotency keys with retries
Anti-Pattern (Not Recommended)
// The outer layer opens a transaction on master.
_ = juice.Transaction(ctx, func(ctx context.Context) error {
if _, err := repoA.WriteOnMaster(ctx, a); err != nil {
return err
}
// Switch to another datasource inside the same callback and write again.
other, _ := engine.With("other")
otherCtx := juice.ContextWithManager(ctx, other)
_, err := repoB.WriteOnOther(otherCtx, b)
return err
})
The code above usually cannot guarantee atomic consistency across both writes.
Troubleshooting Tips
If some steps succeed and others fail, first check whether one business action writes to multiple datasources.
If a transaction seems ineffective, check whether another
engine.With(...)context was mixed into the call chain.Logging the EnvID in middleware, if available in your setup, helps locate datasource drift quickly.