WBert WBert
Login Start free
Blog

How to Handle Distributed Transactions in Microservices

A practical e-commerce example showing how to handle distributed transactions with WBert using PostgreSQL functions, processes, queue infrastructure and realtime state updates.

Distributed transactions in microservices visualized as an e-commerce flow handled through WBert

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 distributed transaction flow across order creation, inventory reservation, payment and confirmation
A distributed transaction usually crosses more than one step and more than one system.

A simple e-commerce example

Suppose a customer places an order. The system needs to:

  1. Create the order
  2. Reserve inventory
  3. Charge the payment
  4. 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

WBert flow using a PostgreSQL function, a queued process and success or failure compensation paths
WBert keeps the local write in PostgreSQL, continues asynchronously and handles success or compensation.

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.

WBert process configuration showing processes.ProcessOrder with InternalQueue, queue name orders and default parameters JSON
An example process configuration for order handling, using the queue named 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.

WBert infrastructure settings showing RabbitMQ selected as queue provider with a queue prefix
The same application flow can run on top of a different queue provider such as 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

ProblemWBert capability
Local transactional writePostgreSQL function executed through db.ExecuteFunction
Longer backend workprocess.Execute and configured processes
Async execution and retriesWBert Internal Queue or RabbitMQ
Compensation after failureApplication process logic plus PostgreSQL state updates
Client status updatesRealtime db_changes subscriptions

Keep the mental model simple

The core idea is intuitive:

  1. commit the local order safely,
  2. continue the rest as a controlled backend flow,
  3. compensate when a later step fails,
  4. 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.