Distributed transactions appear when one business action depends on several steps that do not all happen inside the same local database transaction. A common example is e-commerce: creating an order, reserving inventory and charging the customer.
A single PostgreSQL transaction can safely create the local order record, but it cannot atomically commit an external inventory reservation and a payment provider charge at the same time. That is where a step-by-step backend flow becomes important.
A simple e-commerce example
Suppose a customer places an order. The system needs to:
- Create the order
- Reserve inventory
- Charge the payment
- Confirm the order
If payment fails after inventory has already been reserved, the system must compensate. Otherwise the order, stock and payment state drift apart.
Why a normal database transaction is not enough
The order rows can be created in one local PostgreSQL transaction, but inventory and payment often live outside that single commit boundary. In practice, the safe approach is usually:
- commit the local data you control,
- continue the remaining work step by step,
- compensate when a later step fails,
- keep the user informed of the current state.
Solving it with WBert
Step 1 — Create the order locally
The client calls db.ExecuteFunction to run a PostgreSQL function. This is where the order can be created as pending together with its local rows inside one DB transaction.
View request payload
{
"endpoint": "db.ExecuteFunction",
"parameters": {
"connector": "main",
"functionName": "main.fn_create_order",
"values": {
"customer_id": "CUST-1001",
"amount": 149.90
}
}
}
View PostgreSQL example
create or replace function main.fn_create_order(
p_customer_id text,
p_amount numeric
)
returns jsonb
language plpgsql
as $$
declare
v_order_id uuid;
begin
insert into main.orders (id, customer_id, status, amount)
values (gen_random_uuid(), p_customer_id, 'pending', p_amount)
returning id into v_order_id;
return jsonb_build_object(
'orderId', v_order_id,
'status', 'pending'
);
end;
$$;
Step 2 — Continue asynchronously
After the order exists locally, the remaining work should not hold the HTTP request open. WBert can continue the flow through process.Execute, running a configured process directly or through the queue.
orders.View process request
{
"endpoint": "process.Execute",
"parameters": {
"processName": "processes.ProcessOrder",
"parameters": {
"orderId": "ORD-1042",
"customerId": "CUST-1001",
"amount": 149.90
}
}
}
Step 3 — Reserve inventory
The background process tries to reserve stock for the order. If the stock cannot be reserved, the order can be cancelled early without charging the customer.
Step 4 — Charge payment
If inventory succeeds, the process continues to payment. When payment succeeds, the order can be confirmed. When payment fails, the process compensates: release the reservation and cancel the order.
View process script example
using System;
using AppServer.Processes.Api.Models;
using Newtonsoft.Json.Linq;
public static class ProcessScript
{
public static object Run(JObject parameters, ProcessExecutionContext context)
{
string orderId = parameters.Value<string>("orderId") ?? "";
decimal amount = parameters.Value<decimal?>("amount") ?? 0;
return new
{
ok = true,
orderId,
amount,
status = "processing",
nextStep = "reserve_inventory"
};
}
}
Step 5 — Use queue infrastructure without changing the client contract
The client still talks to WBert in the same way while the underlying queue provider can be changed by deployment. That means the application flow can stay stable whether the queue runs through WBert Internal Queue or RabbitMQ.
Step 6 — Make retries safe
Longer flows should be idempotent. If a queued step is retried, it should not double-charge the customer or reserve the same stock twice. In practice, that means checking the current state before re-running a side effect and storing enough status to know whether a step already succeeded.
Step 7 — Notify the client
While the process continues in the background, the client can subscribe to order changes and update the UI from pending to processing, confirmed or cancelled.
View realtime subscription
client.on("db_changes", {
event: "*",
connector: "main",
schema: "main",
table: "orders"
}, handler);
What WBert is doing in this flow
| Problem | WBert capability |
|---|---|
| Local transactional write | PostgreSQL function executed through db.ExecuteFunction |
| Longer backend work | process.Execute and configured processes |
| Async execution and retries | WBert Internal Queue or RabbitMQ |
| Compensation after failure | Application process logic plus PostgreSQL state updates |
| Client status updates | Realtime db_changes subscriptions |
Keep the mental model simple
The core idea is intuitive:
- commit the local order safely,
- continue the rest as a controlled backend flow,
- compensate when a later step fails,
- show the current state to the client.
That does not magically remove distributed transaction complexity, but it gives the application a practical way to manage it.
For a deeper look at orchestration and compensation patterns, see Saga Pattern in Microservices: A Practical .NET Example.