Tag: Apex

  • Lightning Web Components: building for reuse, not just for one screen

    Most Lightning Web Components I inherit were built for one screen and never touched again until the second screen needed almost the same thing. Someone copies the folder, renames it, tweaks a field reference, and ships it. Six months later there are four components doing the same job with four slightly different bugs. Nobody planned that. It’s just what happens when reuse isn’t a design decision from the start.

    Building for reuse doesn’t mean over-engineering a component before you know if it’ll ever be reused. It means making a small number of choices early that keep the door open, so the second and third use case are a configuration change instead of a fork.

    What reuse actually means here

    Reuse in LWC isn’t about component count. A component that only ever lives on one page can still be badly designed, and a component copied into five places can still be tightly coupled if every copy needed manual edits to work. The real test is simpler: can someone drop this component onto a different object, a different record type, or a different flow without opening the JavaScript file?

    If the answer is no, it’s not reusable. It’s duplicable. Those are different things, and the difference shows up in maintenance cost about a year later, usually right when nobody remembers why there are four almost-identical versions floating around.

    The habits that quietly block reuse

    Hardcoded object and field references

    A component that queries Case fields directly in its JavaScript, or references a specific record type by hardcoded Id, works exactly once. The next team that wants the same UI on Work_Order__c can’t reuse it. They fork it. Now there are two components with the same template and diverging logic, and the next bug fix has to land twice.

    The fix isn’t complicated. Pass the object API name, field list, or record type as a public property (@api) instead of writing it into the component. Wire adapters like getRecord and getFieldValue already support dynamic field references built from those properties. Most teams know this. Fewer actually do it under a deadline, which is exactly when the shortcut gets taken and never revisited.

    Business logic sitting where the template can see it

    When validation rules, formatting logic, and layout are all written in the same JavaScript controller, changing one thing risks breaking another. A component that decides both “how do I calculate this discount” and “how do I render this card” is doing two jobs badly instead of one job well. Split the calculation into its own module, imported where needed, and let the component’s controller focus on wiring data to the template. That calculation module can then get reused in a completely different component, or in Apex-adjacent client logic, without dragging the UI along with it.

    One component trying to handle every variant

    The opposite failure is just as common: a single component with a dozen boolean @api flags controlling which sections render, because someone kept bolting new requirements onto the original instead of composing smaller pieces. I’ve seen components with eleven optional properties where changing one variant meant testing all eleven combinations to make sure nothing else broke. That’s not reuse. That’s a maintenance trap wearing a reuse costume.

    Patterns that actually hold up

    Public properties over internal fetches

    Where possible, have the parent pass data down rather than having every child component independently wire its own Apex call or record query for the same underlying data. It’s tempting to make each component self-sufficient, but five components each running their own query for the same account fields multiplies API calls and cache misses for no real benefit. A parent that fetches once and distributes via properties is both faster and easier to reason about.

    Events up, properties down

    Keep the data flow direction consistent. Parent to child is properties. Child to parent is custom events. A component that reaches up into its parent’s state, or that manipulates a shared record outside its own boundary, breaks the moment it gets moved into a different parent context. If a component only ever communicates through properties in and events out, it can be dropped into an entirely different page without anyone reading its source code first.

    Composition over configuration flags

    Instead of one component with many optional sections, build a few small, focused components and compose them in the parent template. A “record header,” a “related list,” and an “action bar” as three separate components are each easier to test, easier to reuse individually, and easier to reason about than one twelve-property mega-component trying to be all three depending on which flags are set.

    A working example

    On a contact-centre implementation I worked on, the original requirement was a compact case summary card for the agent console. Six weeks later, the same card was wanted on the account page, then on a custom Utility Bar panel, then embedded in a Flow screen. Because the first version had already pulled its object, its field set, and its “compact vs full” display mode out into public properties, none of those three later requests needed new component code. They needed configuration in the Lightning App Builder and, in the Flow case, a handful of input variables mapped to existing properties. What would have been three separate build tickets became one afternoon of admin configuration.

    That’s the actual payoff of designing for reuse. It’s not a cleaner GitHub repo. It’s fewer tickets landing on a developer’s desk for something an admin could configure themselves, if the component had been built to allow it.

    When it’s genuinely not worth it

    Not every component needs this treatment. A one-off internal tool used by two people, or a component built for a feature that’s explicitly temporary, doesn’t need public properties and composed sub-components designed for a future that may never arrive. Over-abstracting a throwaway component wastes time that should go toward the components that actually sit on multiple pages: record headers, related list variants, quick-action panels, anything that shows up in more than one Lightning page across the org.

    The judgment call is knowing which is which before you start typing, not after the third fork shows up in a pull request.

    A quick check before you build

    • Does this component reference a specific object, field, or record type directly in the JavaScript? If yes, can it be a public property instead?
    • Is there business logic mixed into the same file as the rendering logic? If yes, can it move to its own module?
    • Does the component need to reach outside its own boundary to work, touching parent state or global variables? If yes, that’s a sign it won’t survive being moved.
    • Would a second use case require someone to open and edit the JavaScript, or just change configuration? The second answer is the one you want.

    None of this takes much longer to build the first time. It just means resisting the urge to hardcode the one thing you know for certain right now, because the certainty rarely lasts past the next sprint.

  • Governor limits in bulk trigger design: patterns that actually scale

    Most Apex trigger bugs aren’t logic bugs. They’re volume bugs. Code that works fine in a dev org against three test records falls over the first time someone imports 400 leads or a nightly integration batches 2,000 case updates in one go. By then it’s live, and the failure looks like a mystery instead of what it actually is: a trigger that was never built to run more than once per transaction.

    I’ve reviewed enough trigger code across enterprise orgs to know the failures repeat. Same handful of anti-patterns, different object, different team. Here’s what actually holds up under volume, and where the line sits between “fix the trigger” and “this needs to be async.”

    Why governor limits catch people off guard

    Salesforce doesn’t limit how many records you can touch. It limits how many times you can call certain operations within a single transaction: 100 SOQL queries, 150 DML statements, 10,000 DML rows, 10 seconds of CPU time. A trigger that fires once per record, not once per batch, blows through those limits the moment more than a handful of records land in the same transaction.

    The trap is that single-record testing hides the problem completely. You insert one lead, the trigger fires, one SOQL query runs, everything’s fine. Nobody sees the SOQL-inside-a-for-loop until a data load or an API integration sends 200 records through at once and the transaction dies mid-way with a “Too many SOQL queries: 101” error that tells you almost nothing about which line caused it.

    The pattern that fixes 90% of it: bulkify first, always

    Bulkification isn’t an optimisation you add later. It’s the baseline shape of correct trigger code. The rule is simple: never put a query or a DML statement inside a loop over trigger records.

    Collect, then query once

    Instead of querying related records for each item in Trigger.new, pull every relevant ID into a Set<Id> first, run one SOQL query against the whole set, and build a map keyed by the field you need to match on. Every lookup after that is a map read, not a query. This one change is usually the difference between a trigger that handles 5 records and one that handles 5,000.

    Batch the DML the same way

    Same logic applies on the way out. Build a List of records to update or insert as you process the trigger set, and fire one DML statement at the end. Not one per record, and not one per sub-loop either. If you’re updating related records based on the trigger object, group them into a single list before the DML call, even if that means restructuring the loop to build up state first and act second.

    Use maps instead of nested loops

    Nested for-loops over related record lists look harmless until the data volume climbs. A loop inside a loop is O(n²), and at a few hundred records that’s suddenly tens of thousands of iterations for logic that should have been a single map lookup. If you find yourself looping through child records for every parent record, stop and build a Map<Id, List<SObject>> instead, keyed by the parent ID, populated in one pass.

    The pattern people skip: recursion control

    Bulkification gets the attention. Recursion is the one that actually causes production incidents. Code review rarely catches it, and the failure usually shows up months later, when someone adds a second trigger action that happens to update the same object.

    If your trigger updates the record it’s firing on, or updates a related object whose trigger updates back, you can end up re-entering the same trigger multiple times in one transaction without meaning to. The standard fix is a static boolean flag, set to true the first time the trigger body runs and checked at the top of every subsequent entry, so the second and third passes short-circuit. A cleaner version of the same idea uses a static Set<Id> of record IDs already processed rather than a blunt on/off switch, which matters when the trigger legitimately needs to fire again for records it hasn’t seen yet in the same transaction.

    Either way, don’t assume “it worked in the sandbox test” rules this out. Recursion usually needs a specific chain of updates to trigger, and that chain often doesn’t exist until someone builds a second automation six months after the first one shipped.

    One trigger, one handler, per object

    This is more of an architecture decision than a governor-limit fix directly, but it prevents the two problems above from compounding. Multiple triggers on the same object, each written independently, means multiple separate paths that can each query or DML inside a loop, and no single place to coordinate recursion control or execution order. A trigger handler pattern, one trigger per object that delegates to a handler class with clearly separated before/after insert/update/delete methods, gives you one place to enforce bulkification and one place to manage state across the whole transaction. It also means a new developer can find the logic instead of hunting through however many trigger files have accumulated on a Case object over the years.

    When bulkification isn’t enough

    Sometimes the honest answer isn’t a better trigger. It’s moving the work out of the trigger context entirely. If a trigger needs to make a callout, that can’t happen synchronously at all, callouts from triggers have to go through a Queueable or a future method. If the processing genuinely needs to touch tens of thousands of records, batch Apex with its own governor limit reset per chunk is the right tool, not a trigger straining to stay under 10,000 DML rows in one shot.

    A good rule of thumb: if you’re bulkifying correctly and still hitting limits on realistic data volumes, that’s a signal the logic belongs in an async context, not a sign you need a cleverer synchronous workaround.

    Test at the volume that actually breaks things

    A test class that inserts one record and asserts the trigger fired proves almost nothing about whether the trigger scales. Write at least one test method per trigger that inserts 200 records (the standard batch size Salesforce uses for bulk operations) and confirms it completes without hitting limits. That test will catch the SOQL-in-a-loop mistake in about the time it takes to run, long before it reaches a client’s production org during a data migration or an integration spike.

    None of this is exotic. Bulkify the queries, bulkify the DML, control recursion, keep one handler per object, and push genuinely large or callout-dependent work into async processing. The teams that get burned aren’t missing knowledge of these patterns. They’re skipping them under deadline pressure on the trigger that “only handles a couple of records anyway”, right up until it doesn’t.

  • From batch to real-time: what changes when order fulfilment stops running on a schedule

    Most order fulfilment builds on Salesforce start the same way: a scheduled job runs every fifteen minutes, or every hour, and picks up whatever’s ready to move. It works. Nobody questions it for the first year. Then the business grows, volumes climb, and someone in operations starts asking why an order placed at 9:01am doesn’t ship until the 9:15 run picks it up. That question is usually where the batch-to-real-time conversation actually starts, not in an architecture review.

    What batch processing quietly costs you

    A scheduled batch job isn’t wrong. It’s a reasonable default when volumes are low and nobody’s watching the clock. But it carries a hidden tax that only shows up once you look for it: every record sits in a queue for an average of half the run interval before anything happens to it. Run every fifteen minutes, and you’ve built in a 7.5 minute average delay before a single fulfilment step even starts. Stack three or four sequential batch stages, each with its own schedule, and you can lose the better part of an hour before an order actually leaves the warehouse.

    That delay is invisible until a customer calls asking where their order is, or a warehouse team flags that they’re getting work in inconsistent bursts instead of a steady stream. I’ve seen contact-centre teams build entire workarounds around a batch cadence they didn’t even know existed, manually checking and re-triggering processes because “it usually catches up by lunchtime.”

    The failure mode nobody plans for

    There’s a second cost that matters more than latency: batch jobs fail as a unit, or close to it. If a bulk job hits a governor limit or a bad record halfway through a batch of 200, you’re now debugging which of those 200 actually completed, which is a much worse afternoon than debugging one failed record in isolation. Batch design tends to optimise for throughput and quietly deprioritise traceability. Real-time processing, done properly, forces you to think about one record at a time, which makes failures easier to isolate and easier to explain to the business.

    The actual trigger for going real-time

    It’s rarely “we want to modernise.” It’s almost always a specific, measurable pain point: a customer-facing SLA that batch timing can’t meet, a warehouse team that needs work distributed evenly rather than in spikes, or an integration partner who expects near-instant confirmation. If you can’t point to one of those, real-time isn’t a priority yet, and rebuilding a working batch process for its own sake is a good way to introduce risk without a return.

    Where it does matter, the shift usually isn’t “replace the batch job with a trigger.” It’s a genuine change in what fires the work, what carries state, and who’s accountable when something breaks.

    What changes underneath

    Moving off a schedule means the fulfilment logic has to become event-driven rather than time-driven. On Salesforce that typically means a combination of a few things working together, not one silver bullet:

    • Platform Events or Change Data Capture firing the moment an order record actually changes state, instead of waiting for the next poll
    • Asynchronous Apex (Queueable or async processing) picking up the work so the triggering transaction stays fast and doesn’t hold a user-facing save hostage
    • Flow orchestrating the business logic where it’s genuinely declarative, with Apex reserved for the parts that need bulkification, callouts, or complex branching Flow handles poorly
    • A retry and dead-letter pattern, because real-time doesn’t mean failure-proof. Something will fail at 2am with nobody watching, and you need a way to catch it and reprocess without a human noticing a gap three days later

    That last point trips up a lot of first attempts. Teams replace the batch job with a trigger, get excited that orders now process in seconds, and forget that batch processing had accidental resilience built in: if something failed, the next run would often just pick it up again. Real-time architectures need that resilience designed in deliberately, usually through a monitoring object or a Platform Event replay strategy, because there’s no “next run” to bail you out.

    Governor limits don’t disappear, they just move

    A common assumption is that moving to per-record processing sidesteps bulk governor limits. It doesn’t. It just shifts the risk from “one batch might hit a limit” to “a burst of 500 near-simultaneous order updates might trigger 500 near-simultaneous transactions, each doing a callout.” If your real-time trigger fires a synchronous callout per record with no queuing or throttling, you’ve traded a slow, predictable batch problem for a fast, unpredictable concurrency problem. Bulkification discipline still applies. It just applies to unpredictable, uneven bursts instead of a scheduled, sizeable chunk.

    Where teams get this wrong

    The most common mistake isn’t technical, it’s scope. Teams try to make everything real-time in one release because the business asked for “faster fulfilment” without qualifying which step actually matters. Not every stage of a fulfilment process needs sub-second latency. Order confirmation to the customer might genuinely need to be instant. Inventory reconciliation with a warehouse system might be perfectly fine running every five minutes. Treating the whole pipeline as one monolithic thing that must become real-time, rather than identifying the two or three steps where latency actually causes pain, is how a two-week enhancement becomes a three-month rebuild.

    The second mistake is skipping observability. A batch job that fails is loud, usually because someone notices the batch didn’t run or the record count looks wrong. A real-time process that silently drops one event out of ten thousand is quiet, and quiet failures compound. Before flipping the switch, you need a way to answer “did every order that should have processed actually process,” and that usually means a reconciliation job running in the background, ironically often on… a schedule. Real-time fulfilment and scheduled reconciliation aren’t opposites. The second one is what makes you trust the first.

    A path that doesn’t require a rewrite

    You don’t need to tear out the batch job on day one. A practical sequence looks like this: identify the single fulfilment step causing the most business pain, build an event-driven path for that one step alongside the existing batch process, run both in parallel long enough to compare outcomes, then retire the batch step once you trust the numbers. Repeat for the next step if it’s warranted. Most orgs never need to fully retire batch processing everywhere. They end up with a hybrid: real-time where seconds matter, scheduled where they don’t, and a clear, documented reason for which is which.

    That’s a more honest answer than “we went real-time” anyway. The goal was never the architecture. It was the customer who used to wait fifteen minutes and now doesn’t.