Tag: Decision Test

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

  • Marketing Cloud and Service Cloud: where the data should actually live

    Every org running both clouds eventually hits the same argument. Marketing wants the subscriber list clean and current. Service wants the case history complete and fast to load. Somewhere in the middle, a contact record gets updated in one system, doesn’t sync to the other for six hours, and an agent ends up apologising to a customer for an email campaign they already unsubscribed from. Nobody designed for that outcome. It just happens when nobody decided where the data actually lives.

    The question everyone asks wrong

    The usual framing is “should Marketing Cloud and Service Cloud share a database.” That’s not really the question, and chasing it wastes weeks. The real question is narrower: for each piece of data, which system is allowed to change it first, and which system is just reading a copy. Ownership, not location. Once you separate those two ideas, most of the architecture falls out on its own.

    I’ve seen teams spend a full discovery phase debating whether to sync every custom field bidirectionally, when the actual problem was that three fields had two owners each. Fix the ownership model and the sync design gets simple. Skip it and no integration pattern will save you.

    Service Cloud owns the case, Marketing Cloud owns the send

    Start with what each platform is actually good at. Service Cloud is built around the case object and everything that touches a live interaction: entitlements, SLAs, Omni-Channel routing, knowledge attach, agent productivity. Marketing Cloud is built around segmentation, journey orchestration and send-time optimisation at volumes Service Cloud was never designed to handle.

    That split points at a simple rule. Anything that changes because of a conversation with the customer (a case status, a resolution, an escalation, a preference an agent captured mid-call) should be written in Service Cloud first. Anything that changes because of engagement behaviour (an email open, a click, a journey exit, a subscription status) should be written in Marketing Cloud first. Trying to make either system the single source of truth for both kinds of data is where the pain starts.

    Where contact data actually belongs

    Core identity fields, name, primary email, phone, account association, belong in Salesforce as the CRM system of record. Marketing Cloud should hold a subscriber record that references back to that identity, not a parallel version of it. I’ve worked with orgs that let Marketing Cloud own email address changes because “marketing needed it faster.” Within a year they had two email addresses for a meaningful chunk of their contact base, one in each system, and nobody could say with confidence which one was current. Untangling that took longer than the original integration project.

    The sync pattern that actually works

    Once ownership is clear, the sync direction should follow it. Core identity and case-driven fields flow Salesforce to Marketing Cloud. Engagement and journey data flows Marketing Cloud to Salesforce, usually as read-only fields an agent can see but not edit. Two-way sync on the same field is where most of the genuinely hard bugs live, because now you need conflict resolution logic, and conflict resolution logic is where a “simple” integration turns into a six-week debugging exercise involving timestamps, retry queues and someone asking why a customer’s phone number reverted itself.

    Marketing Cloud Connect vs a custom integration

    Marketing Cloud Connect handles the standard case well: syncing contacts, data extensions built from Salesforce reports, and triggering journeys off Salesforce events. If your requirements sit inside that footprint, use it. Don’t build a custom middleware layer to solve a problem the managed package already solves, that’s effort spent maintaining infrastructure instead of shipping outcomes.

    Where I reach for something custom is high-volume event-driven sync (thousands of engagement updates a day that Connect’s polling cadence can’t keep up with), or complex transformation logic between the two data models. Even then, keep the ownership rule intact. A custom integration that blurs who owns what is worse than no integration at all, because it looks automated while quietly making the data less trustworthy.

    Where Data Cloud changes the calculus

    Data Cloud shifts this conversation in a useful direction. Instead of Service Cloud and Marketing Cloud each holding their own partial view and syncing fields back and forth, Data Cloud can sit underneath both as the unification layer, harmonising identity across sources and exposing a single calculated profile to each cloud. That doesn’t remove the ownership question, it just moves it up a level: now you’re deciding which source system is authoritative for each field inside Data Cloud’s identity resolution rules, rather than negotiating a point-to-point sync between two orgs.

    It’s worth genuinely evaluating for any org running both clouds at scale, but it’s not a shortcut around doing the ownership exercise. Teams that skip straight to Data Cloud without agreeing on ownership first end up with the same duplicate-identity mess, just inside a more expensive tool.

    A decision test that holds up in practice

    When a new field or object comes up in a design session, I ask one question: what event causes this data to change? If the answer is a customer interaction an agent is part of, it belongs in Service Cloud. If the answer is a marketing behaviour, an open, a click, a journey stage, it belongs in Marketing Cloud. If the answer is genuinely both, that’s a signal the field is doing two jobs and probably needs to be split into two fields with two clear owners.

    Apply that test consistently and the integration design writes itself. Skip it and you’ll be back in six months arguing about why the unsubscribe list and the case queue disagree with each other, and nobody will remember which system was supposed to be right.