cswork / libraryOpen practice workspace

Interview guides · Aurelius Enterprise

Aurelius Enterprise Software Engineer Interview Guide

Aurelius Enterprise Software Engineer interview guide: reported C++, concurrency, real-time and debugging questions, a 7-day plan and behavioral prep.

The 60-second brief

Aurelius Enterprise builds robotics, perception and complex mechanical systems, and the Software Engineer role covers the software that lets that hardware sense and act in the physical world. Candidates describe tracks that range from robotics orchestration to low-level power electronics, with software engineers working day to day beside mechanical, electrical and systems engineers. The responsibilities candidates describe run from design and implementation through testing and field deployment, plus code reviews, design documentation and post-mortem analyses. The requirements list C++ or a similar low-level language, real-time operating systems and data structures and algorithms, with ROS, Python for scripting and testing, and hardware-in-the-loop testing as extras.

This guide is for candidates applying as a Software Engineer at Aurelius Enterprise, in whichever track the job title names, such as robotics, perception or power electronics. It covers the five technical questions candidates report (memory leaks under concurrency, synchronization primitives, non-deterministic hardware-software bugs, a real-time system you built, and failure modes of your own stack), the two sample scenarios about a sensor-to-output data path and a CPU-bound loop, the stack the requirements list, a seven-day plan and behavioral preparation.

No numbered round list is reported. Candidates describe a technical screen followed by longer sessions on architecture, coding and behavioral topics, in a process that they say runs about three to six weeks.

Focus areas

  • Software Engineering
  • Robotics Software Engineering
  • Perception Engineering

Overview

The work sits where software meets hardware: code that reads sensors, drives actuators or manages power systems, written alongside mechanical and electrical engineers. That shapes what the reported questions look like. They are not puzzle-style algorithm prompts; they ask how you would find a leak in a concurrent robotics process, how you choose between locks, queues and atomics, how you chase a bug that only appears sometimes across a hardware-software boundary, how you structured a real-time system you built, and which failures your own stack tends to produce. Most of them are answered best from systems you have actually shipped.

Candidates also report two sample scenarios: following a piece of data from sensor input to output, and speeding up a loop that is pinned at its CPU limit. Prepare both as measurement exercises. For the data path, list every copy, queue, thread hop and conversion between the sensor and the output, and say where latency and jitter come from. For the hot loop, start with a profiler such as perf, then talk about cache behavior, allocation in the loop, branch-heavy code and data layout before touching algorithms.

The requirements list C++ or an equivalent low-level language, an RTOS, and solid data structures and algorithms, so expect to write and reason about real code in a shared editor with ordinary IDE features, as candidates describe it. Testing is called out explicitly in what candidates report: unit, integration and system-level tests, and hardware-in-the-loop work as a bonus. Have a concrete story about a bug your tests caught or missed, and about how you would test code that cannot run without a device.

Because the roles are described as specialized, decide which track you are interviewing for and go deep there instead of skimming everything. A perception candidate should be able to discuss sensor timestamps, synchronization between sensors and frame-rate budgets; a power-electronics or controls candidate should be ready for timing, fault handling and safe states. Candidates also report long discussions of a past project covering trade-offs, edge cases, failures and lessons, so choose the project you will present now and rehearse it.

Where to focus your preparation

No fixed round list is reported, so this guide uses a single preparation-focus step. Confirm the actual sequence and format with your recruiter.

Preparation focus

Candidates describe a technical screen first, followed by a series of longer sessions covering architecture, coding and behavioral topics, often in a collaborative coding environment with standard IDE features. No further round names or order are reported, so treat this step as the whole preparation target. Reported evaluation areas are technical depth (memory management, concurrency, hardware integration), system design that respects hardware limits and latency, and the ability to explain engineering choices to people outside software. Start by asking the recruiter which sessions apply to your track and whether you will write code, then prepare each of the five reported technical questions from your own projects.

What to demonstrate

  • Technical depth in your track's language and runtime: memory ownership, concurrency, and where software meets sensors, actuators or power systems
  • Architectural reasoning for systems with hardware limits and latency requirements, such as a real-time pipeline you built
  • Debugging and testing habits across the software-hardware boundary
  • How clearly you explain the reasons behind your technical choices, including to non-software colleagues

How to prepare

  • Write a short answer for each reported question (memory leak under concurrency, synchronization primitives, non-deterministic hardware-software bug, real-time system you built, failure modes of your stack) using a real project of yours
  • Trace the sensor-to-output path of a system you know, listing every queue, copy and thread hop, then profile one hot loop and note what you would change
  • Practice writing C++ in a plain editor with basic IDE help only, and add unit tests to what you write
  • Ask the recruiter which track the interviews target, what the sequence looks like and whether coding is shared-editor based

Where candidates lose points

Answering the memory-leak question with "use smart pointers" and no diagnosis method

Describe the sequence: confirm growth over a soak run, locate it with a sanitizer or heap profiler and snapshot diffs, identify the owner and path (cycle, unbounded queue, error path), then fix ownership and add a regression check.

Defining mutexes, spinlocks and condition variables without choosing between them for a constraint

Tie each primitive to a scenario: critical section length, contention, ISR versus task context and priority inversion. Be ready to say why a lock-free ring buffer or an ISR-safe semaphore suits a sensor handoff.

Jumping to "it's a race condition" when asked about an intermittent hardware-software bug

Start with observability: sequence numbers, monotonic timestamps, recorded traffic. Classify loss, timing, memory, concurrency and physical causes, and quantify the failure rate before and after your fix.

Describing the real-time system you built with no deadlines, rates or measured latency

Bring numbers: period, latency budget, measured worst case and how you measured it. Be able to say which stages ran in interrupt versus task context and what the system did when a deadline was missed.

Presenting a project at the "what I did" level and having no story about testing or failures

Prepare the trade-offs behind each decision, the bug your unit, integration or system tests caught or missed, and what you changed afterward, since candidates report long project discussions covering edge cases and lessons.

Questions & practice

How would you handle a memory leak in a high-concurrency robotics system?

Medium · Technical & Domain Expertise

Approach

  1. First confirm that it is a leak. Track RSS and heap size over a steady-state soak run. Memory that stays reachable but keeps growing, such as a queue backing up because the consumer is slower than the producer or a map keyed by object id that is never erased, needs a different fix from allocations that are truly unreachable.
  2. Find the allocation site with tooling: LeakSanitizer/ASan in a test build, heaptrack or jemalloc/tcmalloc heap profiles diffed between two snapshots, and Valgrind memcheck or massif as a last resort. Valgrind is slow and changes thread timing, so a race-dependent leak may disappear under it.
  3. Check the usual concurrent C++ causes: shared_ptr cycles (break them with weak_ptr), callbacks or lambdas that capture shared_ptr to self and are never deregistered, early-return or exception paths that skip a raw delete (fix with RAII and unique_ptr), and threads that are detached and never exit.
  4. For lock-free structures, deferred reclamation (hazard pointers, epoch-based reclamation) stops freeing memory when one thread stalls and holds back the epoch. Name that as a leak source that only appears under load.
  5. Keep allocation off the real-time path altogether: preallocate pools and fixed-capacity ring buffers at startup, set an explicit overflow policy (drop oldest or reject) and count overflows. Add a soak test to CI that asserts memory stays flat.

Follow-up

  • The leak only shows up after a long field run and never under Valgrind. How do you find it?
  • Why does a shared_ptr cycle never free, and how does weak_ptr break it?
  • How would you guarantee that the control loop performs no heap allocation at all?

Track a rolling failure rate per destination for circuit decisions

Easy · sliding window · ring buffer · circuit breaker

The egress service delivers about 1,500 webhooks per second across roughly 40,000 destinations, each call bounded by a 10 second timeout. Maintain, per destination, the failure rate over the trailing 60 seconds so a caller can ask before dispatch whether the circuit should open. Attempts arrive as (destination_id, finished_at_ms, outcome). Requirement: amortised O(1) per attempt, with total memory bounded by the destination count rather than by traffic. Give the structure, its exact memory, and the rule that stops a destination with three attempts from opening a circuit.

Approach

  1. Name the exact-deque version and then reject it as the default. Holding timestamps and advancing a tail pointer past anything older than now minus 60 seconds is a correct two-pointer window at amortised O(1) per attempt, but its memory tracks in-window traffic, so one destination in a retry storm holds hundreds of thousands of entries while thousands of quiet destinations hold none.
  2. Use a ring of 60 one-second buckets per destination, each bucket a pair of counters for attempts and failures. On an attempt, advance the ring by the elapsed whole seconds, zeroing at most min(elapsed, 60) buckets, then increment the head. That is amortised O(1) with a fixed footprint per destination.
  3. State the footprint: 60 buckets times two 4-byte counters is 480 bytes of payload per destination, so 40,000 destinations is roughly 20 to 25 MB with per-entry overhead, bounded by the catalogue rather than by the rate. The cost is granularity, since the oldest bucket ages out in whole seconds, which is far tighter than the decision needs.
  4. Require a minimum sample before the circuit may open. A destination with three attempts and three failures reads as 100 percent and is not evidence; a floor of roughly 20 attempts in the window makes the ratio meaningful, and below that floor use a run of consecutive failures as the trigger instead.

Follow-up

  • The fleet is 30 instances and each sees roughly a thirtieth of a destination's traffic. Where does the rate actually live, and what does a per-instance answer get wrong?
  • A destination answers in 9.5 seconds and succeeds. It is not failing but it is consuming your per-destination concurrency. What signal should open the circuit here?

Archive a resource graph without breaking live references or recursing

Medium · graph traversal · topological order · tenant isolation

Resources reference other resources within a tenant; for the largest tenant the reference table holds up to 2,000,000 nodes and 8,000,000 edges. Archiving a resource must archive everything reachable from it that nothing outside the set still references, refuse when a live external referrer exists, and terminate when references form cycles, which they legitimately do. Produce the archive order and the refusal list, targeting O(V+E). Say what stops the traversal crossing a tenant boundary, and why recursion is the wrong control structure at this size.

Approach

  1. Load the subgraph with the tenant predicate on both endpoints of the edge, not only on the side you started from. Scoping the left table alone is the classic cross-tenant leak: one mis-entered edge then pulls another tenant's resources into the traversal and, worse, into the archive.
  2. Traverse iteratively with an explicit stack. A 2,000,000-node graph can hold a chain deep enough to exhaust a native stack in the low tens of thousands of frames, and that failure is a process crash rather than an error you can return.
  3. Treat cycles as data rather than corruption: compute strongly connected components with Tarjan in O(V+E) using its own explicit stack, then condense. The condensation is a DAG, so a topological order over it gives the archive order, and every member of a component archives in one transaction because no order within a cycle is valid.
  4. Decide refusals with reverse edges. A candidate is archivable only if every in-edge originates inside the candidate set, so build the transpose or count in-degrees restricted to the visited set, and emit each blocked resource with the id of the external referrer, which is the only part of the answer an operator can act on.

Follow-up

  • The graph is read in one query and the archive writes a minute later. What can change in between, and how do you make the write safe?
  • The candidate set is 400,000 resources. Is that one transaction, and if not, what does a half-finished archive look like to a reader?

Replace offset paging on the resource feed with keyset

Easy · keyset pagination · composite index · sort order

resource holds resource_id, tenant_id, owner_user_id, title, body_ref, version, status ('draft','active','archived','deleted'), created_at, updated_at, deleted_at, with an index on (tenant_id, status, updated_at DESC, resource_id DESC). The listing endpoint returns active resources for one tenant, newest update first, 50 per page, today with LIMIT 50 OFFSET n. Tenants reach page 400 and rows are created while they read. Write the keyset query, define what the cursor carries and how it is encoded, and say which part of the index each predicate uses. Assume PostgreSQL 16.

Approach

  1. Name the two failures separately. OFFSET 20000 makes the server produce and discard 20,000 rows, so page cost grows with depth rather than with page size. Independently, any write that changes how many rows sort above the offset moves the window between two fetches, and the direction decides which anomaly you get: an insert lands at the head of updated_at DESC and pushes already-returned rows down past the boundary, so they are returned a second time; a delete above the offset, or a row whose updated_at is bumped above the cursor, pulls rows up and one is never returned at all. Nothing in the response reveals either.
  2. Write the seek: WHERE tenant_id = $1 AND status = 'active' AND (updated_at, resource_id) < ($2, $3) ORDER BY updated_at DESC, resource_id DESC LIMIT 50. The row-value comparison is one index range rather than a disjunction, and both columns are NOT NULL, which is what makes that comparison well defined.
  3. Map each predicate onto the index: tenant_id and status are equality on the leading columns, (updated_at, resource_id) is the range, and the ORDER BY matches the index order so no Sort node appears and the scan stops after 50 rows. The DESC in the definition only matters for mixed directions — a plain ascending btree on the same columns is read backwards for this query.
  4. Put both sort columns in the cursor and nothing the client can tamper with into another tenant: base64 of (updated_at, resource_id), validated server-side, with tenant_id taken from the principal.

Follow-up

  • The client asks for 'jump to page 400'. What do you offer instead, and what does the honest version cost?
  • Sort order becomes user-selectable across four columns. How many indexes is that, and which would you refuse to add?

Explain why the owner filter ignores the listing index

Medium · composite index · query plans · sort elimination

The only index on resource is (tenant_id, status, updated_at DESC, resource_id DESC). A new endpoint returns one user's resources across all statuses, newest created first: WHERE tenant_id = $1 AND owner_user_id = $2 ORDER BY created_at DESC LIMIT 20. On a tenant with 2M rows it takes 900 ms and EXPLAIN shows a sort above a large scan. Explain precisely why the existing index cannot serve it, give the index that can, and state which of these the new index still will not help: owner_user_id alone across tenants; the same query ordered by updated_at. PostgreSQL 16.

Approach

  1. Separate the two jobs an index does. For filtering, a composite btree is seekable only on a left prefix, so with no predicate on status the scan can at best range over tenant_id and test owner_user_id per row; PostgreSQL 16 has no btree skip scan to jump the unconstrained column.
  2. For ordering, the index is sorted by (status, updated_at) within a tenant and not by created_at, so the LIMIT cannot stop early: every matching row is read and then sorted. That is the 'Sort Method: top-N heapsort' line, and it is why the plan reads 2M rows to answer with 20.
  3. Derive the replacement from the access path — equality, equality, then the ordering column: CREATE INDEX CONCURRENTLY ON resource (tenant_id, owner_user_id, created_at DESC). The scan seeks to the (tenant, owner) range and walks 20 entries in order, so the Sort node disappears along with the row-read.
  4. Treat INCLUDE (title, status) as conditional, not free. An index-only scan still visits the heap for any row whose page is not marked all-visible, so on a table taking 1.2k writes/second the win depends on autovacuum keeping the visibility map current, and the wider index costs more on every insert.

Follow-up

  • 90% of rows are status='active'. Would a partial index WHERE status = 'active' change your answer, and for which of the three queries?
  • A dashboard runs this for 40 owners in one page load. What changes about the design?

Explain the trade-offs between different synchronization primitives in your primary programming language.

Medium · Technical & Domain Expertise

Approach

  1. Mutex (std::mutex with std::lock_guard, or std::scoped_lock, which acquires several mutexes without deadlocking): simple and correct by default. Under contention a waiting thread may sleep, which costs a context switch and adds latency jitter.
  2. std::shared_mutex (C++17) lets many readers in at once but allows only one writer. It costs more per acquisition than a plain mutex, so it only pays off when reads clearly dominate and the critical sections are not tiny. Whether writers can starve depends on the implementation.
  3. condition_variable: wait with a unique_lock and a predicate (or a while loop), because spurious wakeups happen. The condition must live in shared state guarded by the mutex, or a notify sent before the wait is lost.
  4. std::atomic and spinlocks: lock-free for single words. Use acquire/release ordering to publish data, relaxed ordering for plain counters, and compare_exchange loops for updates, and watch for ABA. A spinlock suits only very short sections on multicore hardware. With preemption or a single core it burns the whole time slice.
  5. Real-time concerns: a mutex shared across priorities can cause priority inversion. Use priority-inheritance mutexes (PTHREAD_PRIO_INHERIT or the RTOS equivalent), never block inside an ISR, and pass data from an ISR to a task through a lock-free SPSC ring buffer or a semaphore.

Follow-up

  • What is priority inversion, and how does priority inheritance bound it?
  • When would you choose a spinlock over a mutex, and when is that a mistake?
  • Why does condition_variable::wait need a predicate?

Describe the architecture of a real-time system you have built from scratch.

Medium · Technical & Domain Expertise

Approach

  1. Open with the requirements that shaped the design: hard or soft real-time, the loop rates and deadlines, the end-to-end latency budget from sensor to actuator, and what the system does when it misses a deadline (hold the last command, degrade or stop safely).
  2. Walk the data path: driver or ISR, timestamped acquisition, filtering or estimation, control, actuation. Then say how tasks map to threads, with fixed priorities (rate-monotonic: a higher rate gets a higher priority), an RTOS or PREEMPT_RT Linux, and CPU pinning where you used it.
  3. Explain how data crosses between tasks running at different rates: lock-free SPSC queues or double/triple buffers, no heap allocation, blocking I/O or unbounded locks on the hot path, and logging handed to a low-priority thread.
  4. Give evidence: worst-case execution time and jitter measured as histograms or tail percentiles (not the mean), deadline-miss counters, a watchdog, and the schedulability check (the rate-monotonic utilisation bound n(2^(1/n) - 1), which tends to about 69%).
  5. Finish with what failed, what you changed, how you tested it (unit tests, simulation, HIL), and the one design choice you would revisit and why.

Follow-up

  • How did you measure worst-case latency, and what number did you get?
  • What happens to the rest of the system if one task overruns its deadline?
  • Why did you assign priorities the way you did?

What are the most common failure modes for the specific hardware/software stack you specialize in?

Medium · Technical & Domain Expertise

Approach

  1. Name one stack concretely, for example C++ on an RTOS with sensors on CAN or SPI, and organise the failure modes by layer instead of listing them at random.
  2. Hardware and communication: sensor dropout or noise, brownout and resets, connector faults, bus errors (a CAN node going bus-off), dropped, duplicated or out-of-order packets, and timeouts. Timing: missed deadlines, jitter, priority inversion, and clock drift between sensors that corrupts sensor fusion.
  3. Software and integration: stack overflow, heap fragmentation and leaks, use-after-free and uninitialised memory, races, integer overflow in timers or encoder counts, firmware/software version skew, and unit or coordinate-frame mismatches between modules.
  4. For each failure mode, say how it is detected (CRCs, heartbeat timeouts, watchdogs, stack high-water marks, plausibility and range checks on sensor values) and how the system degrades into a defined safe state.
  5. Back it with one real incident from your own work (symptom, root cause, fix, test added), plus how you prevent it now: static analysis, sanitizers in CI, fault injection and HIL tests.

Follow-up

  • Which of these failure modes is hardest to detect in the field, and why?
  • How should the system behave when a sensor goes silent mid-operation?
  • How would you inject that fault in a test rig?

How do you approach debugging a non-deterministic bug in a distributed hardware-software interface?

Medium · Technical & Domain Expertise

Approach

  1. Characterise the bug before theorising. Record how often it happens (one in N runs), under which load or temperature, on which unit and on which firmware and software versions. A measured base rate is what lets you prove a fix later.
  2. Instrument without changing timing. Log into a lock-free in-memory ring buffer that is dumped when the fault fires (printf-style logging can hide a race), timestamp both sides against a common clock such as PTP or hardware timestamps, and capture the bus (CAN, SPI, serial) with a logic analyzer or bus sniffer.
  3. List the classes of hypothesis: thread races or message reordering, timeouts and retries that produce duplicate or stale messages, clock drift between nodes, buffer overruns, struct packing or endianness mismatches, firmware/software version skew, and electrical causes such as noise or brownout.
  4. Narrow it down. Raise the failure rate with stress, injected latency and fault injection. Swap hardware units to separate a unit-specific fault from a systemic one, bisect across firmware and software versions, and replay recorded inputs in HIL or simulation to get a deterministic reproduction.
  5. Fix the mechanism (sequence numbers, idempotent handlers, CRCs, explicit timeouts), add a HIL regression test, and check the rate statistically. With zero failures in n runs the 95% upper bound is about 3/n, so roughly 300 clean runs are needed to claim a rate below 1%.

Follow-up

  • Adding logging makes the bug disappear. What do you do next?
  • How do you tell a hardware fault from a software race on the same symptom?
  • How many clean runs do you need before you call it fixed?

Suggested practice checklist

The plan follows the five reported technical questions and the two sample scenarios (the sensor-to-output path and the CPU-bound loop), then adds coding practice and project stories. Days 2 to 5 each produce something you can run or read aloud.

Day 1 — Pin down your track and map your experience

  • Ask your recruiter which track the interviews target (robotics, perception, power electronics or other), what the sequence of sessions is, and whether coding happens in a shared editor.
  • Write a one-page inventory of your C++, RTOS, concurrency and hardware-integration experience, and pick one project per reported question that you can discuss in depth.

Deliverable: A list of recruiter questions and a one-page inventory mapping each of the five reported questions to a project of yours.

Day 2 — Memory leaks in a concurrent C++ process

  • Write a small C++ program with three planted leaks (a shared_ptr cycle, an unbounded queue, a skipped cleanup on an error path) and run it under AddressSanitizer and heaptrack.
  • Write your answer to the memory-leak question as steps: confirm growth, locate, identify cause, fix ownership, add a regression check.

Deliverable: The leaky program with sanitizer and heaptrack output for each leak, plus a written step-by-step answer.

Day 3 — Synchronization primitives with working code

  • Implement a bounded queue with std::mutex and std::condition_variable (waiting on a predicate), then a single-producer single-consumer ring buffer with acquire/release atomics.
  • Run both under ThreadSanitizer, then write a table of mutex, spinlock, shared_mutex, condition variable, semaphore and atomics against use case, cost and failure mode.

Deliverable: Two queue implementations with a clean ThreadSanitizer run and a one-page comparison table.

Day 4 — Intermittent bugs and failure modes

  • Build a small producer-consumer program with an injected race or delay, reproduce it with stress runs, and work out how many clean runs you need to trust a fix (rule of three).
  • Write a debugging runbook: instrumentation, classification of loss, timing, memory, concurrency and physical causes, one-variable-at-a-time narrowing.
  • Write a failure-mode table for the stack you specialize in, with a detection method and a mitigation for each row.

Deliverable: A reproduced intermittent bug with a failure-rate measurement, a debugging runbook and a failure-mode table for your stack.

Day 5 — Real-time architecture, data path and hot loop

  • Write the architecture of a real-time system you built: requirements, rates, deadlines, task priorities, queues, allocation policy, safe state, test strategy and measured latency.
  • Trace one piece of data from sensor input to output, listing each copy, queue, thread hop and conversion, and mark where latency and jitter come from.
  • Profile a tight loop with perf and note two changes (data layout, allocation removal or similar) and the measured effect.

Deliverable: A one-page architecture write-up, a data-path diagram with annotated latency sources, and a before/after profile of one loop.

Day 6 — Coding practice in a plain editor

  • Review the complexity and memory behavior of both solutions out loud.

Deliverable: Two tested C++ solutions with a spoken complexity explanation for each.

Day 7 — Project deep dive and full run-through

  • Write STAR outlines for four stories: a trade-off you made, explaining a constraint to a mechanical or electrical colleague, a failure or post-mortem, and a vague problem you scoped yourself.
  • Answer all five reported technical questions aloud in a single run and note where you hesitated.

Deliverable: Four STAR outlines, two rehearsed practice stories and a list of weak spots from the spoken run-through.

Project deep dives and cross-discipline stories

Candidates report project discussions that go into trade-offs, edge cases, failures and lessons learned, plus an emphasis on working with mechanical, electrical and systems engineers and explaining technical ideas to non-software colleagues. They also report STAR-structured answers and a focus on testing and safety. Prepare stories that show your reasoning and your measurements, not only the outcome.

  1. Walk me through a project you owned end to end, focusing on the technical trade-offs you made and why.
  2. Tell me about a time you explained a software constraint or decision to a mechanical or electrical engineer who did not share your background.
  3. Describe a bug or failure in a system you built, how you found the cause, and what you changed afterward, including tests.
  4. Tell me about a time you were handed a vague problem and defined the constraints before you started building.
  5. Describe a decision you made using data, and what the data showed that surprised you.
  6. Tell me about a time a test, review or post-mortem changed how you design or write code.

Narrate an outage you owned from page to postmortem

Hard · incident response · blast radius · postmortems

Pick an incident you personally drove, ideally one where writes were affected rather than reads. In six to eight minutes: state the symptom as it first appeared on a dashboard, the blast radius you established before you knew the cause, the mitigation you applied and when, the mechanism you eventually proved, and the follow-up that would prevent a repeat. Bring numbers: error rate, tenants affected, minutes to mitigate, minutes to resolve. If you cannot name what you measured, choose a different incident.

Approach

  1. Open on the signal rather than the cause: which metric at which percentile moved, on which service, at what time, so the listener follows the same evidence you had rather than a conclusion you already reached.
  2. Separate mitigation from diagnosis out loud. State what you did to stop the bleeding (flag off, shed traffic, drain a lease, roll back a deploy) and say plainly that you did it before the mechanism was known, because those are two jobs with different deadlines.
  3. Establish blast radius in countable terms: how many tenants, how many writes, and crucially whether the effect was loss or only delay. An append-only revision table or a pending outbox row means the change survived and the projection was merely behind, which is a repair rather than a data-loss incident.
  4. Prove the mechanism instead of asserting it. Name the trace span that grew, the plan that flipped to a sequential scan, the lease that expired, plus one alternative you ruled out and the signal that stayed flat while you ruled it out.

Follow-up

  • What would you do differently in the first five minutes, given the same dashboard and no more information?
  • Which follow-up action did you deliberately not take, and why was dropping it the right call?

Reverse your own decision and price the reversal

Medium · reversibility · measurement · migrations

Describe a technical decision you made and later reversed. Pick one that cost something: a service you split and merged back, a cache you added and removed, an index you created that pushed the planner onto a worse plan, a projection you rebuilt from scratch. State what you believed when you decided, the measurement that changed your mind, how long the wrong version ran in production, and what the reversal cost in migrations, dual writes, and a deprecation window for callers you did not own.

Approach

  1. State the original rationale without irony, in the version you would still defend given what was known then. If it is not defensible, the story is about carelessness rather than judgement, and a different example serves you better.
  2. Give the measurement that moved with a before and after: the p99 that did not improve, the cache hit rate that sat at 40%, the plan that flipped to a sequential scan once the table passed a size you can name.
  3. Cost the reversal in steps, not adjectives: expand-and-contract deploys, the dual-write window, the callers who had to be notified, the rows already written in the wrong shape that had to be backfilled or abandoned.
  4. Distinguish reversal from rewrite by naming what you kept. Most good reversals preserve the schema or the interface and undo one decision inside it, which is also why they were affordable.

Follow-up

  • What in that decision was irreversible, and did you know it was irreversible when you made it?
  • How did you tell the people who had already built on top of the original decision?

Argue against a design, lose, and commit anyway

Medium · disagreement · service boundaries · decision records

Describe a design you argued against and lost. State the failure you predicted as a named mechanism, not a feeling about complexity: two services that would need one transaction, a projection with no rebuild path, a write path with no idempotency key. Say what evidence you brought, what the decision maker weighed instead, and what you did after the decision was made: what you instrumented, what you wrote down, and whether the prediction came true. Five minutes.

Approach

  1. State the prediction in falsifiable form up front: the mechanism, the condition that triggers it, and the observable outcome. A prediction that cannot be checked also cannot be credited to you later.
  2. Show the evidence you had at the time and label each piece honestly as measured, analogous, or intuition. Keeping the intuition is fine; disguising it as data is the thing that erodes your standing in the next argument.
  3. Represent the opposing case at full strength, including the constraint you did not control: a fixed date, a team boundary, or the fact that the decision was cheap to reverse and yours was not.
  4. Make disagree-and-commit concrete. Name the artefact you left behind so the prediction could be settled without you: the alert and its threshold, the counter on the dashboard, the decision note that recorded the trade-off and the condition that would revisit it.

Follow-up

  • What threshold on that alert would have proved you right, and did anyone ever look at it?
  • If the same proposal arrived tomorrow with the same deadline, would you argue it the same way?