Skip to main content
New tool CRON Expression Builder — preview next run times before you schedule Apex. Open the builder →
Interview prep

Salesforce integration interview questions and answers

Patterns, callout limits, retries and idempotency.

13 questions · 5 of them scenarios

Junior round

Junior Integration questions

Integration junior scenario

A callout works in the sandbox and throws in production. How do you approach it?

Testing How you read an error that names its own fix — whether you take the hint and stop there.

What they tell you

An Apex class posts to a partner endpoint. It has been running in the full sandbox for two weeks. The first production run throws System.CalloutException: Unauthorized endpoint, please check Setup->Security->Remote site settings.

junior

The message names the setting, so I would check Remote Site Settings in production first and compare it to the sandbox. But I would also look at where the URL comes from, because if it is typed into the Apex class then the sandbox and production are only working by coincidence, and the fix is a Named Credential rather than another entry in a setup list.

mid

Sandbox drift is the whole story here. Two weeks of sandbox runs proved the code, not the configuration, and the two orgs were never the same shape. So I add the remote site to unblock the release, then treat the literal URL as the actual defect: move it behind a Named Credential so the endpoint and its credentials travel as metadata, the Apex says callout:Partner_API/orders, and Remote Site Settings stops being part of the story at all. That also kills the class of bug where a sandbox refresh silently repoints an integration at live partner data.

Ask before you answer
  • Is the endpoint the same URL in both orgs, or does the sandbox point at a test host?
  • Is the URL coming from a Named Credential, a custom setting, or a string literal in the class?
Do not say this

Add the endpoint to Remote Site Settings in production and close the ticket.

That clears the error in about a minute and leaves the URL wherever it already was. The next environment fails identically, and if the URL is a literal in Apex then changing hosts is a code deployment. The exception is telling you the endpoint is not managed anywhere.

Deep dive on this
Integration junior

What limits do you have to design a callout around?

Testing Which of these you learned from documentation, and which one you learned from a failed deployment.

junior

A hundred callouts in a transaction, and 120 seconds of total callout time across all of them. Each one defaults to a 10 second timeout, which I can raise. And I cannot call out after a DML statement in the same transaction, so a trigger that saves a record and then talks to another system has to hand the callout to something asynchronous.

mid

The count is rarely what stops me. The 120 second cumulative budget is, because it is shared: three endpoints at 40 seconds each and the fourth call fails no matter how healthy it is. So I set an explicit setTimeout per call rather than leaving the 10 second default and hoping. The one that actually catches people is You have uncommitted work pending — a callout after DML in the same transaction throws, and the fix is not reordering the statements, it is moving the callout into a Queueable so the DML gets its own transaction to commit in.

Integration junior

When would you pick the REST API over SOAP?

Testing Whether you have maintained both, or read a comparison table.

junior

REST for anything new. JSON payloads, OAuth, and it is what a mobile app or a JavaScript client expects to talk to. SOAP when the other side wants a WSDL they can generate client code from, which in practice means older middleware. Both spend the same daily API request allowance, so the choice is about the caller, not about speed.

mid

The deciding factor is who is calling. SOAP hands them enterprise.wsdl, strongly typed, one element per field, which a Java or .NET team with a code generator genuinely likes. The cost lands later: add a custom field and somebody has to regenerate that WSDL and redeploy the client. partner.wsdl dodges the regeneration by going loosely typed, and then they are looking fields up by name, which is most of what REST already gives them for free. So REST is my default and SOAP is a constraint the other side brings to the table.

Deep dive on this
Mid-level round

Mid-level Integration questions

Integration mid scenario

A 200,000 row Bulk update finished with 60,000 failures, all UNABLE_TO_LOCK_ROW. What is happening?

Testing The difference between reading a lock error as bad luck and reading it as a statement about the data.

What they tell you

A Bulk API 2.0 job updating Opportunity. The file came straight out of a reporting export. The job status is complete; the failed-records file holds 60,000 rows and the error on every one of them is UNABLE_TO_LOCK_ROW.

mid

Sixty thousand of them is the tell. One or two would be timing; a third of the file is structural. When Opportunity rows update, the platform locks the parent Account, so two parallel batches carrying children of the same Account will fight each other. An export sorted by close date or by name scatters those siblings across every batch, which is close to the worst case. So I sort the file by AccountId before loading, which keeps each parent's children inside one batch, and if the skew is bad enough that one Account owns tens of thousands of rows I run serial instead and accept the slower job.

senior

I would rather understand the distribution before choosing a fix, because the two fixes have very different costs. If the rows spread reasonably across parents, sorting by AccountId solves it and costs nothing — the contention was an artefact of the export order. If a handful of Accounts own most of the 200,000 rows then that is ownership skew, and sorting just moves the whole fight inside one batch: serial mode stops the errors and the job time goes up a lot, which is a conversation to have before the maintenance window rather than during it. The longer-term answer for real skew is not a load setting at all, it is not hanging 60,000 children off one parent. And I would fix the process that produced an unsorted file, since whoever runs this next month will export it the same way.

Ask before you answer
  • Is the file sorted by the parent Account, or in whatever order the export produced?
  • Is the job running in parallel or serial mode, and how many rows share a single parent?
Do not say this

Rerun the 60,000 failed rows — lock contention is transient, so most will go through on the second pass.

Most of them will, which is exactly the problem: it looks fixed and the same load fails again next month. At this volume the error is not bad timing, it is two batches updating children of the same parent at the same moment, and rerunning a smaller file just makes the collision less likely.

Deep dive on this
Integration mid

Composite API or Bulk API — how do you decide which one a job needs?

Testing How you think about who is waiting for the answer.

mid

Whether anyone is waiting. Composite is synchronous: up to 25 subrequests in one round trip, optionally all-or-none, and I get the results back before the call returns. That is right for a screen saving a parent and its children together. Bulk is asynchronous: I hand over a file, the platform chunks and processes it, and I poll for a result. That is right for a nightly load of 200,000 rows, where nobody is watching and I care about throughput instead of latency. Using the wrong one shows up as either a timeout or a job that finished three hours after anyone needed it.

senior

Beyond latency there is a limits argument that decides it more often. A Bulk API job is charged very differently from the same records sent one request at a time, so 200,000 single REST calls can exhaust a day's API allowance while the equivalent Bulk job barely registers. Composite earns its keep for a different reason: composite proper shares one transaction across the subrequests, so a parent insert and its child inserts roll back together, and I can reference the parent's new Id in a later subrequest without a second round trip. composite/batch looks similar and does not share the transaction, which is a subtle way to end up with orphaned children. Where I get careful with Bulk is failure handling: a job can finish with a success status and a failure file, so any code that treats job completion as job success will lose rows silently.

Deep dive on this
Integration mid

How do you stop a retried message from creating a second record?

Testing That you expect retries at all. Most designs assume every message arrives exactly once.

mid

An external id field on the object and upsert instead of insert. Database.upsert(records, Order__c.External_Order_Id__c, false) matches on the sender's key, so the same message twice updates one record rather than creating two. The field has to be marked External Id and Unique, or the match is best effort and a duplicate slips through the second time. Where this fails is when the sender has no stable key of its own, and then I ask for one rather than inventing a match on name and date.

senior

Upsert on an external id covers the common case, which is the same business record sent twice. It does not cover the harder one: the same event sent twice, where each delivery is meant to change state. For that I keep a processed-message record keyed on the sender's message id with a unique index, and the first thing the endpoint does is try to insert it — a DUPLICATE_VALUE error is the signal to stop and return success, not to log an error. Returning success on a duplicate matters more than it sounds: a sender that gets a 500 will retry, so a strict endpoint turns one duplicate into an infinite one. And I would rather the key came from the sender than from me, because a key I generate from the payload changes the moment they add a field.

Integration mid

Why use a Named Credential instead of putting the endpoint and token in the callout?

Testing Whether you have rotated a secret in a live org, or only written the happy path.

mid

Three things it buys me. The secret leaves the codebase, so nobody reads it out of a repository or a debug log. The endpoint becomes configuration, so the same class points at a test host in a sandbox and the real one in production with no deployment. And the callout stops building its own authorisation header — I write callout:Partner_API/v1/orders and the platform signs it. A hardcoded bearer token is also a rotation problem disguised as working code: when it expires the fix is a deployment, at whatever hour it expires.

senior

The part worth understanding is that the newer model splits the record in two. The Named Credential holds the URL and how to reach it; an External Credential holds the authentication and its principals, and access to a principal is granted through a permission set rather than to everyone who can run the class. That split is what makes least privilege possible — one integration user's principal, not an org-wide secret. It also decides a design question people skip: Named Principal means every user calls as one identity, which is right for a system sync and wrong for anything where the partner needs to know who acted. And it is worth being clear what a Named Credential does not do. It does not retry, it does not respect a Retry-After, and it does not stop one slow host from eating the 120 second callout budget. Those are still mine to write.

Deep dive on this
Integration mid scenario

A nightly sync created 400 duplicate Accounts and the job log says it succeeded. Where do you start?

Testing Where you look first when the logs and the data disagree.

What they tell you

An ERP posts Accounts over the REST API every night. Last night's run created 400 records that duplicate ones already in the org. The job log shows every call returning 201, no errors, and the same job has run cleanly for months.

mid

Two hundred and one on every call means the writes did exactly what they were asked, so this is not a transport problem, it is a matching problem. So the first thing I check is whether the call is an insert or an upsert on an External Id, and whether that field arrived populated last night. A blank external id on an upsert inserts, quietly and successfully, which fits the evidence precisely. Then I look at what changed on the sender's side, because months of clean runs means the code did not change, the payload did.

senior

The shape of the failure tells me most of it before I open anything. Four hundred duplicates in one run with a clean log means the org accepted every write, so the key is what moved. I would query the 400 and look at the External Id column first — all blank points at the sender dropping a field, all populated but different points at the sender changing its id scheme, which is the worse of the two because the old records are now unreachable and tomorrow night makes 400 more. Fixing the payload restores the sync; it does not merge the 400, and I would not let those be cleaned up by hand without first making the field Unique as well as External Id, so the next blank cannot insert at all. That turns a silent duplicate into a loud failure, which is the trade I want on a key field.

Ask before you answer
  • Is there an External Id field on Account, and did every one of last night's payloads carry a value for it?
  • Did the run overlap with the previous night's, or with a manual data load?
Do not say this

Switch on duplicate rules so Salesforce blocks them at the door.

That converts a silent data problem into 400 failed rows a night, which is visible but no less broken. It also answers none of the question: something changed about the key the sender is sending, and a duplicate rule will still be blocking rows long after that change is forgotten.

Senior round

Senior Integration questions

Integration senior

Which integration patterns do you actually reach for, and how do you choose between them?

Testing Which questions you ask before naming a pattern. The names are the easy part.

mid

I choose on three questions. Is a user waiting for the answer — if yes it has to be synchronous, request and reply, and the latency budget is whatever a person will tolerate. Does the receiving system need to know immediately, or is tomorrow morning fine — that separates an event from a nightly batch. And how much data moves, because 500 records is a callout and 500,000 is a Bulk job. Those three answers pick the pattern for me most of the time.

senior

The pattern names matter less than deciding who owns retry. Request and reply pushes failure back to the caller, which is honest when a person is there to see it and useless for a background job. Fire and forget over platform events moves retry to the subscriber and gives me a 72 hour replay window to recover from, which is generous until someone treats it as a queue and lets a subscriber sit broken for four days. Batch sync is the cheapest thing to operate and the one people abandon too early — plenty of integrations described as real time are read once a day by a human. And Salesforce Connect is the one worth considering more often: if the answer is that the data should not be copied at all, an external object means no sync to break, at the cost of every query depending on someone else's uptime. What I would not do is pick a pattern from the diagram before knowing which system is allowed to be the source of truth, because that decision constrains all of it.

Deep dive on this
Integration senior

What OAuth flow do you use for a server-to-server integration, and why?

Testing Where the answer starts — from what the integration needs, or from whichever flow you set up last.

mid

The JWT bearer flow. There is no user at a keyboard, so anything that involves a login page or a password is the wrong shape. The client posts a grant_type of urn:ietf:params:oauth:grant-type:jwt-bearer carrying an assertion it signed with its private key, Salesforce validates that against the certificate on the connected app, and returns an access token — no password stored anywhere, and rotating the key is a certificate swap rather than a credential hunt. The user still has to be pre-authorised for that connected app, which trips people up the first time because the request looks correct and comes back refused.

senior

I pick it for what it removes rather than what it adds. A password flow means a password living in a config store, an account that cannot have multi-factor authentication on it, and a token I cannot revoke without locking out whatever else shares that account. JWT bearer removes the password entirely and makes the trust a key I control the lifetime of. Two details decide whether it works in practice. The integration user is a real user with a profile, so its permissions are the actual blast radius of the integration — that user should not be a System Administrator because it was quicker. And there is no refresh token in this flow, by design: the client re-signs a new assertion when the access token expires, which means the failure mode is a clock. I have watched this flow break on a server whose time had drifted, because the assertion's validity window is short — the exp claim has to sit inside a few minutes of Salesforce's clock — and both ends have to agree on now.

Deep dive on this
Integration senior scenario

A partner API allows 60 requests a minute and you have 5,000 updates to send. How do you build that?

Testing Pacing as something designed in from the start, or bolted on after the first 429.

What they tell you

An outbound sync pushes Opportunity changes to a partner. Their contract allows 60 requests a minute per tenant and one record per request. The current job loops and calls out, and it fails part way through with HTTP 429.

mid

Five thousand records at 60 a minute is 84 minutes of work, so this cannot live in one transaction under any design — the 100 callout ceiling stops it long before the rate limit does. I would chain Queueable jobs, each one sending a small chunk and enqueueing the next, with the chunk size set so a job stays inside 60 requests and one minute. The record ids to send go in a custom object or a field flag, so a failed job restarts from where it stopped rather than from the beginning.

senior

The first thing I would push back on is one record per request, because every other decision here is downstream of that. If they have a batch endpoint, 5,000 records becomes 50 calls and the problem dissolves. If they genuinely do not, then this is a paced queue and I would build it as one: a work-item record per update with a status, a Queueable that claims a chunk, sends it, marks each item done or failed with the response, and enqueues the next chunk. Honouring Retry-After rather than guessing is what keeps it inside their limit when their window is rolling rather than per minute. I would also make it resumable and observable, because an 84 minute job will be interrupted eventually — a deployment, a maintenance window, a partner outage — and the difference between a good and a bad build here is whether restarting it is a button or an investigation. And I would cap the retries per item, so one poison record cannot keep the chain alive forever.

Ask before you answer
  • Does the partner return a Retry-After header, and is the limit per minute or a rolling window?
  • Is one record per request their only option, or is there a batch endpoint nobody has asked about?
Do not say this

Catch the 429 and retry the same call in a loop until it goes through.

A tight retry loop spends the transaction's 120 seconds of callout budget on calls that are guaranteed to be refused, then fails anyway. It also tends to make the partner's throttle worse, and some of them start counting refused requests against you.

Integration senior

Platform Events or Change Data Capture — how do you decide?

Testing Whether you have owned a subscriber that fell behind, or only published from the developer console.

mid

Change Data Capture publishes automatically when a record changes, for the objects I enable, in a shape I do not control — the changed fields plus a ChangeEventHeader telling me what happened and to which record. Platform Events are my own schema, published deliberately with EventBus.publish. So CDC is right when the subscriber wants to mirror the database and does not care why a change happened. A platform event is right when the thing I want to broadcast is a business fact rather than a row edit — an order was approved, not Status__c changed.

senior

CDC's strength and its problem are the same thing: it fires for every change from every source, including a data load and a background job, so a subscriber built for user edits will get a firehose the first time someone loads a file. It also gives the subscriber the burden of interpretation — six fields changed, work out what that meant — where an event I designed carries the meaning explicitly and stays stable when the field layout moves underneath it. Both ride the same bus with the same 72 hour replay window, so a subscriber that falls behind loses messages either way once it is past that window, and the recovery story has to exist before go-live rather than after. Where I lean towards CDC is when I do not own the writers — if changes arrive from four integrations and the UI, I would rather listen at the database than add a publish call to four codebases and hope nobody forgets one.

Deep dive on this
Integration senior scenario

A record shows Cancelled and then Shipped. The sender insists it sent them in order. How do you fix it?

Testing That HTTP offers no ordering guarantee — accepted, or treated as a bug in the writes.

What they tell you

An external system posts status updates to an Apex REST endpoint, which writes the status onto the record. One record ended up back on Shipped after a Cancelled. The sender's own log shows Cancelled sent first, 40 milliseconds before Shipped.

mid

Nothing about HTTP guarantees that two requests sent 40 milliseconds apart arrive in that order, especially with more than one worker posting. So the write is doing what it was told; the endpoint just has no way to know a message is stale. I would ask the sender for a sequence number or their own event timestamp in the payload, store the last one seen on the record, and have the endpoint ignore anything not newer. That makes a late Shipped a no-op instead of a regression.

senior

I would treat this as a design gap rather than a defect, because the endpoint was written for a guarantee that was never on offer. The fix has two halves. First, ordering data in the payload — a monotonic sequence per record is better than a timestamp, since two systems' clocks will disagree eventually and a sequence cannot. Store it in a field like Last_Event_Sequence__c and discard anything less than or equal to it. Second, the update needs to be a single atomic compare-and-set rather than a read then a write, or two messages arriving together both read the old sequence and both apply. Doing that in one update with the sequence in the filter, or with a FOR UPDATE lock, is what makes it correct under concurrency rather than merely usually right. Where I would push back is on treating every status as ordered: if the real rule is that Cancelled is terminal, then say so in the model and refuse any transition out of it, which is simpler than sequencing and closer to what the business actually means.

Ask before you answer
  • Does the payload carry a timestamp or sequence number from the sender, or only the new status?
  • Is one worker posting these, or several in parallel?
Do not say this

Stamp a Received_At__c with System.now() in the endpoint and order on that.

That records when your org got the message, not when the event happened. Two requests sent 40 milliseconds apart can still arrive in either order, and if they land in the same millisecond the tiebreak is arbitrary. Ordering has to come from the sender or it does not exist.