Acumatica · Architecture

Acumatica Microservices Bridge Pattern

Acumatica Microservices Bridge Pattern is the Acumatica operations topic that you only get good at by doing it badly a few times.

John Kihiu12 min read

"Bridge" is the word I've settled on for the integration shape I keep building between Acumatica and standalone microservices, because it's more accurate than "integration" and less misleading than "API." A bridge implies two independent things that stay independent, connected by a defined crossing point - which is exactly what you want between a stateful, session-bound ERP graph and a stateless service that scales independently. Trying to make Acumatica a "microservice" itself, or trying to make a microservice behave like it's part of Acumatica's process, both fail for structural reasons.

Why Acumatica graphs don't decompose into microservices cleanly

PXGraph instances are stateful across a user's editing session - the graph, its cache, and the row state it's tracking persist between requests for as long as the user has the screen open. Microservices architecture generally assumes statelessness at the service boundary, so that any instance can handle any request and horizontal scaling is trivial. You cannot take a PXGraph, wrap it in a thin HTTP layer, and call it a microservice, because the moment two requests for the same editing session land on different instances of that "service," the session state is gone. Acumatica's own scale-out story (the optional app server tier) works around this with session affinity, not statelessness - a deliberately different scaling model than microservices assume, and one that's correct for what a graph actually is: a controller for an interactive editing session, not a stateless request handler.

The bridge shape that actually works: async handoff, not synchronous coupling

Every successful Acumatica-to-microservice integration I've built follows the same shape: Acumatica emits an event (a Business Event, a webhook, a REST API call triggered from a graph action) at a well-defined point in a workflow, a message lands in a queue or bus, and an independently-deployed service consumes it asynchronously, doing its own work on its own schedule, then reporting results back through the REST API or another inbound webhook. The two systems never share a call stack and never share a transaction.

C# · publishing side, in-process
public class SOOrderEntry_FraudCheckBridge : PXGraphExtension<SOOrderEntry>
{
    public static bool IsActive() => true;

    // RowPersisted, not RowPersisting: only publish once the order
    // has actually committed. The microservice on the other side of
    // this bridge should never see an order that got rolled back.
    protected virtual void _(Events.RowPersisted<SOOrder> e)
    {
        if (e.Row == null || e.TranStatus != PXTranStatus.Completed) return;
        if (e.Operation.Command() != PXDBOperation.Insert) return;

        FraudCheckQueue.Publish(new FraudCheckRequested
        {
            OrderNbr = e.Row.OrderNbr,
            CustomerID = e.Row.CustomerID,
            OrderTotal = e.Row.OrderTotal,
            CorrelationId = Guid.NewGuid()
        });
    }
}

The fraud-check service on the other end does its work - calling third-party APIs, running ML scoring, whatever it needs, at whatever latency it needs - entirely off Acumatica's critical path. When it's done, it calls back through the REST API to set a status field and, if needed, put the order on hold. Acumatica never blocks waiting for the microservice, and the microservice never needs to understand PXGraph, PXCache, or anything about Acumatica's internals beyond a documented REST contract.

The part everyone underestimates: correlation and idempotency

The bridge pattern's hardest problem isn't the queue or the webhook, it's making sure the async response lands back on the correct record, exactly once, even when the microservice retries, the queue redelivers, or the callback arrives after the user has already reopened and re-saved the order. I put a correlation ID on every outbound message and require the same ID on the inbound callback, and I make the callback handler idempotent against replays - checking current state before applying a change, not blindly overwriting.

C# · inbound callback, idempotent
[PXOverride]
public IEnumerable ApplyFraudCheckResult(PXAdapter adapter, FraudCheckResult result)
{
    var order = SelectOrderByNbr(result.OrderNbr);
    if (order == null) return adapter.Get();

    // Idempotency guard: a redelivered callback with a correlation ID
    // we've already applied is a silent no-op, not a duplicate update.
    if (order.UsrFraudCheckCorrelationId == result.CorrelationId
        && order.UsrFraudCheckStatus != null)
        return adapter.Get();

    order.UsrFraudCheckStatus = result.Status;
    order.UsrFraudCheckCorrelationId = result.CorrelationId;
    Base.Document.Update(order);
    Base.Save.Press();
    return adapter.Get();
}
Never let a bridge become a synchronous dependency by accident

The failure mode I've seen most often: a "bridge" that starts async gradually accumulates a synchronous call somewhere, because a developer needed a value back immediately for a UI decision and reached for a blocking HTTP call inside a graph event handler instead of redesigning the flow. Once that happens, your microservice's availability and latency directly gate Acumatica's screen responsiveness, and you've quietly turned an independent service into a tightly coupled dependency with none of a real in-process call's transactional safety. If a value is genuinely needed synchronously before a save can complete, that's a strong signal the logic belongs in-process, not behind a bridge.

Wrapping up

Acumatica graphs are stateful and session-bound; microservices are meant to be stateless and independently scaled. Don't try to make one look like the other. Bridge them with an async event-and-callback shape - Business Events or a graph-triggered publish out, a REST API callback in - carry a correlation ID both directions, and make the callback handler idempotent against retries and redelivery. The moment a "bridge" needs a synchronous response to make a UI decision, that's a sign the logic belongs back in-process as a graph extension, not across a network boundary.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.