The hardest line of code I wrote during this US launch was a boolean.
if (opp.isUSVenue) { ... }
Everything US-specific had to live behind that flag: sales-tax calculation, multi-state tax entities, MM/dd/yyyy formatting, a service charge that doesn’t exist anywhere else. The same Salesforce org was already running 50+ live Australian venues and 6 in New Zealand. There was no greenfield org to play in. New region, same database, same triggers, same flows. Break the wrong thing and you stop bookings in two countries that have never heard of US sales tax.
A note on the code: this was built on an employer’s production platform, so every snippet below is illustrative pseudocode. Class names, field API names, and identifiers have been changed or generalised, and internal system names are omitted. What’s real is the platform behaviour, the failure modes, and the reasoning.
Most of the hard problems here share a root cause. I was building around two third-party managed packages, AvaTax for tax and Conga for documents, whose internals I couldn’t see, couldn’t step through, and couldn’t modify. The platform is Salesforce (API version 59.0), the language is Apex, the UI is Lightning Web Components, and deployment is the Salesforce CLI (sf) on Azure Pipelines.
Context
The booking platform is a Salesforce org where an Opportunity is a function booking and Opportunity Line Items are its products. The US brought a tax model AU and NZ don’t have. In AU/NZ, GST is baked into the displayed price, so there is no separate tax calculation. In the US, sales tax is per-state, per-jurisdiction, calculated line by line, and it has to be exact before a contract or invoice goes out.
Tax comes from AvaTax, installed as a managed package. That one sentence is the source of most of the difficulty in this post, so it’s worth being precise about what a managed package means. Its Apex is hidden. Its triggers fire on objects I share with it. When it throws, the stack trace stops at the package boundary. I owned the consequences of its behaviour without owning the behaviour. The whole job was integrating predictably with code I had to treat as a black box.
Problem 1: tax is a callout, and triggers can’t make callouts
AvaTax reaches out to an external service. Apex forbids a callout inside a trigger’s synchronous transaction, and you wouldn’t want one anyway, since tax recalculates on Opportunity and line-item edits that happen constantly while a booking is built. Inline calculation would chain every save to an HTTP round trip.
So the work gets enqueued. The trigger handler collects affected Opportunity IDs and hands them to a Queueable, after checking there is budget left in the async-job governor limit:
if (Limits.getQueueableJobs() < Limits.getLimitQueueableJobs()) {
System.enqueueJob(new TaxSyncQueueable(oppIdsToRecalc, logData));
hasEnqueuedAsyncJob = true;
}
The hard part isn’t enqueuing. It’s stopping. The Queueable updates Opportunities. Those updates re-fire the trigger. The trigger re-enqueues the Queueable. That loop runs until it hits the governor limit and the transaction dies. And because AvaTax also listens on these objects, part of the loop was happening inside package code I couldn’t read, so the only way to map it was to instrument my side and infer theirs from the logs.
The fix is layered. A static hasEnqueuedAsyncJob flag stops a single transaction enqueuing twice. On top of that, the enqueue is suppressed whenever the code is already running asynchronously, because that’s the signature of the package re-entering me:
Boolean inAsyncContext = System.isQueueable() || System.isFuture();
// Suppress the enqueue here to break the chain:
// Queueable -> DML -> Trigger -> enqueue -> Queueable -> ...

That System.isBatch() || System.isFuture() || System.isQueueable() guard ends up in several spots, each one a re-entry point I found by reproducing the loop and reading where control came back in. None of them are obvious from outside. All of them are load-bearing.
Problem 2: UNABLE_TO_LOCK_ROW, when one of the writers is invisible
Under real booking volume the symptom was UNABLE_TO_LOCK_ROW, and the first job was working out who held the lock, because one of the two writers was inside the managed package.
Here is the trace. Tax calculation makes AvaTax update the Account. My own handler was also updating that Account in the same transaction, to set its US tax entity (the org keeps a separate entity for one state and a default for the rest). Two writers, one Account row, one lock. Standard contention, except I could only see half of it. I confirmed the package’s write by elimination: remove my Account update, the lock disappears. I couldn’t read its code to confirm it directly.
Once the cause was clear there were two ways out: serialise the writers, or stop them sharing the row in one transaction. For the Account contention I chose the second, moving my write off the transaction the package runs in and into a separately-invoked service:
// Moved off the tax-calculation transaction entirely: when the managed
// package updates the Account during calc, a second writer on the same
// row triggers UNABLE_TO_LOCK_ROW. Decoupling removes the contention.
private static void updateAccountTaxEntity(Opportunity opp) { ... }
For the service-charge recalculation, a percentage charge US venues add on top of the line-item total, the contention was self-inflicted and transient, so retrying was correct. Here I used partial-success DML and classified the failures instead of letting them throw:
Database.SaveResult[] results = Database.update(oppsToUpdate, false);
Set<Id> retryable = new Set<Id>();
for (Database.SaveResult r : results) {
if (r.isSuccess()) continue;
for (Database.Error e : r.getErrors()) {
if (e.getStatusCode() == StatusCode.UNABLE_TO_LOCK_ROW) {
retryable.add(failedOppId); // transient, try again
} else {
terminalErrors.add(describe(e)); // real, surface it loudly
}
}
}
if (!retryable.isEmpty() && attempt < MAX_ATTEMPTS) {
System.enqueueJob(new ServiceChargeSyncQueueable(retryable, attempt + 1));
}
Re-enqueue the lock failures with a bounded attempt counter. Throw on everything else. The discipline is in the classification. Retrying blindly buries real defects, and throwing on a transient lock fails bookings that just needed to wait a beat. Two fixes that look opposite, move the write out versus retry the write, because the two root causes were opposite: cross-package contention I couldn’t serialise, against concurrency on my own records that I could.
Problem 3: region-aware formatting from one codebase
US dates are MM/dd/yyyy, AU/NZ are dd/MM/yyyy, and times have to land in the venue’s actual timezone (US venues alone span Eastern through Pacific). No second codebase: the conversion reads the venue’s timezone off the record and branches on the same isUSVenue flag.
TimeZone tz = TimeZone.getTimeZone(opp.venue.timezone);
String shown = opp.isUSVenue
? value.format('MM/dd/yyyy, h:mm a', tz.getID())
: value.format('dd/MM/yyyy, h:mm a', tz.getID());
It writes display strings back onto the Opportunity, guarded by an isExecuting static so its own update doesn’t recurse through the trigger. Unglamorous. But get it wrong and you ship a contract dated 06/04 that the customer reads as June 4th and the venue reads as April 6th, on a legal document, in a country that reads dates the other way.
Problem 4: documents that must not run before tax is final
Conga generates the US invoices and contracts, and Conga is the second black-box package in this story. Run a Conga job before AvaTax has settled and it stamps stale tax onto a legal document. Because tax is asynchronous (see Problem 1), “is the tax current right now?” is a real question whose answer changes over the life of a transaction, and neither package exposes a hook to coordinate the other.
So I built the coordination myself, out of a single readiness flag on the Opportunity. The finalize step sets it false before kicking off recalculation. The recalculation flips it true when AvaTax reports current. Document generation refuses to run until it’s true. It’s a small explicit state machine straddling the async boundary (not ready, recalculating, ready, generate) sitting in the one place both packages happen to share: my own object’s fields. When you can’t make two third-party systems talk to each other, you make them both talk to a field you control.

Deploying into a shared, live org
I never had “deploy everything and see.” Changes land in a live multi-region production org, so the pipeline deploys only the diff. On Azure Pipelines, SFDX-Git-Delta computes the metadata delta between the last commit and HEAD and deploys just that:
sf sgd source delta --to HEAD --from HEAD^ --output deploy/ \
--source force-app/main/default --ignore .forceignore
sf project deploy start \
--manifest deploy/package/package.xml \
--post-destructive-changes deploy/destructiveChanges/destructiveChanges.xml \
--target-org targetOrg
A delta of a handful of classes validates and runs its tests in a fraction of the time a full-source deploy needs. That is what makes small, frequent, reversible changes into shared production safe rather than terrifying.
What I’d do differently
The recursion guards are the part I’m least proud of. By the end there were System.isQueueable() checks scattered across the tax handler, each one a patch over a re-entry loop found in production. The right design is a single explicit recursion-control layer, one owner of “have we already processed this record in this transaction?”, instead of every method re-deriving it. The flags work. The problem is that the next engineer has to reconstruct the intent from comments that are bug post-mortems, not design.
I’d also reach for partial-success DML with error classification on day one, not after the all-or-nothing version had already failed in production. Anything that writes to a record a managed package also writes to should assume contention from the start. That is the meta-lesson of the whole project. When you integrate with code you can’t see, you don’t design against its implementation, you design against its observable behaviour. You instrument relentlessly to find out what that behaviour actually is. And you keep every coordination point on your own side of the boundary, where you can still change it.
Close
The US launch didn’t require re-architecting the AU/NZ booking engine, and that restraint was the win. A region flag, an async tax path that respects governor limits, two concurrency fixes aimed at two different root causes, a self-built readiness gate between two uncooperative packages, and a delta pipeline were enough to run three tax regimes out of one org. Most of the engineering wasn’t writing US features. It was making three systems I only half-controlled behave predictably, and making sure none of it could reach across the isUSVenue boundary and touch a country that was already, quietly, taking bookings.