Acumatica · Customization

Multiple DAC Extensions on the Same Acumatica DAC

A mature Acumatica instance - three years in, five customization projects deep - tends to accumulate several independent PXCacheExtension classes on the same base DAC, one per.

John Kihiu12 min read

A mature Acumatica instance - three years in, five customization projects deep - tends to accumulate several independent PXCacheExtension classes on the same base DAC, one per project, written by different developers who never talked to each other. Individually each is fine. Together they raise questions nobody thinks about until something breaks: can two extensions declare the same field name? What happens if they both hook the same event? Here's what actually happens, not what the training material implies.

This is the expected shape of a real instance, not a rare pattern

Any given production DAC - ARInvoice, SOOrder, InventoryItem - routinely carries a dozen extensions once ISVs, internal customizations, and third-party integrations are all accounted for. Acumatica merges every active PXCacheExtension<T> targeting a DAC into one composite cache row shape at runtime. You never see the seams from inside a single extension's code; each one behaves as though it's the only one, which is exactly the isolation the design is meant to provide.

C#
// Project A's extension
public sealed class SOOrderExt_ProjectA : PXCacheExtension<SOOrder>
{
    public static bool IsActive() => true;
    [PXDBString(20)]
    public string UsrRegionCode { get; set; }
    public abstract class usrRegionCode : PX.Data.BQL.BqlString.Field<usrRegionCode> { }
}

// Project B's extension - same base DAC, entirely separate field, no conflict
public sealed class SOOrderExt_ProjectB : PXCacheExtension<SOOrder>
{
    public static bool IsActive() => true;
    [PXDBDecimal(2)]
    public decimal? UsrLoyaltyDiscount { get; set; }
    public abstract class usrLoyaltyDiscount : PX.Data.BQL.BqlDecimal.Field<usrLoyaltyDiscount> { }
}

Field name collisions: the schema sync catches it, but only at deploy time

Two extensions declaring the same field name on the same DAC is a genuine conflict - there's exactly one underlying column, and Acumatica's customization publish process (and the underlying schema sync) will surface an error rather than silently letting one win. This is why a shared naming convention across a client's customization projects matters more than it seems like it should early on: UsrRegionCode is a name generic enough that two unrelated projects independently choosing it isn't far-fetched. I've seen this collision block a production deploy the week it mattered most, purely because two vendors used the same obvious field name for different purposes.

A naming convention with a project or vendor prefix pays for itself once

On multi-vendor instances I now push for a convention like Usr{VendorInitials}{FieldPurpose} instead of bare Usr{FieldPurpose}, specifically to avoid this collision. It looks slightly uglier in code review and saves a very bad afternoon later when a second vendor's customization needs to coexist with yours.

Multiple extensions both hooking the same event: no collision, but ordering is real

Unlike fields, two extensions can both declare a RowPersisting<SOOrder> handler without any compile or deploy error - both run. The order they run in is determined by extension registration order, which in turn follows customization project publish order on the instance, not something declared anywhere in your code. This is fine when the two handlers are independent (each validates something unrelated). It becomes a real bug when one extension's logic depends on a value another extension's handler is supposed to have set first, and you have no declared way to guarantee that ordering:

C#
// Fragile: assumes ProjectA's handler already set UsrRegionCode
// by the time this one runs. True today, not guaranteed after a republish.
protected virtual void _(Events.RowPersisting<SOOrder> e)
{
    if (e.Row.UsrRegionCode == "EU" && e.Row.UsrLoyaltyDiscount > 10m)
        throw new PXSetPropertyException("EU orders cap loyalty discount at 10%.");
}

The fix isn't to fight for ordering control - it's to not depend on it. Either read the value the other extension would have set from a source both handlers can compute independently (a shared BQL lookup, not a field another handler happens to populate first), or consolidate genuinely coupled logic into one extension so the dependency is explicit in one file instead of implicit across two unrelated customization projects.

IsActive() is what actually keeps extensions from interfering

Every extension should implement IsActive() deliberately - tied to a license feature, a setup checkbox, a company-level flag - rather than hardcoding true. On instances where not every branch or tenant wants every customization active, this is the mechanism that keeps an unused extension from firing its handlers at all, which sidesteps a whole category of cross-extension interference simply by not running the code when it isn't relevant to the current context.

Wrapping up

Multiple PXCacheExtension classes on one DAC is the normal, expected state of any real Acumatica instance, and the framework merges them cleanly as long as field names don't collide - which the schema sync will catch, but only at publish time, so a naming convention up front is cheaper than the deploy-day surprise. Event handlers from different extensions coexist without conflict, but ordering across extensions is not something your code should ever depend on; if one handler genuinely needs another's result, compute it independently or merge the logic into one place instead of hoping publish order stays stable.

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.