Tag: Configuration

  • Data migration mistakes that show up three months after go-live

    Every data migration gets tested at cutover. Record counts match the source system, a sample of accounts and contacts gets eyeballed, a handful of business users click through their most important records and confirm nothing looks obviously wrong. Hypercare runs for two or three weeks, the incident count stays low, and everyone moves on. Then, somewhere around month three, the real problems start showing up. And by then nobody’s thinking about the migration anymore, so the first instinct is to blame the new automation, or the new integration, or “something Salesforce changed.”

    It’s rarely Salesforce. It’s usually the migration, and the reason it took three months to notice is that the mistakes that matter aren’t the ones cutover testing is built to catch.

    Why cutover testing misses the damage that matters

    Cutover validation answers one question: did the records land? Count comparisons, field-level spot checks, a UAT pass against a sample. All of that is necessary. None of it tests what happens once real users, real automation and real time start interacting with the migrated data. The bugs that surface later aren’t data quality bugs in the traditional sense. They’re mismatches between what the migration assumed and what the org actually does with a record over its lifecycle.

    I’ve seen this pattern often enough on enterprise orgs that I now treat it as the default outcome of any migration that only tests for “did it land correctly,” rather than “will it behave correctly.”

    The five mistakes that don’t show up on day one

    Backdated CreatedDate breaking date-based automation

    Migrated records usually carry their original creation date, which is the right call for reporting. But entitlement processes, SLA timers, and any flow with a time-based path built on CreatedDate or a derived field will behave differently for a record created three years ago versus one created this morning. A case migrated with its original 2023 timestamp can slip straight through an SLA escalation that was designed around cases created after go-live. Nobody notices during hypercare because hypercare is full of freshly created records, not migrated ones. It shows up the first time someone runs an aged-case report or an escalation fires (or doesn’t fire) on a record nobody remembers was migrated.

    Ownership that technically works but breaks sharing

    A common shortcut: assign every migrated record to the integration user or a generic “Data Migration” user, then reassign in a follow-up script. When that follow-up step is incomplete, or when territory and queue assignment rules only fire on record creation (not on data load), you end up with records that are visible to the wrong people, invisible to the right people, or sitting in a queue nobody’s monitoring. This one is genuinely hard to catch at cutover because sharing visibility depends on who’s looking. A system admin testing UAT sees everything. The actual case owner three months later doesn’t.

    External IDs that aren’t as unique as they look

    If the migration used an external ID field to support future upserts from an integration, and that field wasn’t validated for true uniqueness against production edge cases (leading zeros stripped, whitespace, case sensitivity on a text field instead of a proper external ID field), you get silent overwrites. The first few weeks nothing collides because the integration is dealing with genuinely new records. Then a legacy record with a near-duplicate key gets touched by an inbound sync and quietly overwritten. Nobody gets an error. The data is just wrong, and it stays wrong until someone happens to compare it against the source system.

    Roll-up and formula fields that drift instead of break

    Roll-up summaries recalculate on the trigger events they’re built for: a child record inserted, updated or deleted. A bulk data load that doesn’t fire those events in the expected order, or that loads parent and child records out of sequence, can leave a roll-up value that was correct on migration day and never gets touched again unless something forces a recalculation. Three months in, someone compares a roll-up total against the underlying child records and finds a gap. It’s not corruption. It’s a number that was right once and nobody ever asked Salesforce to check it again.

    Validation rules disabled for the load and never fully restored

    Almost every migration disables validation rules, and sometimes triggers, to get bulk data through without a wall of errors. The riskier version of this is when a rule gets disabled at the metadata level rather than bypassed for the load user specifically, and re-enabling it becomes a manual step buried in a cutover runbook that doesn’t get checked off with the same rigor as the actual data load. Bad records start entering the system weeks later through completely normal channels, because the guardrail that should have stopped them was never switched back on.

    Catching these before they’re three months old

    None of this requires exotic tooling. It requires treating the first ninety days after go-live as part of the migration, not the period after it.

    • Before the load, list every automation with a time-based or CreatedDate-dependent path and decide explicitly how migrated records should behave against it, rather than discovering the answer in production.
    • Test sharing visibility as the actual end users who will own the records, not as a system admin. Profile-level testing catches what UAT with a full-access login won’t.
    • Run a uniqueness and format audit on any field used as an external ID or upsert key, against real edge cases from the source system, not a clean sample.
    • Force a full recalculation of roll-up and formula fields after the load completes, then reconcile a sample against the source rather than assuming the load triggered it correctly.
    • Put “validation rules and triggers restored to pre-migration state” on the cutover checklist as its own signed-off line item, with a metadata diff to prove it, not a verbal confirmation.
    • Schedule a deliberate data health check at thirty, sixty and ninety days post go-live. Nobody plans for this, which is exactly why it catches things.

    The teams that get burned by this aren’t careless. They’re just measuring the wrong moment. A migration that “worked” on go-live day and a migration that holds up at day ninety are different things to test for, and only one of them is the one your users actually experience.

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

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