projects~/ deep-dive/po-allocation
~/ cat projects/po-allocation/DEEP_DIVE.md

The purchase-order allocation platform, taken apart.

An end-to-end distributed pipeline that turns a client's raw purchase-order file into store-level allocations and delivers them back — under a hard 15-minute SLA. The interesting part is not the allocation maths. It is that nothing in the chain promises exactly-once execution, so every boundary had to be made idempotent, leased, and replayable.

1–2M allocation records / PO15 min hard SLA100 concurrent POs load-tested150–200M row-level computations90s → <4s persistence

00The one-pager

Everything below in a single screen. If a conversation only has five minutes, this is the whole project.

problem

A retail client pushes purchase-order files to GCS. Each PO must be allocated across hundreds of stores respecting demand, store capacity, pack structures, allocation multiples, PO type and allocation method — automatically, and inside a 15-minute SLA.

why it is hard
  • A few hundred PO rows explode into ~180K store-level rows; several POs share a worker.
  • 15–18 distinct processing paths (3 PO types × 5–6 allocation methods).
  • Five independent systems in one flow, none of them exactly-once.
  • Persistence, not computation, was the SLA bottleneck.
what I owned

The workflow end to end: orchestration, batch/state model, idempotency and leases, preprocessing pipeline, integration contract with the existing optimisation service, post-processing persistence, finalisation and recovery, plus capacity validation.

design principle

Assume at-least-once everywhere. Make correctness come from deterministic business identifiers, durable Postgres state, expiring leases and replayable outputs — not from hoping a message is delivered once.

key decisions
  • Caller-generated batch_id, so Init retries are safe.
  • allocation_code as the idempotency key, not the task ID.
  • Postgres for locks, because the protected state already lives there.
  • Polars over Pandas for concurrent, transformation-heavy work.
  • DuckDB → COPY → temp table → set-based insert.
  • Outbox + deterministic GCS paths for final delivery.
outcome

Validated POs process automatically with no manual intervention, retries and crashes became routine rather than incidents, and result persistence stopped being the thing that ate the SLA.

>20×faster result persistence
1–2Mrecords per PO
100concurrent POs sustained
150–200Mrow-level computations
15 minSLA, end to end
9failure modes designed for

01The two-minute answer

Said out loud, in this order. Problem → ownership → hardest part → performance → outcome.

spoken version

“The client pushed PO files to our GCS bucket. Each file held multiple purchase orders, and each PO could carry hundreds of product- and pack-level rows. The system had to allocate that inventory across hundreds of stores while respecting PO type, allocation method, pack constraints, allocation multiples, demand and store-level capacity — automatically, inside a 15-minute SLA.”

“My ownership was the end-to-end allocation workflow. The ingestion team landed and validated PO data. From there my team built the automatic processing flow: Google Workflows per PO type, an Init API for batch assignment, a Process API for bounded picking, Cloud Tasks for per-PO preprocessing, Postgres-based idempotency, Polars transformations, integration with the existing Gurobi service, post-processing persistence, and final output generation.”

“The hardest part was correctness under retries and partial failures. Batch IDs generated inside the API could strand POs when a response was lost, so we moved generation to the Workflow and made Init idempotent. The Cloud Task plus DB-update window we handled by accepting at-least-once dispatch and making Preprocessing idempotent on allocation_code. For final delivery we used an outbox plus deterministic upload paths so GCS writes could be retried safely.”

“On performance, preprocessing used Polars because PO rows expand to store level and several POs share a worker. In post-processing we took result persistence from roughly 90 seconds to under four, using DuckDB for cleansing, COPY into a Postgres temp table, and one set-based insert into the main table. Net: end-to-end ownership, distributed failure handling, and a pipeline that meets its SLA.”

02Scope & ownership

The single most common way this answer goes wrong is claiming the whole stack. Be precise about the boundary — it reads as seniority, not modesty.

AreaMineSomeone else's
File ingestion & PO validationConsumed the contractIngestion team — implementation
Orchestration (Workflows, Init, Process)Design + implementation—
State model, idempotency, leasesDesign + implementation—
Preprocessing & Polars pipelineDesign + implementation—
Optimisation solver internalsInput/output contract, retries, failure handlingExisting shared Gurobi service
Post-processing & persistenceDesign + the rewrite—
Finalisation & deliveryDesign + implementation—
Capacity & load validationOwned—
say it like this

“My ownership was not one API. I owned the allocation workflow end to end — orchestration, state transitions, idempotency, preprocessing, the integration with the existing optimisation service, post-processing persistence, finalisation, failure recovery and capacity validation. I did not build the solver, and I did not build ingestion.”

Facts to have ready

duration

Exact months from kickoff to production, split into design / build / hardening / go-live.

team composition

Backend engineers on the allocation workflow, ingestion engineers, optimisation-service contacts, PM, EM, QA. Name who reported into the work and who was a partner team.

your own code

Name the modules and APIs you wrote or rewrote personally — Init/Process handlers, the idempotency layer, the Polars strategy framework, the persistence path. Interviewers probe this when the story sounds architectural.

03How it was broken down

The work was sequenced so that each milestone produced something observable, and so that the risky parts — the distributed boundaries — were hardened before scale testing rather than after.

M1 — Contract & state model

design

Agree the ingestion handoff, define po_master statuses, and pick allocation_code as the identifier that ties PO, receipt and batch together for tracing and idempotency downstream.

M2 — Happy path, one PO

build

Workflow → Init → Process → Cloud Task → Preprocessing → solver → Post-processing → Finalize, working end to end for a single PO with no concurrency and no retry handling.

M3 — Business coverage

build

The 15–18 combinations of PO type and allocation method, behind a pluggable strategy interface so a new method is a new class rather than a new branch in a growing conditional.

M4 — Correctness hardening

the real work

Caller-generated batch IDs, the Postgres idempotency table with owner and lease, atomic takeover, the outbox for finalisation, and deterministic output paths. Each one came from writing down a specific crash window and closing it.

M5 — Performance & capacity

measure, then cut

Profile the stages, find persistence eating most of the budget, rewrite it around DuckDB and COPY, then load-test at 100 concurrent POs to confirm the SLA holds with headroom.

M6 — Go-live & observability

operate

Batch IDs, allocation codes, retry counts, statuses and attempts all traceable, so a stuck PO is a query rather than an investigation.

04The business problem

The client pushed purchase-order inventory data to a GCS location. The system had to convert that raw PO data into store-level allocations and write final output files back to GCS. Allocation is hard because it is not an even split: it has to respect product-level inventory, store-level demand, pack structures, allocation multiples, PO type and allocation strategy simultaneously.

The shape of the data is what drives the architecture. A PO arrives with a few hundred product-level rows. Once those rows are exploded across eligible stores, the working set grows by three orders of magnitude.

300 PO rows × 600 stores      = 180,000 store-level candidate rows

10–12 POs active on one worker:
180,000 × 10                  = ~1.8M rows of intermediate processing

100 concurrent POs system-wide = 150–200M row-level computations

And the requirement was never “process the file eventually”. It was to pick up validated POs automatically and have allocation results available inside the operating SLA.

05Input model

Each client file held at most ~1,000 rows, spread across multiple purchase orders. A logical PO was identified by po_id + receipt_id, and one PO could carry 100+ rows with different product and pack attributes.

AttributeMeaning in the workflow
po_id + receipt_idLogical identity of a PO/receipt unit.
po_typeReceipt, Split, or Prior Distro — three separate processing flows.
allocation method5–6 methods, producing 15–18 combinations with PO type.
product_codeProduct/SKU-level input used to build store-level allocation rows.
pack_id, pack size, master pack sizeInputs for pack-aware distribution.
allocation multipleAllocated quantity must land on multiples of a business-defined value.
business columnsAdditional columns driving the 15–18 processing paths.

06Requirements

Functional

  • Ingestion. Client pushes raw PO files to GCS.
  • Validation. PO-level validation before allocation processing begins.
  • Automatic picking. Validated POs are picked up with no manual intervention.
  • PO type separation. Distinct handling for Receipt, Split and Prior Distro.
  • Chunked dispatch. Bounded work per call — 20 POs per Process invocation.
  • Preprocessing. Explode to store level, then transform, validate, filter, aggregate.
  • Optimisation. Call the existing Gurobi service for constrained allocation paths.
  • Post-processing. Consume solver output, transform, persist.
  • Delivery. Generate the final output and upload to GCS.
  • Recovery. Survive retries, crashes, duplicate tasks and lost responses.

Non-functional, and what each one forced

RequirementDesign consequence
15-minute SLAEvery stage bounded and measurable; the persistence bottleneck had to go.
At-least-once deliveryCloud Tasks and Workflow retries mean every API must be idempotent.
ScalabilityMultiple POs per worker — memory and CPU had to scale past a single PO.
ReliabilityEvery gap between a DB write, a task creation and a GCS upload is a crash window.
ObservabilityBatch IDs, allocation codes, retry counts, statuses and attempts all traceable.
Separation of concernsClear ownership lines between ingestion, allocation, optimisation and delivery.

07High-level system design

GCS is the integration point for client files and solver input/output. Google Workflows orchestrates picking, one workflow per PO type. Cloud Tasks gives retryable async execution per PO. PostgreSQL holds durable lifecycle and idempotency state. Polars does the transformation work, DuckDB and COPY do the persistence, and the shared Gurobi service handles the constrained allocation paths.

GCS — client PO fileingested + validated → po_masterGoogle Workflows × 3Receipt · Split · Prior Distro — generates batch_idInit → Process APIassign batch · pick 20 POs · allocation_codeCloud Tasks — per-PO fan-outat-least-once · isolated retry queuesPreprocessing API — Polarslease on allocation_code · explode to store levelGurobi service (existing)MILP · packs, capacity, multiplesPost-process → Finalize → GCSDuckDB COPY · outbox · deterministic pathPostgreSQLpo_masteridempotency+ owner · leaseallocation_resultoutbox eventsthe onlysource of truthfor correctnessnothing here isexactly-once —every arrow is acrash window
the framing that matters

“The system deliberately avoids assuming exactly-once execution across Workflow, Cloud Tasks, Postgres, GCS and the solver. Every distributed boundary is made safe with deterministic identifiers, durable state transitions and replayable outputs instead.”

08Lifecycle & state

Retries are only unambiguous if the database can answer “has this already happened?”. The key objects are po_master, the preprocessing idempotency table, the allocation result tables, and the finalize outbox.

po_masterpo_id + receipt_idpo_typeallocation_methodbatch_idallocation_codeprocessed_statuspreprocessing_idempotencyallocation_code PKstatusowner_idlease_expires_atcreated_atupdated_atallocation_result_mainallocation_codestore_idproduct_codeallocated_qtypack_idcreated_atfinalize_outboxevent_id PKallocation_codeevent_typestatusowner_idlease_expires_atattempt_countallocation_codeallocation_codeupload intentthe business recordevery stage keys off
One identifier threads every table — which is what makes a retry answerable with a query.
StatusMeaning
-120Validation failed at PO/receipt level — must never be picked for allocation.
-100Validated, waiting to be picked.
0Picked / processing started (stage-specific semantics).
IN_PROGRESSA worker currently owns this logical allocation operation.
COMPLETEDDurably done. A retry returns success without reprocessing.
FAILEDTerminal or retry-analysable, depending on business semantics.

allocation_code is the central business identifier. It ties PO, receipt and batch together, and it is the same value used for idempotency, tracing, solver input, solver output, post-processing and finalisation. One identifier for one logical operation, all the way through.

09Stage walkthrough

Workflows and the Init API

Three Google Workflows, one per PO type. Each generates a batch_id, calls Init to assign that batch to eligible POs, then calls Process repeatedly until the batch is exhausted.

the original bug

batch_id was generated inside Init. If Init tagged rows and then crashed before responding, Workflow retried, generated a fresh batch ID, and the already-tagged POs were left behind under the old one — stranded, and breaching the SLA.

The fix was to move identity generation to the caller, so a retry is the same logical operation.

before — identity created inside the side effectafter — identity created by the callerInit API generates batch_id = 123UPDATE po_master SET batch_id = 123rows are now taggedcrash before the response reaches WorkflowWorkflow retries Init → new batch_id = 456POs tagged 123 are never picked upstranded · SLA breachWorkflow generates batch_id = 123call Init(123)UPDATE … SET batch_id = 123crash before the response reaches WorkflowWorkflow retries Init(123)same identitysame rows, same outcome — idempotentnothing strandedgenerate the key before the side effect, not after it
The same crash, two outcomes. The only difference is who generated the identifier.
Workflow:
  batch_id = current_timestamp_bigint
  call Init(batch_id)

Init API:
  UPDATE po_master
     SET batch_id = :batch_id
   WHERE processed_status = -100
     AND batch_id = -1
     AND po_type = :po_type;

-- Init(batch_id) is now safe to retry: the identity is stable.
the lesson, generalised

“The idempotency key has to exist before the side effect begins. If the server generates the identity after it starts mutating state, a lost response leaves the caller with no stable way to retry.”

Process API and Cloud Task dispatch

Process fetches the next set of unique POs for the batch — 20 at a time — assigns each an allocation_code, creates a Cloud Task per PO, updates status, and returns progress so the Workflow knows whether to loop again.

110 unique POs in one batch:

  call 1 → 20 POs → all_processed = false
  call 2 → 20 POs → all_processed = false
  ...
  call 6 → 10 POs → all_processed = true

Why 20. Bounded work per call keeps each HTTP request inside its timeout, controls the rate of Cloud Task creation, keeps progress observable, and lets the Workflow drive the loop. Dispatching an arbitrarily large batch in one call is the version of this that pages someone at 2am.

dual-write window

Process must create a Cloud Task and update Postgres. Those cannot commit atomically — Cloud Tasks is an external system. If the task is created and the DB write fails, a Workflow retry picks the same PO and creates a second task. That is not a bug to eliminate; it is a property to absorb downstream.

Process API must write to two systems that cannot commit togetherProcess picks a POassigns allocation_codeCloud Task createdsucceededDB status update failscrash / networkWorkflow retry picksthe same PO againa second Cloud Task for the same POsame allocation_code → one owner, one runcorrectness lives downstream, in the idempotency key
Two systems, one HTTP call, no shared transaction — so the duplicate is designed for, not prevented.
say it like this

“We accepted that Cloud Task dispatch is at-least-once. Correctness came from business-level idempotency in the Preprocessing API, not from pretending the task would be created exactly once.”

10Locks vs. durable state

A row lock only protects concurrent transactions while the transaction is open. A SELECT … FOR UPDATE with no state change is not enough: after COMMIT the lock is gone and a later request sails through. The useful pattern is lock, validate, and transition state — atomically.

req Areq BBEGIN + FOR UPDATErow lockedUPDATE status-100 → 0COMMITlock releasedFOR UPDATEblocked, waiting…lock acquiredA is goneWHERE status = -100no longer matches → no-optime →the lock expired with the transaction. the durable state did not.
FOR UPDATE bought ordering. The status column is what actually made B a no-op.
MechanismSolvesDoes not solve
FOR UPDATESerialises concurrent transactions on the same row.Does not durably record that the PO was already processed.
NOWAITFails fast instead of blocking on a held lock.Does not skip ahead; the application must handle the error.
SKIP LOCKEDQueue-like consumers skipping locked rows.Unnecessary when the workflow already serialises picking per batch.
Status columnBlocks later requests by recording durable state.Says nothing about external side effects (task creation, GCS upload).
Idempotency keyTies retries and duplicate tasks to one logical operation.Still needs durable completion + lease state to survive crashes.

In this system there was no normal concurrent Workflow execution for the same batch_id. The real problem was never two workers racing — it was retry after partial completion. That is why idempotent downstream processing mattered more than SKIP LOCKED.

11Idempotency & leases

A duplicate request into Preprocessing could be a retry of the same Cloud Task, or a genuinely separate task created for the same PO. Task ID and retry count identify the transport attempt, not the work — so the key had to be the business operation: allocation_code.

Cloud Task → Preprocessing APIallocation_coderow for allocation_code?COMPLETEDreturn 200no work, no duplicateno rowINSERT … ON CONFLICT DO NOTHINGIN_PROGRESS + owner_id + leaseIN_PROGRESSlease expired?noretryable responseno duplicate workyesatomic takeoverUPDATE … WHERE lease_expires_at < NOW()run preprocessingPolars · joins · validationsmark COMPLETEDWHERE owner_id = :meexactly one callerreaches this column
Four arrivals, four different correct answers — and only one of them does the work.
CREATE TABLE preprocessing_idempotency (
    allocation_code   TEXT PRIMARY KEY,
    status            TEXT NOT NULL,   -- IN_PROGRESS / COMPLETED / FAILED
    owner_id          TEXT,
    lease_expires_at  TIMESTAMP,
    created_at        TIMESTAMP NOT NULL DEFAULT NOW(),
    updated_at        TIMESTAMP NOT NULL DEFAULT NOW()
);

Three statements carry the whole protocol — claim, take over, complete.

-- 1. first claim: insert if absent
INSERT INTO preprocessing_idempotency (
    allocation_code, status, owner_id, lease_expires_at
)
VALUES (:allocation_code, 'IN_PROGRESS', :owner_id, NOW() + INTERVAL 'N minutes')
ON CONFLICT DO NOTHING;

-- 2. takeover: only if the previous owner looks dead
UPDATE preprocessing_idempotency
   SET owner_id = :new_owner,
       lease_expires_at = NOW() + INTERVAL 'N minutes',
       updated_at = NOW()
 WHERE allocation_code = :allocation_code
   AND status = 'IN_PROGRESS'
   AND lease_expires_at < NOW();

-- 3. complete: only if this worker still owns the operation
UPDATE preprocessing_idempotency
   SET status = 'COMPLETED',
       updated_at = NOW()
 WHERE allocation_code = :allocation_code
   AND owner_id = :owner_id;

If the row is already COMPLETED, the API returns success immediately. If it is IN_PROGRESS with a live lease, the request does not duplicate work. If the lease has expired, a retry can atomically take ownership.

Why not allocation_code + retry_count

Retry count changes on every attempt. Put it in the key and every retry becomes a different key — which is exactly not having idempotency. Retry count is a metric; it is not part of identity.

Edge cases this closes

Edge caseHandled by
Same Cloud Task retriesSame allocation_code → same row. No new logical operation.
Duplicate Cloud Tasks createdDifferent task IDs, one allocation_code. Only one owner processes.
Worker crashes after claimingLease expires; a later retry takes ownership atomically.
Worker crashes mid-PolarsNo COMPLETED state exists, so the work replays after lease expiry.
Worker succeeds, response lostDB says COMPLETED — the retry gets a 200 and does nothing.
Delayed duplicate arrives lateDurable COMPLETED blocks it even long after any lease is gone.
Two workers attempt takeoverConditional UPDATE — exactly one wins.
Processing outlives the leaseConservative TTL, or heartbeat renewal keyed on owner_id.
Redis unavailableNot a scenario — Postgres is the source of truth.
say it like this

“The idempotency key was the logical allocation operation, not the Cloud Task. Cloud Tasks gave us at-least-once delivery; Postgres idempotency made repeated delivery safe.”

Postgres vs. Redis for the lock

QuestionPostgresRedis
Where does the protected state live?Best when it is already in Postgres.Better for ephemeral or cross-service work not tied to one row.
Is durability required?Strong fit — idempotency, payments, orders, allocation state.Not sufficient alone for correctness-critical completion state.
ThroughputFine at moderate concurrency; can get hot under huge lock volume.Excellent for very high-frequency short-lived locks.
Failure behaviourSurvives restarts, transactional.TTL, eviction and restart semantics need careful handling.
FitsPO processing, payment idempotency, order transitions.Cache stampedes, rate limiting, short-lived coordination.
decision framework

Keep the lock close to the state it protects. Postgres for correctness-critical durable transitions; Redis for high-frequency ephemeral coordination where a duplicate is tolerable or protected elsewhere.

12Finalisation & the outbox

Finalize reads the persisted allocation result, generates the client-facing output, uploads it to GCS, and marks the allocation complete. Same shape of problem as before: the upload can succeed and the process can die before the DB is updated.

one transaction — both or neitherpersist allocation resultinsert outbox FINAL_UPLOADFinalize workerclaims the eventdeterministic path…/{allocation_code}uploadto GCSmark outbox + allocationCOMPLETEDcrash before the markretry: same event, same paththe retry overwrites one object — it does not create a second one
The outbox makes the intent durable. The deterministic path makes the upload safe to repeat.
Post-processing transaction:
  BEGIN;
    persist allocation result;
    insert outbox event FINAL_UPLOAD(allocation_code);
  COMMIT;

Finalize worker:
  claim outbox event;
  generate deterministic file path from allocation_code;
  upload to GCS;
  mark outbox event COMPLETED;
  mark allocation COMPLETED;
the nuance most people miss

A transactional outbox does not make an external side effect exactly-once. It guarantees the intent is durably recorded alongside the state change. The upload itself still has to be idempotent — which is why the destination path is derived deterministically from allocation_code: a retry overwrites the same object rather than creating a second one.

say it like this

“The outbox gave us durable at-least-once finalisation. Deterministic output paths and allocation-code idempotency made repeated finalisation safe.”

13The failure matrix

This table is the project. Every row is a crash window someone wrote down before it happened in production.

StageFailureRiskProtection
InitBatch assigned, response lostNew batch created; old POs strandedWorkflow-generated batch_id as the key
ProcessTask created, DB status not updatedSame PO picked againPreprocessing idempotency on allocation_code
PreprocessingTask retries after timeoutDuplicate computationIdempotency row + status + owner + lease
PreprocessingCrash after claimingPO stuck in progressLease expiry + atomic takeover
PreprocessingCompletes, response lostTask retriesCOMPLETED returns 200, no reprocessing
PreprocessingWork outlives the leaseSecond worker startsLease renewal / conservative TTL
GurobiOutput written, callback failsPost-processing never startsRetry via allocation_code + GCS path
Post-processingPersistence too slowSLA breachDuckDB → COPY → temp → main
FinalizeUpload succeeds, DB write failsOutput uploaded twiceOutbox + deterministic object path
Final outputDelayed duplicate task arrivesReprocess / re-uploadDurable COMPLETED + same object path

14Preprocessing & why Polars

Once ownership was acquired, Preprocessing fetched the PO data, expanded it to store level, joined store and demand data, applied filters and validations, computed derived columns, and prepared solver input.

One PO goes from hundreds of product rows to hundreds of thousands of store-level rows. With several POs on a worker, that is around two million rows of intermediate data at once — and the work is transformation-heavy: filters, joins, group-bys, validations, ranking and business rules across 15–18 combinations of PO type and allocation method.

Polars fits that shape: a Rust columnar engine with vectorised operations, parallel execution and memory-efficient representation. Python describes the computation; the heavy lifting happens in native code.

ApproachStrengthWhy not here
Plain PythonFlexible, simple for small logicInterpreter loops, hand-rolled joins and grouping, unmaintainable at this complexity
PandasMature, fine for medium datasetsWeaker under concurrent heavy transformation with memory pressure and an SLA
SQL onlyExcellent for relational work in the DBDynamic multi-strategy business logic is awkward to express and test
SparkGood for large distributed batchOperationally far too heavy for a per-PO, low-latency request path
PolarsRust engine, columnar, parallel, lazy optimisationNewer ecosystem — needed some team ramp-up
do not overclaim

Pandas can handle 180K rows. The honest reason for Polars is the combination: multiple concurrent POs, large intermediate sets, transformation-heavy pipelines, memory limits and SLA pressure.

15Gurobi & the optimisation

Some allocation strategies are optimisation problems, not calculations. The system has to choose the best distribution among enormously many valid ones while satisfying inventory, capacity, pack, multiple, demand and business constraints simultaneously.

Decision variable
  x[p, s, store] = units of product p / size s allocated to a store

Inventory constraint
  Σ over stores  x[p, s, store]  ≤  available_inventory[p, s]

Store capacity constraint
  Σ over products x[p, s, store] ≤  store_capacity[store]

Objective
  maximise Σ demand_score[p, s, store] × x[p, s, store]

In practice many variables must be integers — you cannot ship 3.7 units — and pack constraints plus allocation multiples push the problem from pure LP toward mixed-integer linear programming. Gurobi handles both, using presolve, simplex/barrier for the LP relaxation and branch-and-cut for the integer part.

Preprocessingstore-level inputwrite solver input→ GCScall solver APIallocation_code+ config + GCS pathGurobi serviceMILP · existingsolver output → GCScallback → post-processsomeone else's serviceI did not write the solvermy ownership: correct store-level input, reliable invocation,output handling, and retry keyed on allocation_code
The dashed box is the ownership boundary. Everything either side of it is mine.

Why not greedy

Greedy optimises the next local decision, which is fine for the simpler methods. But once packs, multiples, inventory limits, demand and capacity interact, filling the highest-demand store first can break a pack constraint or foreclose a better global distribution. A solver is the difference between a locally sensible answer and a globally good one.

the ownership line — say this unprompted

“We used an existing Gurobi-based optimisation service for the constrained paths. I did not build the solver internals. My ownership was the system around it: preparing correct store-level input, calling it reliably, consuming output, persistence, and making the whole pipeline meet its SLA.”

1690 seconds → under 4

When the solver finished, it wrote output to GCS and called Post-processing, which read the output, transformed it into the application's allocation-result format, validated and coerced it, and persisted it. For a single PO that could be 200K rows or more — and the naive path was eating most of the SLA budget on its own.

old — batched INSERT loopnew — DuckDB → COPY → temp → mainPython rows / dataframeINSERT 1,000 rowsnetwork round trip · parse · plan · WALrepeat ~200× for 200K rows×200DuckDB — cleanse + coerceschema-aware, onceCOPY → Postgres temp tableset-based validation in SQLINSERT INTO main SELECT … FROM temp~90 secondsunder 4 seconds>20× — persistence stopped owning the SLA budget
Not a faster insert — a different unit of work. The bars are to the same scale.

the old path

rows = transformed_output_rows
for batch in chunks(rows, 1000):
    INSERT INTO allocation_result_main (...) VALUES (...), (...), ...;

# 200K rows ÷ 1,000 = ~200 round trips, each paying:
#   SQL + parameter construction, network latency, parse, plan, execute,
#   WAL generation, index maintenance, transaction bookkeeping,
#   plus Python loop overhead on top.

the new path

  1. Read and transform the solver output.
  2. Use DuckDB for in-engine cleansing and schema-aware type coercion.
  3. Bulk-load into a PostgreSQL temporary table with COPY.
  4. Run any set-based validation or normalisation in SQL.
  5. INSERT INTO main SELECT … FROM temp — one set-based operation.
  6. Commit.

The win is not a faster insert. It is a different unit of work: hundreds of application-driven statements became one bulk ingestion plus one set-based move. DuckDB earns its place before the COPY by making the data schema-aligned on arrival, so Postgres is not re-validating and re-coercing row by row. The temp table earns its place by giving a staging area where dedup and normalisation happen in SQL before the final table is touched.

CostBatched INSERT loopCOPY → temp → main
Network round trips~200 for 200K rowsOne bulk load
Parse / plan overheadPaid per batchAmortised to near nothing
Python overheadLooping, batching, parameter serialisationDelegated to the bulk path
Database executionMany separate insertsBulk ingest + one set-based insert
Type coercionRepeated, inconsistent, in PythonOnce, schema-aware, in DuckDB
Failure handlingPartial batches complicate recoveryStage, then commit one result
Observed~90 secondsunder 4 seconds
say it like this

“The improvement came from cutting repeated application-to-database round trips and moving from row-oriented writes to a bulk load plus a set-based database operation.”

17Scale & capacity validation

A single PO passing is not evidence. The dimension that matters is intermediate row expansion multiplied by concurrency — so the load tests were built around concurrent POs, not throughput of one.

PrincipleHow it shows up here
Bound work per requestProcess picks 20 POs at a time, never the whole batch.
Fan out safelyCloud Tasks per PO, with controlled concurrency and isolated retry queues.
Use durable statePostgres tracks status, ownership, completion and outbox events.
Optimise the hot pathPolars for transformation, DuckDB COPY for persistence.
Never redo expensive workResults are persisted before delivery, so delivery retries independently.
Make retries safeBusiness-level idempotency on allocation_code.

Load tests sustained 100 concurrent POs and roughly 150–200M row-level computations inside the SLA.

18Technology decision record

TechnologyWhyAlternative & trade-off
GCSDurable file handoff between client, preprocessing, solver and output.Direct API transfer — more coupling, more memory pressure.
Google WorkflowsStateful orchestration per PO type, with retries and loop-until-done.A cron/scheduler has no state; Kafka is heavier than this needs.
Cloud TasksRetryable per-PO async execution with controlled dispatch.Kafka is better for streams, weaker for HTTP work items with retry semantics.
PostgreSQLDurable lifecycle, idempotency, lease, results and outbox in one place.Redis is faster for ephemeral locks, insufficient as the only source of truth.
PolarsTransformation-heavy, memory-efficient, parallel — under an SLA.Pandas handles one PO fine; less suited to concurrent heavy workloads.
Gurobi (existing service)Mature solver for genuinely constrained allocation.Greedy works for simple methods; building a solver is not a real option.
DuckDBFast local analytical transforms and schema-aware cleansing pre-load.Python/Pandas coercion adds per-row overhead.
Temp table + COPYBulk staging and one set-based final insert.1,000-row batch inserts meant ~200 round trips and 90 seconds.
OutboxDurably records finalisation intent so delivery can be replayed.Direct upload then status update leaves an unsafe crash window.

19Alternatives seriously considered

The four decisions most likely to be challenged, and the honest version of each argument — including what the rejected option would actually have been good at.

  • Kafka instead of Cloud Tasks. Kafka is the better answer for a continuous event stream with multiple consumers and replay. This was discrete, per-PO, retryable HTTP work with a natural completion — Cloud Tasks gave that with far less operational surface. Trade-off accepted: weaker replay story, which the outbox and durable state cover instead.
  • Redis for locking. Genuinely faster for high-frequency short-lived locks. Rejected because the state being protected already lived in Postgres, and adding Redis would have introduced a second consistency boundary while still needing durable completion state somewhere. Trade-off: more lock traffic on the primary database, acceptable at this concurrency.
  • Pandas instead of Polars. Cheaper in team familiarity and ecosystem. Rejected on the concurrency profile, not on single-PO size. Trade-off accepted: a ramp-up cost on a newer library.
  • SKIP LOCKED queue semantics. The natural instinct for a picking API. Rejected because the Workflow already serialised picking per batch — the actual threat was retry-after-partial-completion, which a lock does nothing about. Solving the wrong problem elegantly is still solving the wrong problem.
  • Doing more work in SQL. Would have kept computation next to the data. Rejected because the business logic branches across 15–18 paths with derived calculations — far easier to structure, unit test and extend in application code with a dataframe engine.

20Cross-functional work & estimation

Four groups touched this: the ingestion team upstream, the optimisation team who owned the solver service, PM and EM on scope and dates, and the allocation team building the workflow. Most of the coordination cost was in contracts, not code.

  • Ingestion. Agreeing what “validated” means and which statuses the allocation flow is allowed to pick — the -120 / -100 split is that agreement written into the schema.
  • Optimisation team. A service contract: what store-level input looks like, how allocation_code, configuration and GCS paths are passed, what the callback guarantees, and what happens on failure. This is the boundary where most integration bugs would otherwise live.
  • PM. Translating “fast enough” into a number. The 15-minute SLA is what made the persistence rewrite fundable rather than a nice-to-have.
  • EM. Sequencing — specifically the argument for spending a milestone on correctness hardening before scale testing, on the grounds that load-testing an incorrect system just produces confident wrong numbers.

How the estimates were built

Estimated per milestone rather than per ticket, with the two genuinely unknown areas — solver integration behaviour and persistence throughput — explicitly time-boxed as spikes before committing dates. The 15–18 processing combinations were estimated as one framework plus N strategies, so the cost curve was flat per additional method instead of linear in unknowns.

the PM/EM story to have ready

One concrete instance where you converted ambiguity into scope: what was unclear, the options you put in front of them, the trade-off you recommended, what got cut or deferred, and the outcome. A named decision beats a description of the process.

21Mentorship

The question is never “did you help people”. It is whether someone's scope grew because of you, and whether you can name it. Structure each story the same way: where they started, what the gap was, what you actually did, what they owned afterwards.

Track recordContext
Tech Lead — Impact AnalyticsLed the allocation platform team; design reviews, delivery and production ownership.
5 developers mentored — WorkIndiaDirected delivery across design, implementation and production support.
3 engineers led — BlowhornLast-mile delivery and the Kirana module, built from inception.

The one story to prepare properly

engineer + starting point

Who they were, their level, and what they could not yet do on their own.

the gap

Be specific and technical — e.g. could implement a feature but not reason about failure modes; wrote correct code but not testable code; strong locally, avoided cross-team design conversations.

what you did

The concrete mechanism: pairing on a design doc, handing over a scoped-but-real piece of the workflow, review comments aimed at reasoning rather than syntax, putting them in front of the partner team on purpose.

the outcome — the part that counts

What they owned afterwards that they did not own before, and the ladder movement: promotion, scope increase, becoming the on-call owner of a component. That sentence is the whole answer.

shape of the answer

“X was strong at implementation but had not owned a distributed failure story. I handed them the finalisation and outbox path with the failure matrix as the spec, reviewed their design rather than their diffs, and had them present it to the optimisation team. They ended up owning that component end to end, including on-call, and moved to [level] at the next cycle.”

22Self-initiated work

Vendor-driven and reactive work does not demonstrate much. Two things here were not asked for by anyone.

Inside this project — the reliability and performance rework

Nobody filed a ticket saying “batch IDs generated server-side will strand POs on retry”. That came from sitting down and enumerating what happens if each API dies between its side effect and its response. The same applies to the persistence rewrite: the requirement was an SLA, not a technique — profiling the stages and finding that one step was spending 90 seconds was self-directed, and the DuckDB/COPY design was a proposal, not an assignment.

Outside it — mtptrace

An org-wide OpenTelemetry library, started because debugging across FastAPI microservices was guesswork and the commercial APM bill was real. Runtime module discovery, wrapt-based auto-instrumentation, AST analysis, build-time caching, configurable sampling, error-preserving span filters, tenant-aware tracing, trace–log correlation and automatic PostgreSQL query visibility — packaged so a team gets tracing by adding an import. It replaced the commercial APM with Grafana Tempo and cut trace-storage cost by roughly $21,000 a year.

That is the cleaner “innovation and initiative” answer, because it was invented rather than assigned, adopted beyond one team, and had a number attached.

if asked for proactive work specifically

“Two kinds. Within the allocation platform, the reliability model and the persistence rewrite were both things I identified and proposed rather than picked up. Separately, I built mtptrace — an OpenTelemetry library adopted across our FastAPI services that replaced a commercial APM and saved about $21K a year. That one started as nobody's project.”

23Learning curve

Three things this project actually taught, stated as principles rather than war stories.

  • Identity before side effect. If the server invents the identifier after it starts mutating state, a lost response is unrecoverable. Generate it at the caller. This one rule would have prevented the original Init bug and generalises to every retryable API.
  • Idempotency belongs to the business operation. Task IDs, retry counts and message IDs describe transport. Key on the thing the business considers one operation, or duplicates find a way through.
  • The bottleneck is rarely the interesting code. The expensive part of this pipeline was not the optimisation or the transformations — it was writing rows to a database in the obvious way. Profile before optimising anything you find intellectually appealing.

Ramp-up cost worth naming honestly: Polars was new to the team, and reasoning about lease durations versus worst-case processing time took iteration to get right — a lease too short causes duplicate work, too long causes stuck POs.

24Outcome & metrics

MetricBeforeAfter
Result persistence per PO~90 secondsunder 4 seconds (>20×)
PO pickupManual intervention possibleFully automatic on validation
Retry after partial failureStranded POs, SLA breachSafe replay, no duplicate work
Duplicate final uploadsPossible on crashDeterministic path, idempotent
Validated concurrency—100 concurrent POs in SLA
Computation volume in SLA—150–200M row-level computations
business impact — get the real numbers

Go-live date and outcome, share of POs processed automatically, SLA adherence in production, manual effort removed, incident/rework reduction. Engineering metrics prove competence; a business number is what makes it a staff-level story.

25The hardest part

Two candidate answers. Lead with the first — it is a reasoning story, not a tuning story — and keep the second as the follow-up when they ask about performance.

1. Correctness across five systems that each promise only at-least-once

The difficulty was not any single mechanism; it was recognising that Workflow, Cloud Tasks, Postgres, GCS and the solver service cannot be made to agree atomically, and that every gap between a side effect and its acknowledgement is a distinct failure mode with a distinct fix. Enumerating those windows produced the failure matrix; the matrix produced the design. Caller-generated identity, a business-level idempotency key, an owner-and-lease protocol with atomic takeover, and an outbox with deterministic destinations are four different answers to four different questions — treating them as one “make it idempotent” task is how these systems end up subtly broken.

The subtlest single decision was the lease. It converts “a worker is processing” from an assertion into a claim with an expiry, which is the only version of that statement that survives a process being killed.

2. Persistence, once it turned out to be the bottleneck

Harder in a different way: the code was correct, readable and obviously slow only once measured. The fix required stepping outside “make the insert faster” and changing the unit of work entirely — bulk load into staging, then one set-based move.

26Question drill

Read the question, answer it out loud, then open the card and compare.

Scope, ownership, leadership

What exactly did you own — and what didn't you?

Owned: architecture, orchestration, the state and idempotency model, the preprocessing pipeline, solver integration, persistence performance, reliability and capacity validation.

Did not own: the Gurobi service's mathematical formulation and internals, and the upstream ingestion implementation. Say the second half unprompted — it buys credibility for the first.

How did you break the work down?

Six milestones: contract and state model → single-PO happy path → the 15–18 business paths behind a strategy interface → correctness hardening → performance and capacity → go-live and observability. Correctness deliberately preceded load testing.

How did you collaborate with your PM and EM?
  • PM: turned “fast enough” into the 15-minute SLA, which is what justified the persistence rewrite.
  • EM: argued for and won a hardening milestone before scale testing.
  • Both: milestone-level estimates with the two unknown areas time-boxed as spikes first.

Have one specific ambiguity-to-scope decision ready with a name and an outcome.

How did you mentor engineers?

One story, four beats: where they started, the specific technical gap, the mechanism you used (scoped real ownership plus design-level review, not diff-level), and what they owned afterwards including any level change.

What was self-initiated rather than assigned?

Within the project: the failure-mode enumeration that drove the reliability design, and the persistence rewrite. Outside it: mtptrace, the org-wide OpenTelemetry library that replaced a commercial APM and saved ~$21K/year.

Technical

Why Google Workflows?

Stateful orchestration with retries and loop-until-complete semantics, and a clean separation of the three PO-type flows. A scheduler alone carries no state between calls.

Why Cloud Tasks and not Kafka?

Kafka is stronger for event streams with multiple consumers and replay. This was discrete, per-PO, retryable HTTP work with a defined completion — Cloud Tasks fit it with much less operational weight.

Why is allocation_code the idempotency key?

Because it identifies the business operation. Task IDs and retry counts identify transport attempts — a duplicate task for the same PO has a different task ID but the same allocation_code, and that is exactly the case you need to catch.

Why Postgres for idempotency? When would Redis be better?

Postgres because the protected state already lived there — one consistency boundary, durable across restarts, transactional with the business state.

Redis when the coordination is high-frequency and ephemeral — cache stampede prevention, rate limiting, short-lived task coordination — and duplicate execution is either tolerable or protected somewhere else.

Isn't SELECT … FOR UPDATE enough?

No. The lock dies with the transaction. It serialises two concurrent transactions but records nothing durable, so a retry ten minutes later proceeds happily. Locks handle concurrency; durable state and idempotency handle retries.

Why Polars over Pandas?

Not because 180K rows is too much for Pandas — it is not. Because several POs run concurrently on one worker with transformation-heavy pipelines, memory constraints and an SLA. Polars' Rust columnar engine and parallelism fit that profile.

Why a solver at all — couldn't you write the allocation rules yourself?

For the simpler methods, yes, and those are handled deterministically. Once pack constraints, allocation multiples, inventory limits, demand and store capacity interact, greedy choices are locally sensible and globally poor. That is a constrained optimisation problem, and it belongs in a mature MILP solver.

Why did DuckDB COPY make persistence 20× faster?

It changed the unit of work. Batched inserts meant ~200 round trips, each paying parse, plan, WAL and Python overhead. The new path does schema-aware coercion once in DuckDB, one bulk COPY into a temp table, then a single set-based insert into the main table.

What happens if the Gurobi service fails?

Retry keyed on allocation_code and the GCS paths, with statuses observable throughout. Because downstream completion is durable, a retry cannot duplicate work that already finished.

Does the outbox give you exactly-once delivery?

No, and it is worth being precise about that. The outbox durably records the intent to upload alongside the state change. The upload itself is made safe separately, by deriving the destination path deterministically from allocation_code so a retry rewrites the same object.

What would you redesign?
  • Introduce the outbox earlier — at Process dispatch, not just at finalisation.
  • Tighten the status model; some statuses carry stage-specific meaning that is easy to misread.
  • Formal ownership heartbeats and lease metrics rather than relying on a conservative TTL.
  • Alerting on leases that expire without completion — the earliest signal that something is wedged.

27What not to say

Do not saySay instead
“I built the Gurobi solver.”The service already existed; I integrated it and owned the workflow around it.
“Polars was required — 180K rows is too much for Pandas.”Pandas handles one PO; Polars was chosen for concurrent transformation-heavy work under an SLA.
“Cloud Tasks gave us exactly-once processing.”Cloud Tasks gives at-least-once; idempotency is what makes duplicates safe.
“The outbox solved exactly-once upload.”The outbox made finalisation durable; idempotent upload made retries safe.
“FOR UPDATE solves duplicates.”Locks solve concurrent transaction conflicts; durable state solves retries and late duplicates.
“We processed millions of rows.”Name the shape: 300 rows × 600 stores per PO, ~1.8M intermediate rows per worker.

28Gaps to fill before the interview

Six facts this write-up cannot supply. Each one is a question a hiring manager will ask, and a blank is more damaging than a modest number.

  • Project duration. Months from design to production, split by phase.
  • Team composition. Backend, ingestion, optimisation, PM, EM, QA — and who you led.
  • Your own code. Named modules, APIs and functions you personally wrote or rewrote.
  • Business impact. Go-live outcome, automation rate, SLA adherence, manual effort removed, incidents avoided.
  • PM/EM story. One concrete instance of turning ambiguity into scope and a technical plan.
  • Mentorship story. One engineer: gap, coaching, and what they owned afterwards.
closing line

“The biggest design principle was making every distributed boundary recoverable. Deterministic business identifiers, durable state, leases, idempotent consumers, bulk persistence and outbox-backed finalisation — so retries and crashes became normal operating conditions instead of production incidents.”

That's the whole system. The other five are back on the projects page — or the terminal on the home page takes questions.

← back to projects