Resource Reconciler: Plan CREATE Changes, Then Create Resources and Save Bindings

Read the full interview experience this question came from →

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

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

Perplexity logo
Perplexity
Nov 2, 2026
hardSoftware EngineerTechnical ScreenSoftware Engineering Fundamentals
0
0

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.

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 Guidance

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

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})]

Clarifying Questions for this Part Guidance

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

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

Clarifying Questions for this Part Guidance

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

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

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

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