Tag: Real-time

  • What a good Salesforce technical lead actually does in code review

    Most code review comments on a Salesforce PR are really about formatting. Bracket placement, an unused variable, a missing test class. None of that is wrong to flag, but it’s not the job. The job is catching the thing that passes every static check and still takes the org down at 50,000 records, three releases from now.

    Reading for what scales, not what compiles

    A trigger that queries inside a for loop will pass code coverage. It’ll pass a linter. It might even pass a demo with ten test records. It fails the first time someone runs a data load, and by then the person who wrote it has usually moved to a different project. Governor limits don’t show up in a diff. You have to know what the code will do at volume, which means picturing the bulk case before you read a single line of the actual implementation.

    I ask four things before I look at logic: what triggers this, how many records can hit it in one transaction, what happens if it runs twice, and whether it depends on a specific order of operations somewhere else in the org. Most defects I catch in review live in the gap between those questions and what the developer assumed while writing the happy path.

    The bulkification checklist nobody writes down

    SOQL and DML inside loops are the obvious ones, and most experienced Apex developers catch those themselves now. The subtler failures are recursive trigger calls, callouts fired per record instead of batched, and collection logic that looks bulk-safe but still does an expensive comparison somewhere in the middle. None of that fails a unit test with 200 records. It fails a Tuesday morning data load with 40,000.

    Test coverage tells you almost nothing

    Seventy-five percent is a compliance number, not a quality signal. A test class can hit every line and still assert nothing beyond “no exception was thrown.” When I review test code, I’m checking for negative cases: a null field, a duplicate external ID, a user without the right permission set, or a picklist value that no longer exists. If every test in the class uses the same clean data setup, the coverage number is decorative.

    The better question in review isn’t “does this have tests.” It’s “would this test fail if the logic were wrong.” Write the bug in your head, then check whether any assertion in the class would catch it. Half the time it wouldn’t, and that’s the actual finding, not the coverage percentage sitting at 82%.

    Where governance debt actually gets created

    Every org I’ve worked in that’s been customised for years has the same pattern behind it. Nobody made one bad decision. A hundred small, reasonable-looking decisions got approved individually, and the org ended up with several ways to do the same thing: a validation rule here, a flow there, an Apex trigger doing the same check somewhere else. Each PR looked fine on its own.

    Catching that in review means asking a question most reviewers skip: does this already exist somewhere else in the org? Before approving a new flow that enforces a business rule, I check whether that rule is already sitting in a validation rule, a different flow, or a trigger nobody’s touched in two years. It takes an extra five minutes. It’s the difference between one source of truth and three that quietly disagree with each other by the time someone debugs a production issue.

    Naming and structure aren’t nitpicks

    A trigger handler named after the object instead of the intent. A flow with no description. An Apex class mixing unrelated responsibilities because it was faster to bolt on than to split out. These get waved through constantly because none of them break anything today. What they break is the next person’s ability to safely change the org without reading every line first, from scratch, with no context. I’ll push back on structure even when the logic is correct, because the logic being correct once isn’t the bar. The bar is someone else understanding it without asking the original author, who by then might not even be on the project anymore.

    Security review isn’t a separate step

    Field-level security, sharing model behaviour, CRUD and FLS enforcement on SOQL: these tend to get treated as a checklist item for a security audit months later, if at all. They should be part of every review, not a phase that happens afterward. Is this Apex class running without sharing when it should respect it? Does this LWC expose a method that skips permission checks a profile would otherwise enforce? Catching that in review costs one comment. Catching it in production costs an incident report and a client conversation nobody wants to have.

    What I actually leave as comments

    Not “nice work,” and not a list of style preferences dressed up as requirements. I leave the specific scenario that breaks it: this fails when the account has no owner, this recursion loop triggers if the same record updates twice in one transaction, this assumes a single currency and the org has four. Specific, reproducible, tied to a real condition that exists in this org. A developer can act on that immediately. “Please refactor for clarity” gives them nothing to act on and just adds a review cycle for its own sake.

    Good review also says yes fast. If a PR does what it needs to do, handles bulk correctly, respects sharing, and the tests would actually catch a regression, approve it. Slowing down good work just to leave a comment teaches people to dread review instead of using it.

    The habit that makes the biggest difference

    This isn’t about being harsher on people’s code. Good review protects the org the code will run in six months, not the sandbox it happened to be written in. The developer who wrote it is usually thinking about today’s ticket. The reviewer’s job is thinking about the org at scale, under load, with the security model intact, and readable by whoever inherits it next year. That gap is the whole difference between a review that catches typos and one that actually protects the platform.

  • Designing a Salesforce solution around one metric leadership will actually track

    Ask most delivery teams what a Salesforce enhancement is meant to achieve and you’ll get a feature description back. “We’re adding a custom object for renewals.” “We’re building an approval process for discounts over 15%.” Ask what number on a leadership dashboard is supposed to move because of it, and you’ll usually get a pause.

    That pause is the problem. A solution built without a target metric gets evaluated on whether it shipped, not whether it worked. Six months later nobody can say if the renewals object actually reduced churn, or if the approval process actually cut discount leakage, because nobody wired the build to a number in the first place.

    Why “it works” isn’t the same as “it moved something”

    I’ve sat in enough steering committees to know the difference between a project that’s technically complete and one that’s actually earning its budget. The first gets a green status and a demo. The second gets renewed funding next year because someone can point at a chart and say “that’s us.”

    Most orgs I’ve reviewed have dozens of automations, reports and custom fields that were built for a stakeholder request, went live, and then quietly stopped being anyone’s responsibility. Nobody’s watching whether they’re doing anything. The gap isn’t in testing. It sits earlier, at the requirements stage, long before a single flow gets built.

    Pick the metric before you open the object manager

    Before any real design work starts, force one question: what single number does this need to move, and who already looks at that number every week? Not a KPI you’ll invent for the project. One that already exists on someone’s dashboard, in someone’s board pack, or in someone’s head when they walk into a Monday meeting.

    For a contact-centre client I worked with, that number was average handle time on a specific case type. Not “improve agent efficiency.” Not “modernise the case workflow.” Handle time, on that case type, measured weekly, already tracked by the operations lead before we touched anything. Every design decision after that got tested against one question: does this reduce handle time on that case type, or doesn’t it?

    That constraint kills a lot of scope creep on its own. A nice-to-have field that doesn’t touch the metric gets deprioritised without an argument. A workaround that adds three clicks but saves the agent from a phone call gets fast-tracked, because someone can show the maths.

    What changes in the technical design

    Once you know the metric, it should show up in three places in the build, not just the final report.

    • The data model needs a field or relationship that captures the event driving the metric, at the point it happens, not reconstructed later from case comments or a separate spreadsheet.
    • The automation needs to fire close enough to that event that the data is accurate. A nightly batch job that recalculates handle time after the fact is a different design than a trigger or flow that stamps timestamps as the case moves through statuses.
    • The reporting layer needs to expose the metric in a way the person who owns it can actually check without asking IT for a custom report every Friday.

    Skip any of those three and you end up with a metric that technically exists but nobody trusts, because getting to it takes fifteen minutes and a pivot table.

    A composite example: the discount approval that didn’t cut leakage

    A typical enterprise org I’ve seen runs something like this. Finance flags discount leakage as a problem. The response is an approval process: discounts above a threshold route to a manager before the quote goes out. It ships. Everyone moves on.

    Eighteen months later, leakage hasn’t moved. The approval process captured whether a discount was approved, not what happened to margin as a result. Nobody built a field for the original list price versus the approved price at the line-item level. Nobody reported on override frequency by manager. The process existed, the metric didn’t, and finance is back asking for the same fix under a different name.

    More approval steps won’t fix that. Going back to the one number that actually matters, margin retained per deal, and checking whether the data model can produce it today, will. Usually it can’t, because the build was scoped around the workflow rather than the measurement.

    The scoping conversation this forces

    When a stakeholder brings a request, I ask them to finish this sentence: “I’ll know this worked when [metric] moves by [amount] by [date].” If they can’t finish it, the request isn’t ready for a solution design session yet. It’s a blunt filter, and a genuinely useful one for separating a real business problem from a feature someone saw in a demo.

    It also changes who signs off. A feature list gets approved by whoever asked for it. A metric commitment gets approved by whoever owns that number, and that person has a much stronger incentive to push back on scope that won’t move it.

    What this looks like in a solution design document

    Practically, add one section before the requirements table: the target metric, its current baseline, who owns it, and how the proposed build is expected to shift it. Keep it to a few lines. If you can’t fill it in, that’s the signal to go back and ask better questions before committing engineering time.

    I’ve started refusing to take a build past discovery without this filled in. It’s cut the number of “why did we build this again” conversations down to almost none, mostly because writing the metric down forces a conversation that usually gets skipped entirely.

    Where this earns its keep

    The payoff isn’t just cleaner requirements. It’s a much easier renewal conversation for whoever’s paying for the work. “We shipped the feature” competes for budget every year. “We moved handle time by 22% on the highest-volume case type” doesn’t need to compete. It gets funded again without a fight.

    That’s the actual argument for doing this on every enhancement, not just the big ones. Small requests accumulate into an org nobody can explain the value of. One metric, tracked from design through to the dashboard, is what turns a backlog of tickets into a track record.

  • 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.