Acumatica · Architecture

Acumatica Monolith vs Modular

Acumatica Monolith vs Modular is the Acumatica operations topic that you only get good at by doing it badly a few times.

John Kihiu12 min read

A client's CTO asked me last year, fairly bluntly, why we were "adding more code to the monolith" instead of building his new commission-calculation engine as a separate service. It's a fair question, and the honest answer required explaining that Acumatica isn't a monolith in the pejorative sense the term usually carries - it's a modular monolith, and that distinction changes the calculus on almost every "should this be a separate service" decision you'll face on an Acumatica project.

Acumatica is a modular monolith, and that's a real architecture, not a compromise

Deployed, Acumatica is one process (or a small process family - web tier, optional app server tier) sharing one database. That's the monolith part. But internally it's sharply modularized: AR, AP, GL, SO, PO, CA, IN each own their own DACs, their own graphs, and communicate through defined integration points rather than reaching into each other's tables directly. This is precisely the "modular monolith" pattern that's had a resurgence in software architecture writing over the last several years, as a reaction against premature microservices decomposition. Acumatica got there first, by necessity - a single-tenant-per-database SaaS platform serving thousands of customers cannot afford the operational overhead of forty independently-deployed services per customer instance.

The default: keep customizations in-process

Most customizations I write should live inside the same process as everything else, as a graph extension or a new graph in the same customization project. The reasons are concrete, not just convenience: you get the transaction boundary for free (your custom logic rolls back with the rest of the save if anything fails), you get PXCache's dirty-tracking for free (no separate change-detection mechanism to build), and you get the event pipeline's ordering guarantees for free (RowPersisting fires inside the same transaction as everything else touching that document). Pulling logic out of process means rebuilding all three yourself, badly, because you don't have access to the internals that make them work.

C# · in-process is the default
public class APInvoiceEntry_CommissionCalc : PXGraphExtension<APInvoiceEntry>
{
    public static bool IsActive() => true;

    // Runs inside the same transaction as the bill's own persist.
    // If this throws, the whole save rolls back - no distributed
    // transaction, no compensating action, no eventual consistency
    // to reason about.
    protected virtual void _(Events.RowPersisting<APInvoice> e)
    {
        if (e.Row == null) return;
        var ext = e.Row.GetExtension<APInvoiceExt>();
        ext.UsrCommissionAmount = CalculateCommission(e.Row);
    }
}

When breaking out of the process is worth the cost

There are real reasons to pull logic out of Acumatica's process, and I've done it on several projects, but the bar is higher than "this feels like it should be its own service." The cases that have actually justified it for me:

None of these apply to "this logic feels conceptually separate." Conceptual separateness is what module boundaries and well-organized customization projects are for; it doesn't require a network hop.

Every process boundary you add, you now own the failure modes of

The CTO's instinct - separate service, separate deployability - is not wrong in the abstract. What it misses is that Acumatica's Business Events, webhooks, and REST API already give you a clean integration seam for the cases that genuinely warrant it, without hand-rolling a distributed system. The moment logic leaves the process, you've signed up for retry policies, idempotency, monitoring a second deployable, and reasoning about what happens when the external service is down during month-end close. I've seen that trade-off pay off exactly when the four bullet points above apply, and cost more than it saved every other time.

Wrapping up

Acumatica's monolith is modular by design, not an accident you need to architect your way around. Default to keeping customizations in-process as graph extensions, where you get transactional consistency and the event pipeline for free. Reach for a separate service only when the work is genuinely long-running, has a different scaling shape, needs an independent release cadence, or needs a runtime Acumatica can't provide - and when you do, use Business Events, webhooks, or the REST API as the seam, not a direct database connection into Acumatica's schema from an external process.

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.