Interview guides · AERONIX
AERONIX Software Engineer Interview Guide
AERONIX Software Engineer interview guide: reported questions on debugging, thread safety, hardware limits and legacy integration, plus a 7-day plan.
The 60-second brief
Candidates describe AERONIX as a company that builds products for defense and aerospace partners, with facilities reported in Melbourne, FL and Arlington, VA. Specific products and programs are not named in candidate reports, so ask your recruiter which program and team the opening belongs to. The Software Engineer role is described as often aligned with Lead Systems Engineer responsibilities. Reported duties include designing, implementing and testing software, working closely with hardware engineers so the code suits the target platform, maintaining and refactoring existing codebases for performance and reliability, taking part in code reviews, and helping shape technical plans for later releases. The stack candidates list is C or C++ (Python is also named), an RTOS, hardware-in-the-loop (HIL) testing, TCP/IP and UDP networking, and Git.
This guide is for engineers preparing for an AERONIX Software Engineer interview, especially those with embedded, real-time or systems-level C and C++ experience. Reports describe the role as typically asking for around five or more years in software or systems engineering. The guide covers the five reported questions (debugging an unclear system-level fault, performance versus maintainability, a hardware or memory constrained feature, thread safety, and modular architecture around legacy hardware), the reported real-time and architecture topics, a behavioral section and a seven-day plan. It does not cover pay.
No fixed round sequence is reported. Candidates describe recruiter screenings followed by deep technical discussions with senior engineering staff and leadership, spread over a few weeks and varying by location and team.
Focus areas
- FPGA Design
- Hardware Description Languages (HDLs)
- Digital Design
Overview
The work candidates describe sits where software meets hardware: code tuned to a specific platform, written alongside hardware engineers, maintained and refactored over time, and reviewed by peers. The five reported questions come straight from that work. They ask how you debug when the cause is unclear, how you trade speed against maintainability, how you fit a feature into tight hardware or memory limits, how you keep multi-threaded code safe, and how you structure software that must talk to older hardware. Candidates describe the questions as practical rather than puzzle-style, so your own past projects are the raw material for most answers.
The real-time and embedded topics candidates report are interrupt handling and priority management, memory management and leaks in long-running processes, the effect of OS kernel scheduling on real-time behavior, and the advanced trio of DMA, cache coherency and bus contention. Two example questions are reported: "How would you optimize a high-frequency data loop?" and "Describe how you handle race conditions in an embedded environment." For each topic, prepare a one-sentence rule, one way it fails, and one place you met it. For example: keep an interrupt handler short and defer the work to a task; a low-priority task holding a lock that a high-priority task needs causes priority inversion, which priority inheritance addresses; a DMA transfer can leave the CPU cache stale unless you invalidate or clean the right lines and align the buffers.
On the architecture side, the one reported topic is decoupling strategies for large systems. The stack list adds an RTOS, HIL testing, TCP/IP and UDP, and Git. Candidates also mention FPGA design, hardware description languages, digital design, system-level engineering and systems integration. Be ready to explain where your software meets those layers: register maps, timing, interface contracts and how you test against hardware or a simulation of it. As general preparation, also be able to describe how subsystems talk to each other, what happens when one fails, and how you keep documentation and technical debt under control.
On the behavioral side, candidates report a question about implementing a feature under strict hardware or memory constraints, and they mention mentoring junior developers and communicating progress. Prepare STAR stories with real numbers (bytes, microseconds, defects found) and rehearse talking through your reasoning and assumptions aloud. If your strongest language is not C or C++, be ready to explain how that experience applies to this kind of environment.
Where to focus your preparation
Candidates do not report a fixed sequence of stages, so the single entry below groups what to prepare across the screening and technical discussions they describe.
Preparation focus
Candidates describe a process that starts with recruiter screenings and moves into deep technical discussions with senior engineering staff and leadership, taking a few weeks and varying slightly by location and team. No stage-by-stage order or format is reported, so confirm with your recruiter whether you will write code, whiteboard a design or talk through past projects. Treat the stage as a technical conversation about systems: the five reported questions, the real-time and embedded topics, and your own projects. Start each answer by stating your assumptions, then walk through the reasoning and the trade-offs you weighed.
What to demonstrate
- Depth in real-time and embedded topics: interrupts, memory, scheduling, DMA, cache coherency and bus contention
- Debugging and design reasoning on system-level problems where the cause is unclear
- Systems thinking: how software decisions affect hardware, network behavior and overall stability
- Clear communication of technical choices and past project decisions
How to prepare
- Ask the recruiter which stages exist, whether code will be written, and which team and location the role sits in
- Write a short answer for each of the five reported questions, then say each one aloud with your assumptions and trade-offs stated
- Prepare one hardware-constrained project, one debugging incident and one concurrency bug from your own work, each with numbers
- Review C or C++ fundamentals that these topics rely on: volatile, memory layout, allocation strategy, atomics, and RTOS primitives such as mutexes, semaphores and message queues
Where candidates lose points
Answering the unclear-root-cause debugging question with 'add logging and see what happens'
Give a sequence: reproduce and capture the failing state, record what changed, split the system into halves to bisect, check that your instrumentation is not altering timing, and separate trigger from cause. Name the tools you would use on a target, such as a debugger, logic analyzer or trace buffer, and close with a regression test.
Treating performance versus maintainability as a slogan ('premature optimization is bad') instead of a decision
Say you profile first, optimise only the measured hot path, isolate it behind a clear interface, document why it is written that way, and add a benchmark or test that guards it. Give one real case where you chose each side and the number that justified it.
Answering thread safety with only 'use a mutex'
Start with what state is shared and who owns it. Then cover immutability, confining data to one thread, message queues, atomics, lock ordering to avoid deadlock, priority inversion on an RTOS, and the fact that an interrupt handler cannot block on a mutex. Name the race you found and how you proved it was fixed.
Telling embedded or hardware-limit stories without numbers or your own decisions
Before the interview, write each story with the constraint, the measurement before and after, the alternative you rejected, and your specific part. If you cannot supply a figure, pick a different story.
Describing legacy-hardware integration as 'wrap it in an adapter' with no detail
Name the boundary: a hardware abstraction layer with a narrow interface, a documented protocol or register contract, a simulated device so tests run without the hardware, and HIL runs for what simulation cannot show. Say how a new device would plug in without touching the callers.
Questions & practice
What strategies do you use to ensure thread safety in multi-threaded applications?
Medium · Technical Domain Expertise
Approach
- Start by shrinking the shared mutable state: confine data to one owning thread, make data immutable after publication, or pass messages through queues (RTOS message queues) instead of sharing memory. Then name each remaining shared variable and the invariant that protects it.
- Protect each invariant with a mutex held through RAII (std::lock_guard, std::scoped_lock). Hold locks briefly, never block on I/O while holding one, and use a single global lock order to avoid deadlock. std::scoped_lock can take several mutexes at once without deadlocking. Wait on a std::condition_variable with a predicate, cv.wait(lock, pred), so spurious wakeups are handled.
- Use std::atomic for single-word state, with acquire/release ordering when publishing data to another thread. volatile is not a synchronisation primitive in C or C++: it stops the compiler caching the value but gives no atomicity or ordering between threads.
- Embedded specifics: an ISR cannot block on a mutex, so use a short interrupt-disabled critical section, an ISR-safe queue call (in FreeRTOS, xQueueSendFromISR) or a lock-free single-producer/single-consumer ring buffer. Prevent priority inversion with priority-inheritance mutexes. In FreeRTOS, mutexes have priority inheritance and binary semaphores do not.
- Verify the design rather than trust it: ThreadSanitizer (-fsanitize=thread) or Helgrind on host builds, stress tests that vary thread timing, and a review of the lock discipline.
Follow-up
- Walk through how priority inversion happens and two ways to prevent it.
- Why does double-checked locking break without atomics, and what does C++11 give you instead (std::call_once, thread-safe function-local statics)?
- Is a volatile flag shared between an ISR and a task ever enough? When is it, and when is it not?
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
- 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.
- 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.
- 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.
- 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?
Canonicalise a request body into a stable idempotency fingerprint
Medium · parsing · canonicalisation · hashing
idempotency_key.request_fingerprint is a SHA-256 over the method, path and canonicalised body, and a retry whose fingerprint differs must be rejected with 422 rather than served the stored response. Write the canonicaliser. Bodies are JSON up to 256 KB nested at most 32 levels; clients vary key order, whitespace and unicode escaping, and some send 64-bit ids as JSON numbers. Produce a deterministic byte string such that semantically identical bodies match and any semantic difference does not. State your complexity and name two normalisations you refuse to perform.
Approach
- Parse once into a tree, then re-serialise under fixed rules: object keys sorted, array order preserved, one escaping convention, no insignificant whitespace. Parsing is O(n) and sorting keys is O(k log k) per object, so O(n log n) overall with O(depth) stack, and the 32-level cap is enforced during parsing because hostile nesting is how a canonicaliser becomes a stack overflow.
- Sort keys by their UTF-8 bytes and say why the obvious implementation is wrong in some runtimes: a default string comparison that orders by UTF-16 code units places surrogate pairs, meaning code points from U+10000 up, below U+E000 to U+FFFF, which is not UTF-8 byte order, so two services written in different languages disagree on the same document.
- Do not re-encode numbers through a double. IEEE-754 binary64 represents integers exactly only up to 2^53, so normalising a 19-digit id through a float changes it, and 1 against 1.0 cannot be reconciled without deciding whether they are the same value. Preserve the literal token, and require ids as strings at the API boundary if you want them comparable.
- Reject duplicate keys rather than picking one. JSON permits them and parsers disagree, most keeping the last, so any choice you make ties the fingerprint to a parser detail that the code handling the request does not necessarily share.
Follow-up
- A client sends the same logical request with an extra field your API ignores. Same key, different fingerprint, so you return 422. Is that the right answer?
- Where does the fingerprint get computed relative to request decompression and the body-size limit?
Write the update path that detects a concurrent edit
Medium · optimistic concurrency · transactional outbox · audit history
resource carries version INT NOT NULL DEFAULT 1. resource_revision holds revision_id, resource_id, version, actor_user_id, change_kind, patch JSONB, request_id, created_at with UNIQUE (resource_id, version). outbox_event holds aggregate_type, aggregate_id, aggregate_version, event_type, payload, status. A PUT carries the version the client read. Write the exact statements for the single transaction that applies the edit, records the revision and enqueues 'resource.updated', and give the handler's branch on zero affected rows. Then say what PostgreSQL 16 does under READ COMMITTED when two of these updates hit one row at once.
Approach
- One transaction, three writes, no network call inside it: UPDATE resource SET title = $3, version = version + 1, updated_at = now() WHERE resource_id = $1 AND tenant_id = $4 AND version = $2; then INSERT the resource_revision row at version $2 + 1; then INSERT the outbox_event row at the same aggregate_version. The event goes to a table rather than a broker because no transaction spans both.
- Branch on the affected-row count before doing anything else. Zero has three causes — stale version, wrong tenant, row gone — so re-read once and map to 409 carrying the current version, or 404 for an id outside the caller's tenant, which also stops the endpoint confirming that another tenant's id exists.
- State the engine behaviour instead of assuming it. Under READ COMMITTED the second UPDATE blocks on the row lock, and when the first commits PostgreSQL re-evaluates the WHERE clause against the newly committed row, so the version predicate now fails and the statement reports zero rows. Under REPEATABLE READ the identical collision raises SQLSTATE 40001 instead, so the handler must fold both shapes into one conflict response.
- Keep UNIQUE (resource_id, version) even though the predicate already serialises writers. It is what makes a lost update unwritable if any other path ever reaches the revision table, and it converts a logic bug into 23505 rather than into a silently missing history row.
Follow-up
- A client sends the version it read ten minutes ago and the resource has moved three versions. What is in your 409 so it can resolve the conflict without a full re-fetch?
- Two editors, two disjoint fields, no overlap. Does your answer still refuse the second write, and should it?
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
- 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.
- 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.
- 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.
- 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?
How do you balance the trade-offs between performance optimization and code maintainability?
Medium · Technical Domain Expertise
Approach
- Turn performance into a stated requirement first: a deadline, throughput or memory budget. Then profile (perf, gprof, or a cycle counter such as DWT CYCCNT on Cortex-M) to find the hot path. Code outside the hot path keeps the readable version.
- Go for algorithmic and data-layout wins before micro-tuning. Moving from O(n^2) to O(n log n), using contiguous arrays for cache locality and keeping allocation out of the loop usually leave the code readable. Unrolling, intrinsics and assembly are only for measured hotspots.
- Let the compiler do the work: inline functions instead of macros, constexpr and templates for abstractions with no runtime cost. Read the generated assembly before hand-tuning, because the 'optimisation' is often already there.
- Isolate any optimised routine behind a clean interface. Keep a simple reference implementation and test the fast one against it (differential tests), and comment why it exists with the measurement that justified it.
- Say where the line falls: on a hard real-time path a missed deadline is a failure, so performance wins there, and the target is bounded worst-case execution time, not average throughput. Avoid data-dependent branches, cache-miss variance and unbounded allocation on that path. Everywhere else maintainability wins. A benchmark in CI catches regressions either way.
Follow-up
- Describe an optimisation you rejected. What would it have bought, and why was it not worth the cost?
- How do you stop a hand-tuned routine from silently degrading when the next engineer edits it?
- A teammate wants to replace virtual calls in a tight loop with a switch on a type tag. How do you decide?
How do you approach the design of a modular software architecture that needs to integrate with legacy hardware
Medium · Technical Domain Expertise
How do you approach the design of a modular software architecture that needs to integrate with legacy hardware?
Approach
- Pin down the legacy interface before designing: physical link and protocol, register or frame layout, endianness, timing windows, error and reset behaviour, and undocumented quirks. Capture it in an interface specification and confirm it against the real unit, not only the manual.
- Layer the design: a driver or hardware-abstraction layer that owns every quirk of the device, an adapter that translates its frames into domain messages, and application logic that depends only on an abstract interface (ports and adapters, dependency inversion). Hardware-specific code then lives in one module.
- Design for testability: the abstract interface allows a device simulator or mock for unit tests and CI, while hardware-in-the-loop runs against the real unit cover timing and electrical behaviour that a simulator cannot reproduce.
- Make the boundary defensive: timeouts, bounded retries, CRC or checksum validation, explicit serialisation instead of memcpy into packed structs (alignment and endianness), and a defined degraded mode for when the device stops responding.
- Plan for evolution and cost: version the interface, select per-revision adapters by configuration or at compile time, and weigh the abstraction overhead on hard real-time paths (virtual dispatch against templates or static polymorphism).
Follow-up
- The legacy device's documentation is wrong or missing. How do you establish its real behaviour?
- How would you later replace the legacy hardware without touching the application layer?
- The device needs a response within a fixed window. Which layer owns that deadline, and how do you guarantee it?
Explain your process for debugging a complex system-level issue where the root cause is unclear.
Medium · Technical Domain Expertise
Approach
- Characterise before theorising: when it started, what changed (build, config, hardware revision, load), how often it fails, and under which conditions. If it is intermittent, measure the failure rate first so a fix can be judged against it.
- Collect evidence the system already gives you: timestamped logs, the reset-cause register, watchdog history, core dumps or fault registers (on Cortex-M the CFSR/HFSR plus stacked PC and LR). Check that the instrumentation is trustworthy before believing the symptom.
- Narrow by bisection: git bisect across builds, swap the hardware unit, disable subsystems one at a time. For timing faults use GPIO toggles on a logic analyser or hardware trace instead of printf, which changes timing and can hide the bug.
- Test one falsifiable hypothesis at a time against the usual system-level causes: stack overflow (check stack high-water marks), heap fragmentation in a long-running process, a race on shared data, priority inversion, DMA buffers that are not cache-coherent, uninitialised memory.
- Fix the root cause rather than the trigger, add a regression test (unit or hardware-in-the-loop), and soak it. Rule of three: if it failed about once per N hours, zero failures over about 3N hours gives roughly 95% confidence that the rate has dropped below that.
Follow-up
- The fault disappears when you add logging. What does that tell you, and what do you use instead?
- The unit resets in the field with no debugger attached. What would you have built in beforehand to diagnose it?
- How do you tell the actual cause apart from something that only correlates with the failure?
Suggested practice checklist
The seven days move from confirming the format, through real-time fundamentals, debugging, concurrency and architecture, to stories and a full dry run. Each day is tied to the five reported questions or to the stack candidates list.
Day 1 — Confirm the format and inventory your experience
- Send your recruiter questions on the stages, whether you will write code, and which team and location the role belongs to.
- List every project where you used C or C++, an RTOS, interrupts, DMA, networking (TCP/IP or UDP) or HIL testing.
- Mark the gaps against the reported topics: interrupt handling, memory management, kernel scheduling, DMA, cache coherency and bus contention.
Deliverable: A recruiter question list and a one-page table of topics with your experience and gaps.
Day 2 — Real-time and embedded fundamentals
- Write a cheat sheet covering interrupt rules (keep handlers short, defer work), priority inversion and inheritance, preemptive priority scheduling and jitter, and DMA with cache invalidate/clean and alignment.
- Outline an answer to 'How would you optimize a high-frequency data loop?': measure first, avoid allocation and branches in the loop, use fixed buffers or double buffering, and check cache behavior.
- Outline an answer to 'Describe how you handle race conditions in an embedded environment.' covering interrupt-versus-task sharing, disabling interrupts briefly, and lock-free hand-off.
Deliverable: A one-page cheat sheet plus two written answers to the example questions.
Day 3 — System-level debugging
- Write an eight-step debugging runbook for a fault with an unclear cause, from reproduction through bisection to a regression test.
- Walk through one real incident from your own work using the runbook and note where you checked your instrumentation.
Deliverable: A debugging runbook and one incident story with the trigger, cause and fix stated separately.
Day 4 — Thread safety and race conditions
- Write a single-producer single-consumer ring buffer in C or C++ using atomics and explain the memory ordering you chose.
- List the ways shared state can be protected (ownership, immutability, mutex, atomics, queues) with the failure each one avoids.
Deliverable: Working ring-buffer code plus a one-page thread-safety answer you can speak in a couple of minutes.
Day 5 — Architecture around legacy hardware and the performance trade-off
- Sketch a modular design: hardware abstraction layer, device driver interface, protocol layer, application logic, and a simulated device for tests and HIL.
- Add failure handling: what the system does when a subsystem stops answering (degrade, retry, reset) and how you would document it.
- Choose TCP or UDP for a telemetry stream and a command link between two subsystems, covering message framing, loss and reordering, latency and head-of-line blocking, and say what the application layer must add on top of UDP.
- Prepare a two-sided answer on performance versus maintainability with one real case for each side and the number that decided it.
Deliverable: An architecture diagram with notes on failure states and the TCP/UDP choice, and a written trade-off answer with two cases.
Day 6 — Behavioral stories
- Write the hardware or memory constraint story in STAR form with the budget, the measurement and the trade-off.
- Write a story about mentoring a junior developer and one about communicating progress on a long task, since candidates mention both. Add practice stories for a technical disagreement, conflicting deadlines and explaining a complex idea to a non-technical person.
Deliverable: A set of STAR stories, each with a number and your own role stated.
Day 7 — Full dry run
- Run a full mock on a timer you set yourself, answering all five reported questions aloud with assumptions stated first.
- If your strongest language is not C or C++, practice explaining how its concepts carry over.
- Write the questions you will ask: the team's stack, development culture and handling of technical debt.
Deliverable: A recorded mock, a list of weak answers to redo, and your questions for the interviewers.
Stories from hardware-constrained work
Candidates report one behavioral question about implementing a feature under strict hardware or memory constraints, and they mention mentoring junior developers and communicating progress. Strong stories here show a decision you made under a hardware, memory or reliability limit, with figures. The prompts after the first three are practice suggestions rather than reported questions.
- Describe a time you mentored a junior developer through a hard problem.
- Describe how you communicated progress on a long task to the people depending on it.
- Practice prompt: tell me about a technical disagreement with a teammate or hardware engineer and what evidence settled it.
- Practice prompt: describe a time you had to prioritise tasks with conflicting deadlines and what you deferred.
Describe a time you had to implement a feature with strict hardware or memory constraints.
Medium · Technical Domain Expertise
Approach
- Choose a feature where you made the design decisions and the limit was concrete. Open with the budget in numbers you actually had: bytes of RAM or flash, a cycle or latency budget, bus bandwidth, or a fixed buffer size. A story without a measurable constraint reads as a general optimisation story.
- Use STAR but keep Situation and Task to two sentences, and spend most of the answer on the options you weighed. Depending on your real project this could be static allocation or a pool instead of the heap, ring buffers, fixed-point instead of floating point, packed structures, lookup tables, or moving transfers to DMA. Describe only what you did.
- Show how you measured: map-file or size output, stack high-water marks, a cycle counter, a profiler or a logic analyzer, with a before and after figure. State how you confirmed the change was what fit the feature, not a side effect.
- Name the trade-off you accepted (flexibility, readability, a dropped option) and who agreed to it. If there was a disagreement, say what evidence settled it.
- Finish with the result in numbers and what keeps the limit from regressing, such as a size check in the build or a test on the target. The mistake that sinks this answer is a team story where you only watched, or a list of techniques with no figures.
Follow-up
- How did you decide the budget was the real limit, and what would you have done if the feature still did not fit?
- What did you give up to get there, and would you make the same call today?
- How did you keep the constraint from regressing after the change shipped?
Tell callers you do not own that their integration breaks
Medium · deprecation · compatibility · stakeholders
A field in a write endpoint's response must change shape. You own the endpoint; you do not own the four internal callers or the outbound webhook consumers who read it. Describe a deprecation you were responsible for: what you shipped first, how you established who was actually reading the field, the window you gave and what set its length, what you did about the consumer who never moved, and how you decided removal was safe. Name the signal you used, not the announcement you sent.
Approach
- Establish the reader set empirically rather than from a wiki of owners: per-field usage counters keyed by principal, or access logs attributed to a consumer. State the blind spot of whichever you pick, since a consumer that reads the field only on a monthly job will not appear in a week of logs.
- Ship additive first. Populate the new field alongside the old one so no reader is forced to move, which is also what keeps a rolling deploy safe, because old and new instances answer the same requests at the same time and a rollback must still find the old shape present.
- Set the window from the slowest legitimate consumer's release cadence, not from your calendar, and decide separately what to do for a consumer with no release process at all, such as an external webhook endpoint you can only email.
- Convert silence into evidence before you rely on it: a short, low-traffic removal window that makes a still-dependent consumer fail visibly and loudly while you are watching, rather than at three in the morning after you have moved on.
Follow-up
- How would you detect a consumer that reads the field only during a monthly export?
- One caller refuses to move and has a commercial relationship behind it. What changes in your plan and what does not?
Ship under a deadline and bound the debt you chose
Medium · pagination · technical debt · tradeoffs
You have four days to ship a tenant-facing listing endpoint. The version you would defend uses keyset pagination over (tenant_id, status, updated_at DESC, resource_id DESC); the version you can finish uses LIMIT/OFFSET with no matching index. Describe a deadline call you actually made of this shape: what you shipped, what you knowingly deferred, how you bounded the damage with a mechanism rather than an intention, and the specific numeric condition that would force the follow-up. Name who you told and where you wrote it down.
Approach
- Name the deferred failure precisely instead of calling it slow. OFFSET n makes the database produce and discard n rows, so cost grows with page depth; without an index matching the sort, every matching row is read and sorted before the limit applies; and rows inserted between two page fetches shift across the boundary so items are skipped or repeated with nothing in the response to signal it.
- Bound the blast radius with something mechanical rather than a promise: cap maximum page depth, cap page size, restrict the endpoint to one internal caller, or keep it behind a flag. State which failure each cap removes and which it leaves standing.
- Attach a number to the trigger and wire it to an alarm: the first tenant crossing N resources, or the endpoint's p99 crossing its share of the 400 ms budget, so the debt announces itself instead of waiting to be remembered.
- Write it where the next engineer looks, which is the code and the ticket, not a chat message: what was deferred, why, the cap, and the trigger.
Follow-up
- At what page depth does the offset version breach your latency budget, given your page size and row counts?
- What breaks first when you switch to keyset pagination later, and what does a client holding an old page token see?