Resource Reconciler: Plan CREATE Changes, Then Create Resources and Save Bindings
Company: Perplexity
Role: Software Engineer
Category: Software Engineering Fundamentals
Difficulty: hard
Interview Round: Technical Screen
Implement the core of a resource reconciler: a component that compares the resources you want with the resources that actually exist, works out the changes needed, and then carries them out against a remote provider. The exercise is long and was given in six parts; the report describes the first two, which are reproduced here.
Three inputs describe the world:
- `desired` maps each logical address (a name such as `"search_api"`) to a `ResourceSpec`, the configuration you want that resource to have.
- `state` maps a logical address to a `ResourceBinding`: the `resource_id` of the remote resource the address was bound to, and the `attributes` recorded at the time.
- `client` talks to the remote provider. `client.read(resource_id)` returns the actual `RemoteResource`, or `None` if it does not exist.
A `Change` describes one action to take: an address, a kind, a resource id and attributes. Its `kind` is an enum with members such as `CREATE`, `UPDATE` and `DELETE`.
```python
from dataclasses import dataclass
from enum import Enum
from typing import Any, Protocol
Attributes = dict[str, Any]
@dataclass
class ResourceSpec:
attributes: Attributes
@dataclass
class ResourceBinding:
resource_id: str
attributes: Attributes
@dataclass
class RemoteResource:
resource_id: str
attributes: Attributes
class ChangeKind(Enum):
CREATE = "create"
UPDATE = "update"
DELETE = "delete"
@dataclass
class Change:
address: str
kind: ChangeKind
resource_id: str | None
attributes: Attributes
Desired = dict[str, ResourceSpec] # logical address -> desired configuration
State = dict[str, ResourceBinding] # logical address -> recorded binding
class ResourceClient(Protocol):
def read(self, resource_id: str) -> RemoteResource | None: ...
def create(self, attributes: Attributes) -> RemoteResource: ...
class StateStore(Protocol):
def put(self, address: str, binding: ResourceBinding) -> None: ...
```
The source names `RemoteResource` without listing its fields; assume it carries the `resource_id` and `attributes` shown. `Change` and `ChangeKind` may have further fields and members, but only the ones shown matter for these two parts.
### Clarifying Questions
- Are `client` calls slow network calls, and is the code expected to run single-threaded?
- Should `desired`, `state` and their attribute dictionaries be treated as read-only?
- Can `client.read` raise for reasons other than "not found", such as a timeout, and how should that differ from a `None` result?
### Part 1 — Plan the CREATE changes
Write `plan(desired: Desired, state: State, client: ResourceClient) -> list[Change]`, which returns the changes needed, sorted by address. In this part you may assume that the only change kind needed is `CREATE`; you do not need to handle `UPDATE`.
- If `state` has no binding for a desired address, emit a `CREATE` change for that address with the desired attributes, without calling the client.
- If `state` has a binding, call `client.read` with its `resource_id`. If the remote resource no longer exists, emit a `CREATE` change for the address with the desired attributes.
- If the bound resource still exists, emit nothing for that address in this part; an attribute difference would be an `UPDATE`, which is out of scope here.
Assume a `CREATE` change carries `resource_id=None`, since the new resource has no id until it is created.
```python
desired: Desired = {
"search_api": ResourceSpec(attributes={"replicas": 3}),
}
state: State = {
"search_api": ResourceBinding(resource_id="r-123", attributes={"replicas": 1}),
}
# client.read("r-123") returns None: the bound resource no longer exists.
plan(desired, state, client)
# [Change(address="search_api", kind=ChangeKind.CREATE,
# resource_id=None, attributes={"replicas": 3})]
```
```hint When to call the client
For each desired address, decide whether the recorded state alone already settles the question, and when only the remote provider can.
```
#### Clarifying Questions for this Part
- Should a `CREATE` for an address with a stale binding carry `None` as its `resource_id`, or the old id for reference?
- Should addresses that appear in `state` but not in `desired` be ignored in this part?
#### What This Part Should Cover
- Correct handling of the three cases: no binding, a binding whose resource is gone, and a binding whose resource still exists
- Calling the client only when there is a binding to check, and how errors from `read` are treated
- Output deterministically ordered by address, without mutating the inputs
### Part 2 — Execute the creations and save the bindings
Write `apply(changes: list[Change], client: ResourceClient, store: StateStore) -> None`, which executes a plan produced by Part 1. For each `CREATE` change:
1. Create the remote resource with `client.create(change.attributes)` and take the `RemoteResource` it returns.
2. Save a `ResourceBinding` from the change's address to the new resource with `store.put(address, binding)`; `put` inserts a new binding or replaces an existing one.
A change counts as complete only when its remote resource exists and its binding has been saved. When a client call raises an exception, the exception must propagate to the caller of `apply`.
```hint What "complete" means
List what has changed in the outside world after `create` returns but before `put` returns, and ask what the next run of `plan` would see if execution stopped in between.
```
```hint Failing loudly
Consider what the caller needs to know, and what must already be true of the earlier changes, when the third change's `create` raises.
```
#### Clarifying Questions for this Part
- If `store.put` raises after `client.create` succeeded, what should happen to the newly created resource?
- Should `apply` stop at the first failure, or attempt the remaining changes and raise afterwards?
- Should the saved binding record the attributes returned by `create` or the attributes in the change?
#### What This Part Should Cover
- The order of operations within one change, and why
- What is true of every change, completed or not, after each possible failure point
- Exception propagation that neither swallows nor hides the original error
- The window between `create` and `put`, and how a rerun behaves after a failure there
### What a Strong Answer Covers
- A clean split between planning, which only reads, and applying, which has side effects
- Deterministic, address-ordered plans and execution
- Explicit reasoning about every failure point and the state it leaves behind
- Tests with a fake client and a fake store, including injected failures
- Code that leaves room for `UPDATE` and `DELETE` without a rewrite
### Follow-up Questions
- Extend `plan` to emit an `UPDATE` when the bound resource exists but its attributes differ from the desired ones. What do you compare against: the recorded binding or the remote resource?
- Addresses that are in `state` but no longer in `desired` should be deleted. How do you order `DELETE` changes relative to the others?
- The process crashed after `create` returned but before `put`. How would you make the next run adopt that resource instead of creating a duplicate?
- Two reconcilers run against the same state store at the same time. What goes wrong, and how would you prevent it?
Overview: Implement the first two parts of a resource reconciler in Python: plan CREATE changes sorted by address by comparing desired resource specs with recorded bindings and the remote provider, then create each resource and save its binding to a state store. Tests careful client calls, deterministic output, exception propagation and partial-failure reasoning.
Read the full Perplexity Software Engineer interview experience this question came from