Articles
Combine Interview Questions: Publishers, Backpressure, and Cancellation
Prepare for Combine interviews with publisher demand, subscription lifetimes, cancellation, scheduler choices, search pipelines, and practical test cases.
Combine interview questions test how you manage a stream over time: who produces values, how much work a subscriber requests, which subscription stays alive, and what happens when a screen disappears. Knowing map and filter is useful, but it is not enough to explain cancellation, stale search results, or a retained view model.
A publisher describes a source of values and completion. A subscriber receives a subscription and requests demand. The subscription connects them and supports cancellation. Operators build a pipeline between these roles.
This guide uses a search screen and a small demand trace as practice examples. The aim is to explain observable behavior and ownership, not recite a list of operators.
What happens when a subscriber connects?
A subscriber first receives a subscription. It can request values through demand, receive elements, and eventually receive completion or cancel its participation. Completion is either normal finishing or failure, with the publisher's declared failure type defining the error channel.
Demand represents how many additional elements the subscriber is prepared to receive. A subscriber requesting two elements is not asking the publisher to deliver unlimited values and hoping it keeps up. Additional demand can be requested later, including through the return value of the subscriber's receive method.
Do not infer that every publisher buffers every value while demand is zero. Buffering, dropping, and production behavior depend on the concrete publisher and operators. In an interview, identify the source: a sequence, timer, subject, network task, or custom publisher can have different practical behavior.
The ordinary sink convenience subscriber requests unlimited demand. That is convenient for many UI pipelines, but it does not demonstrate a custom backpressure policy. If the question explicitly asks about bounded demand, show the subscription and request calls rather than only a sink closure.
Work through a demand example
Assume a well-behaved custom publisher has values A, B, and C available. The subscriber requests .max(2). It may receive A and B, but a third element requires additional demand. If receiving A returns .max(1), that adds demand rather than replacing the outstanding allowance with a new total of one.
Write down a running balance: initial demand two, minus one for A, plus one returned by A's receive callback, minus one for B. Under this simple accounting, demand remains for C. The exercise tests additive demand and prevents the common mistake of treating each request as an absolute cap.
Now cancel after B. The consumer no longer wants values from that subscription. Whether a remote operation has already happened is a separate matter. Cancelling local observation cannot automatically undo a request that a server already accepted.
For a custom implementation, test reentrancy and synchronization as well as arithmetic. A subscriber can request more while receiving an element. Holding an inappropriate lock across callbacks or assuming callbacks cannot trigger more work can produce deadlocks or broken accounting.
How do subscribe(on:) and receive(on:) differ?
The distinction is about where parts of subscription work and downstream delivery are scheduled. subscribe(on:) affects upstream subscription, request, and cancellation scheduling. receive(on:) affects delivery of downstream elements and completion on the selected scheduler.
For a UI pipeline, you may perform upstream work away from the main queue and arrange downstream state updates on the main queue. The presence of one scheduling operator does not prove every operation in the chain runs on the same scheduler.
| Question | What to inspect |
|---|---|
| Where does subscription-related work run? | Upstream scheduling and subscribe(on:) placement |
| Where does this downstream closure receive values? | The nearest relevant receive(on:) boundary |
| Is expensive decoding blocking UI work? | Where decoding sits relative to scheduler changes |
| Is actor isolation satisfied? | The Swift concurrency contract as well as runtime scheduling |
Do not describe queue scheduling as a complete substitute for Swift actor isolation. They are related concerns, but compiler-checked isolation and runtime dispatch are not identical guarantees. Match the code to the project's Swift concurrency mode and APIs.
How do you prevent stale search results?
Suppose a user types ca, then cat. The request for cat finishes first. If the older ca response arrives afterward and overwrites the screen, the UI displays results for the wrong query.
A common Combine shape is to turn each query into an inner request publisher and use switchToLatest so the most recently emitted inner publisher supplies downstream values. The operator cancels the previous inner subscription when a new one replaces it.
queryPublisher
.removeDuplicates()
.debounce(for: .milliseconds(300), scheduler: RunLoop.main)
.map { query in api.searchPublisher(query) }
.switchToLatest()
.receive(on: RunLoop.main)
.sink(receiveCompletion: { completion in
// Handle completion according to the screen's policy.
}, receiveValue: { results in
// Update the current search results.
})
.store(in: &cancellables)
This is a pipeline sketch: the exact failure types and API implementation must line up. The debounce interval is illustrative, not a universal usability rule. Decide how empty queries, duplicate normalized text, loading state, and cancellation should behave.
Why does catch placement matter?
A search request can fail while the query stream should remain usable. If failure terminates the whole outer pipeline, later typing may stop producing results. Recovering inside each inner request publisher can keep the outer query stream alive, depending on the chosen recovery publisher and failure types.
For example, map a failed request to a screen-state value such as failed(query, error) rather than always replacing it with an empty result array. An empty list can mean a successful search with no matches; presenting an outage as “no results” loses useful information.
Ask what should retry and why. Retrying a transient read may be reasonable. Retrying an operation with external side effects requires idempotency reasoning. An operator named retry does not decide whether repeating the business action is safe.
Test a sequence with success, failure, and a later successful query. Confirm that the third event still reaches the UI. This is a stronger test of the error boundary than verifying only that one failure produces a message.
What is the difference between map and flatMap?
map transforms each element into another value. If the transformation returns a publisher, the resulting values are publishers. flatMap subscribes to publishers produced from upstream values and merges their output according to its semantics and configured concurrency.
That difference matters for search. Merging several active request publishers can deliver results from old and new queries. If the desired policy is “latest query wins,” explain why switching to the latest inner subscription fits better than simply merging all responses.
For a batch of independent image downloads, merging may be exactly what you want. Bound concurrency according to memory, network, and server constraints. Preserve an identifier with each result so completion order does not accidentally become display order.
Do not select an operator solely because its name sounds more asynchronous. Write the expected relationship among inputs, active work, and outputs. Then choose the operator that implements that relationship.
Why must you retain AnyCancellable?
The cancellation token represents ownership of the subscription's lifetime. AnyCancellable cancels on deinitialization. If you discard the returned token immediately, asynchronous work can stop before the value you expected arrives.
Storing tokens in a set is a common pattern. The next question is when that owner should release or cancel them. A view model can own work for its useful lifetime, while a short-lived screen interaction may need a narrower cancellation boundary.
Be alert to a retain cycle: an owner stores a cancellable, the subscription retains a closure, and the closure strongly captures the owner. Use capture semantics that fit the desired lifetime, and verify deallocation. Automatically writing [weak self] everywhere without considering whether work should finish can also obscure intent.
Does cancellation guarantee the operation never happened?
No. Cancellation expresses that the subscription should stop its work and delivery according to the implementation. It does not retroactively erase an effect already completed by another system.
For a search read, cancelling old network work primarily saves resources and prevents obsolete results from driving state. For a purchase request, the server may have committed the order even if the local subscription is cancelled or the response is never observed. Recovery needs an operation identifier and a way to query the outcome.
This distinction is useful in interviews because it connects reactive lifetimes to real application behavior. A cancelled spinner and a cancelled business transaction are different claims. Define the state transition the UI should show while an uncertain outcome is reconciled.
When testing cancellation, observe both the subscriber and the underlying fake operation. Verify that no unwanted downstream update occurs and that the resource receives cancellation where supported. Avoid claiming that one proves the other in every implementation.
How do you test a Combine pipeline reliably?
Control time and inputs. A debounce test that sleeps for an arbitrary wall-clock interval can be slow and flaky. Use a testable scheduler abstraction where appropriate, or a controlled publisher and explicit expectations for the behavior under test.
Record the events you care about: subscription, demand, values, completion, and cancellation. A fake search API can keep separate controllable publishers for ca and cat. Complete the newer one first, then try to complete the older one, and assert that the screen still represents the latest query.
Test lifetime separately from value transformation. Create the owner, subscribe, release the owner, and verify that expected resources are released. Then test an intentional long-lived owner to make sure cancellation is not occurring too early.
| Exercise | Evidence to collect |
|---|---|
| Bounded demand | Number and order of requested and delivered elements |
| Latest search wins | Only current query results change visible state |
| Error recovery | A later query still succeeds after an earlier failure |
| Cancellation | Underlying fake receives cancellation and stale UI updates stop |
| Memory ownership | Owner and subscription deallocate at the intended boundary |
How should you explain Combine in an interview?
Describe a concrete stream from source to owner. For the search example: query changes produce request publishers; switching selects the latest request; recovery is scoped to each request; UI delivery occurs on the appropriate scheduler; the view model retains and eventually cancels the subscription.
Then name the edge case you would test first. Out-of-order responses, failure followed by new input, and owner deallocation are more informative than a happy-path map demonstration alone. You show that the pipeline behaves correctly over time, which is the reason reactive tools are useful.
How should you test a cancellation requirement?
Write the expected timeline before choosing operators. Start request A, start request B, complete B, and then make A attempt to deliver a late result. The screen should still show B. Separately check that cancelling the subscription reaches the underlying operation where the publisher supports cancellation. A correct visible result does not prove that old network work stopped. Use a controllable test publisher or fake client so response order is deliberate, not dependent on network timing. Include the empty-query case and a failure followed by a new successful query.
Sources and Further Reading
- Apple: processing published elements with subscribers
- Apple: receiving and handling events with Combine
- Apple: Subscription
- Apple: AnyCancellable
- Apple: switchToLatest
Research checked September 24, 2026. Code is a pipeline sketch; align publisher failure types, scheduler choices, and actor isolation with the project's Swift version.