Interview guides · ADTRAN
ADTRAN Software Engineer Interview Guide
ADTRAN Software Engineer interview guide: reported questions on memory, debugging and embedded experience, the C/C++ and Linux stack, and a 7-day plan.
The 60-second brief
ADTRAN works in telecommunications: network infrastructure that moves data for service providers and enterprises. Candidates describe Software Engineer work as spanning advanced technology modules and laboratory environments, where software is written for networking hardware and tested against it. Candidates describe the role as writing production code for that infrastructure, working with hardware engineers and product managers, and testing against hardware prototypes.
This guide is for Software Engineer candidates at ADTRAN. It covers the four questions candidates report (memory allocation in resource-constrained environments, debugging a complex system-level issue, keeping code maintainable and scalable as requirements change, and experience with real-time operating systems or embedded development), the stack candidates list for the role (C/C++, networking protocols and Linux/Unix, with embedded work, virtualization or containerization and CI/CD as extras), and a plan for working through all of it. No fixed round order is reported, so the guide is organised by topic rather than by stage.
Candidates report no named rounds for this role. The reported path runs from initial screenings to deeper technical evaluations that often include members of the team you would join, and most candidates finish within a few weeks. Confirm each stage with your recruiter.
Focus areas
- Software Engineering (General)
- Senior Software Engineering
- Technical Problem Solving
Overview
The day-to-day work candidates describe covers the full life of a software component: designing features, writing production code, taking part in code reviews, contributing to documentation and technical design reviews, and joining system integration and validation. Validation often happens in a laboratory, where software runs against real hardware prototypes. For preparation, that means C/C++ and Linux/Unix fundamentals, a working picture of networking protocols, and the ability to talk about code in terms of how it behaves on a device, not only in a unit test.
The four reported questions split cleanly. Three are technical conversations: how you manage memory in a resource-constrained environment, how you debug a complex system-level issue, and how you keep code maintainable and scalable as requirements evolve. The fourth asks about your experience with real-time operating systems or embedded software. Candidates also report that anything listed on a resume can be taken apart: if C++ is on yours, be ready to explain how it works underneath, including object lifetime, allocation and what the compiler and linker do with your code.
Candidates also describe scenario prompts such as speeding up an algorithm and weighing two communication protocols for a specific situation. Prepare for these by stating assumptions, naming the constraints (memory, latency, reliability of the link) and comparing options with a trade-off, rather than reciting a memorised answer. When you talk about past projects, say "I" and be exact about which part was yours.
Where to focus your preparation
No round names are reported for this role, so the guide treats the loop as one preparation stage that covers every reported topic. Ask your recruiter for the real sequence and format.
Preparation focus
No round list, order or format is reported, so treat this stage as everything you should be ready to discuss: the four reported questions, the C/C++, networking and Linux/Unix stack, and your past projects with the reasoning behind your choices. Ask the recruiter whether a stage involves writing code, drawing a design or discussing your resume, then weight the plan below accordingly.
What to demonstrate
- Be ready to show command of the stack candidates list: C/C++, networking protocols and Linux/Unix
- Be ready to talk about resource use, meaning memory, CPU and latency, on a real device
- Be ready to show a systematic way of debugging and of structuring code that has to keep changing
- Be ready to explain your own project decisions, constraints and trade-offs clearly
How to prepare
- Write a one-page answer on memory in constrained environments covering static allocation, fixed-size pools, stack depth and fragmentation, and be ready to sketch a block pool in C
- Write out a step-by-step debugging sequence and apply it to one real incident from your own work, with the evidence you collected at each step
- Prepare a diagram-led walkthrough of one past project: components, interfaces, what failed, and what you changed
- Prepare your embedded or real-time story, or an honest account of your closest experience, and ask the recruiter which stages involve live coding
Where candidates lose points
Answering the memory question with "avoid leaks and free what you allocate" and nothing more
Name the actual techniques: static allocation at start-up, fixed-size block pools with an O(1) free list, bounded stack depth, avoiding fragmentation from repeated variable-size allocation, a worst-case memory budget, and what the code does when an allocation fails.
Starting the debugging answer with a theory about the cause, or blaming hardware or software without evidence
Open with reproduction and what changed, collect logs, a backtrace or a packet capture, narrow the fault with a test that rules out half the candidates either way, and only then name a cause. Include how you tell the lab hardware apart from your code as a suspect.
Describing every project with "we", so your own contribution is invisible
Rehearse each story with "I" and name the module, interface or decision that was yours, while still crediting teammates for what they did.
Claiming RTOS or embedded experience in broad terms, then having no detail about scheduling, interrupts or memory when asked
Claim only what you can explain mechanically. If your closest experience is Linux user-space C/C++, say that and describe what you would learn first. Candidates report that being honest about gaps and explaining how you would find the answer is preferred to guessing.
Jumping into an optimisation or protocol comparison without asking about constraints, or leaning on a memorised algorithm
Ask about the limits first (memory, latency, link reliability, expected load), state your assumptions aloud, compare two options with an explicit trade-off, and explain why you would pick one for that scenario.
Questions & practice
Explain how you manage memory allocation in resource-constrained environments.
Medium · Technical Competency and Domain Knowledg
Approach
- Start with the constraints: how much RAM, whether there is an MMU, and whether the code runs bare-metal, on an RTOS or on embedded Linux. The answer changes with each. Then say the default you would choose: allocate everything at startup and use no general-purpose heap on the steady-state path, so a device with months of uptime cannot fail from fragmentation.
- For objects created at run time, such as packet buffers, sessions or timers, use fixed-size block pools. Allocation and free are O(1) pops and pushes on a free list. Pools have no external fragmentation, but they waste memory internally when an object is smaller than its block. Pick a few size classes from the measured object sizes.
- In C++, name the allocations that are easy to miss: std::vector and std::string growth (call reserve up front), std::function captures, exceptions, and iostreams. Use RAII with unique_ptr and a custom deleter that returns the block to its pool. A std::pmr::monotonic_buffer_resource over a fixed buffer (or a hand-rolled bump arena using placement new) works well for per-request scratch memory that is released all at once; remember that destructors are not run on release.
- Decide what happens when memory runs out before it does. Check every malloc or pool return. Choose a policy for exhaustion (drop the packet and count it, apply backpressure, or refuse the new session) rather than crashing. On Linux, overcommit means malloc can succeed and the OOM killer can still act later, so do not treat a non-NULL return as a guarantee.
- Measure instead of guessing. Use size or the linker map file for the text, data and bss footprint, fill stacks with a pattern to find their high-water marks, keep pool high-water counters, and run Valgrind or AddressSanitizer on host builds to catch leaks and use-after-free. Avoid recursion and large stack arrays on small task stacks.
Follow-up
- How would you size a packet-buffer pool, and what would you do when it runs dry under a traffic burst?
- Why is calling malloc from an interrupt handler a problem, and what would you do instead?
- A device leaks a few bytes an hour and fails after weeks in the field. How would you find the leak?
Merge partitioned event streams into one ordered feed with bounded lateness
Hard · k-way merge · watermarks · out-of-order streams
The read-model service consumes 64 log partitions carrying about 4,000 events per second in total. Each partition is ordered within itself, but partitions drift by up to 30 seconds, and the activity feed must present a tenant's events in occurred_at order. Produce the merge. State its complexity, the buffer it requires in events and in bytes, what happens when one partition is idle, and what you do with an event that arrives after you have already emitted its position. Payloads average 1 KB.
Approach
- Merge with a min-heap over the 64 partition heads keyed on (occurred_at, event_id): O(log P) per event and O(n log P) overall. The tie-break on event_id is what makes the output deterministic when two partitions carry the same millisecond, which matters because the feed is paginated and a non-deterministic order reorders pages under the reader.
- Emitting the heap head is only correct once every partition has produced everything up to that timestamp, so the emit condition is a watermark: the minimum across partitions of the highest occurred_at seen, less the allowed lateness. Events are held until the watermark passes them, which is what turns individually ordered streams into a jointly ordered one.
- Size the buffer from the lateness rather than guessing: 4,000 events per second times 30 seconds is 120,000 buffered events, and at 1 KB each about 120 MB of heap. That number is the real price of the ordering guarantee and belongs in front of whoever asked for it.
- Handle the idle partition explicitly, because it fails the feed rather than corrupting it: a partition with no traffic never advances its own maximum, so the watermark freezes and output stops entirely. Either every partition emits a periodic idle marker carrying the broker's current time, or the watermark falls back to wall clock for a partition silent beyond a threshold.
Follow-up
- The lateness budget is raised to five minutes. What is the new buffer, and what besides memory changes?
- The consumer restarts. Where does it resume from, and what does the feed look like for the first 30 seconds?
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
- 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.
- 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.
- 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.
- 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
- 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?
Keep soft-deleted accounts from blocking re-registration
Easy · soft delete · unique constraints · engine differences
app_user holds user_id, tenant_id, email CITEXT, password_hash (NULL for SSO principals), email_verified_at, auth_version, status ('invited','active','suspended','deactivated'), created_at, updated_at, deleted_at. Two live accounts for one address inside a tenant must be impossible, but an address freed by a soft delete must be reusable, and the same tenant may delete and re-register it repeatedly. Write the uniqueness DDL for PostgreSQL 16, then the equivalent for MySQL 8 where partial indexes do not exist, and say what each permits once three deleted rows already hold that address.
Approach
- Start from what is actually unique: not (tenant_id, email), but (tenant_id, email) among live rows. PostgreSQL says that directly — CREATE UNIQUE INDEX app_user_live_email ON app_user (tenant_id, email) WHERE deleted_at IS NULL. A full constraint over the same two columns burns the address permanently the first time someone deletes an account.
- Keep case-insensitivity in the type or the index, never in the application: CITEXT as given, or UNIQUE (tenant_id, lower(email)) as an expression index where the extension is unavailable. A case-sensitive unique column is exactly how two accounts for one human appear.
- For MySQL 8 the predicate has to move inside the key: add a discriminator column that is a constant 0 while the row is live and is set to user_id on delete, with UNIQUE (tenant_id, email, deleted_marker). Live rows share the constant and still collide; deleted rows differ from each other and stop colliding.
- State the NULL variant and its dependency: leaving the marker NULL for deleted rows also works, because a unique index treats NULLs as distinct — true in MySQL, and true in PostgreSQL only under the default NULLS DISTINCT, which PostgreSQL 15 lets you reverse. Check the polarity against the three existing deleted rows: constant-on-live is what preserves the collision you want, and reversing it silently admits duplicate live accounts.
Follow-up
- A deleted account re-registers with the same address the next day. Do the old resource rows follow the new user_id, and how does the API keep the two principals apart?
- How do you honour an erasure request while resource_revision.actor_user_id still references this table?
How do you ensure your code remains maintainable and scalable as project requirements evolve?
Medium · Technical Competency and Domain Knowledg
Approach
- Draw module boundaries around things that change for different reasons. Put hardware and protocol specifics behind narrow interfaces, such as a hardware abstraction layer or a protocol codec, so a new board variant or protocol version adds an implementation and leaves the callers untouched.
- Make change safe through tests and tooling. Inject dependencies so unit tests can swap in fakes for drivers and sockets, run integration tests against real hardware, and run clang-tidy or cppcheck plus sanitizer builds in CI. Code review then covers design, because the tools already catch the mechanical mistakes.
- Avoid speculative generality. Add an extension point when the second real use case arrives, not for imagined ones. Name the trade-off: early abstraction costs indirection and makes the code harder to read, while late abstraction costs a refactor, and the tests make that refactor cheap.
- Plan for compatibility when data crosses version boundaries. Version message and configuration formats, have readers skip unknown fields (TLV-style or protobuf-style field numbers), and gate new behaviour behind flags so devices in the field and newer software can coexist.
- For scale, measure before you optimize and choose structures for the expected growth. A linear scan over interfaces or sessions is O(n) per lookup, while a hash map gives O(1) on average. Use bounded queues so overload shows up as counted drops instead of unbounded memory growth.
Follow-up
- Walk through a refactor you led: what triggered it, and how you kept behaviour unchanged while you did it?
- How would you change a message format without breaking older devices that are already deployed?
- How do you decide whether to refactor now or ship and pay the cost later?
Design the bulk write endpoint a migration script retries blindly
Medium · batch api · partial failure · idempotency
A customer's migration script pushes 2 million resources through POST /v1/resources:batch, up to 500 items per call, and retries any call that errors or times out. Within a call some items fail validation, some collide with rows that already exist, and some succeed. Specify the request and response shape, whether a batch is atomic or per-item, how idempotency works for the call and for each item, the status code for a mixed outcome, the size and item-count limits with their error codes, and exactly what the script does after a timeout mid-batch.
Approach
- Choose atomicity deliberately and price it. All-or-nothing means one transaction holding locks for the whole batch on a primary already absorbing about 1.2k writes/second, which bounds batch size by lock duration, and it turns one bad row into 499 rejections the script must resend. Per-item partial success is the right default for a migration, and the contract's job is then to make a partial outcome impossible to miss.
- Require a client-supplied id on every item and echo it in every result. Deriving a per-item key from the array index breaks the first time the script resends a batch with the failures removed: the indices shift, previously-succeeded items acquire new keys, and they are created a second time.
- Key the effects at two levels. The call's Idempotency-Key covers an exact resend of the same bytes; per-item keys of (tenant_id, client_item_id) make a partially-applied batch safe to resend whole. Resending an identical batch must reproduce the same per-item results, not 500 conflicts the script has to interpret.
- Answer a mixed outcome with one status plus per-item detail: 200, or 207 borrowed from WebDAV if you prefer it - document whichever you pick - carrying an array of client_item_id, per-item status, and either resource_id or an error code from the same taxonomy the single-item endpoint uses. Reserve 4xx for the request as a whole: unparseable body, too many items, payload over the limit (413). Put a failed count at the top level so that even a script checking only the cheapest thing cannot conclude success while rows were dropped.
Follow-up
- At 500 items per call, what is the wall-clock time for 2 million rows, and what pacing do you publish so the migration does not become an incident?
- One item in every batch fails with the same code. How does the script discover that without a human reading logs?
Describe your process for debugging a complex system-level issue.
Medium · Technical Competency and Domain Knowledg
Approach
- Pin down the symptom before forming a theory. What exactly fails, how often, on which hardware or software versions, and what changed since it last worked (firmware, kernel, configuration, traffic profile)? Turn an intermittent report into a reproduction you can trigger, or at least a condition that makes it more frequent.
- Find the layer where good turns into bad. For a networking fault, capture packets with tcpdump or Wireshark on both sides to see whether traffic leaves the box at all. Then split application, kernel and driver, and hardware using strace for system calls, dmesg for driver errors, and perf for CPU or latency hot spots.
- Bisect. When there is a known-good build and a known-bad build, git bisect cuts the search to about log2(n) builds. Apply the same idea to configuration and inputs by removing half the variables each step.
- For crashes and corruption, enable core dumps (ulimit -c, or the core_pattern setting) and read the backtrace in gdb. Rebuild with AddressSanitizer for memory errors and ThreadSanitizer for data races. Heavy printf logging can change timing and hide a race, so log into an in-memory ring buffer and dump it on failure.
- Finish the job: show that the fix removes the failure under the original reproduction, add a regression test, run it on real hardware as well as in the development environment, and write up the root cause separately from the trigger.
Follow-up
- The bug shows up only on the lab hardware and never on your development machine. What do you try first?
- The failure happens about once a week in the field and you cannot attach a debugger. How do you collect enough evidence?
- How do you show that your change fixed the root cause and did not just change the timing?
Suggested practice checklist
The plan follows the four reported questions and the stack candidates list: C/C++ memory, debugging, maintainable design, speed and protocol trade-offs, then your embedded story and a full mock. Days that practise a reported question carry it.
Day 1 — Confirm the format and audit your resume against the stack
- Message your recruiter to ask for the sequence of stages and whether any stage involves writing code or drawing a design.
- List every language and tool on your resume and write two sentences on how each works underneath, starting with C/C++ and Linux.
- Mark which of the nice-to-have areas you can speak to: embedded, virtualization or containerization, CI/CD.
Deliverable: A sent recruiter message plus a one-page resume audit listing your mechanics notes for each language and tool.
Day 2 — Memory in resource-constrained environments
- Write a one-page answer on static versus dynamic allocation, fixed-size pools, stack budgeting and fragmentation.
- Implement a fixed-size block pool in C with a free list so allocate and free are O(1), and write down what happens when it is exhausted.
- Say your answer aloud once and note where you stalled.
Deliverable: A working block pool, its exhaustion behaviour in writing, and the one-page memory answer.
Day 3 — Debugging a complex system-level issue
- Write a debugging sequence: reproduce, establish what changed, gather logs or a backtrace or a packet capture, bisect, confirm the cause, then prevent a repeat.
- Pick one real incident from your career and write it as a timeline showing the evidence behind each step.
- Add the lab-specific branch: how you decide whether a fault sits in your software or in the prototype hardware.
Deliverable: A one-page debugging playbook and one incident timeline written from your own experience.
Day 4 — Maintainable design and networking fundamentals
- Take a module you built and write how it changed as requirements changed: its boundaries, its tests and what you would restructure.
- Write a TCP versus UDP comparison for telemetry versus configuration traffic: retransmission, head-of-line blocking, ordering, congestion control. Review Linux sockets in C (blocking vs non-blocking, epoll, network byte order, partial reads) and Ethernet/VLAN/IP layering.
- Work through the practice question on a bulk write endpoint that a script retries blindly, and write down the constraints you would ask about first.
Deliverable: A module-evolution note, a TCP/UDP and sockets note, and a solved design practice question with its clarifying questions listed.
Day 5 — Algorithms and speed
- Solve the practice question on merging partitioned event streams, stating time and space complexity.
- Solve the practice question on archiving a resource graph without recursion, and explain how you avoided stack growth.
- For each solution, write one way you would make it faster and what that costs in memory or clarity.
Deliverable: Two working solutions with complexity notes and an optimisation trade-off for each.
Day 6 — Your embedded story and ownership stories
- Write the answer to the real-time and embedded experience question: scope, one decision, how you verified it on the target, and what you would change.
- If your experience is thin, write the honest version using your closest work and how you would close the gap.
- Rehearse the practice questions on shipping under a deadline and on arguing against a design, speaking in first person singular.
Deliverable: A written embedded-experience story and two rehearsed practice stories, each with a named personal decision.
Day 7 — Full dry run
- Answer all four reported questions aloud in one sitting, then walk through one past project with a diagram.
- Revisit the answers where you hesitated and tighten them.
- Write two questions for your interviewers about the product, its testing in the lab and network stability.
Deliverable: A completed spoken run-through, a project diagram and two written questions to ask.
Embedded experience and ownership stories
The reported behavioral question is about your experience with real-time operating systems or embedded software. Prepare stories where you made a technical decision, can say why, and can show how you verified it. Candidates describe the role as involving hardware engineers and prototype testing, so a decision you checked on real hardware is a natural example. Candidates also advise saying "I" and being honest about gaps.
- Tell me about a system-level bug you traced to its root cause. What did you do, and what evidence did you use?
- Describe a time you had to explain a technical constraint to a product manager or another non-technical colleague.
- Tell me about a time you did not know the answer to a technical problem. How did you find it?
- Describe a code review where you changed your mind or changed someone else's.
What is your experience with real-time operating systems or embedded software development?
Medium · Technical Competency and Domain Knowledg
Approach
- Open with scope in two sentences: the kind of device, whether it ran an RTOS, bare-metal code or embedded Linux, your language, and exactly what you owned. Then say what made it real-time, such as a response deadline, an interrupt handler or a fixed task schedule.
- Pick one decision you made and explain the reasoning. For example: what ran in interrupt context versus a task, how you assigned task priorities, how you avoided priority inversion with a mutex that supports priority inheritance, or how you budgeted RAM and flash and what you cut.
- Say how you verified it on the target, not only on a host build: measured worst-case latency, stack high-water marks, behaviour under load, a bug you could only reproduce on hardware. Name the tool or measurement you used.
- If your experience is limited, say so plainly and give the closest real example, such as memory-constrained C/C++ on Linux, then describe how you would close the gap. Claim only what you can explain mechanically, since a broad claim you cannot back up with detail weakens the answer.
- Keep the story in first person singular. A list of technologies is weaker than one decision you personally made and can defend.
Follow-up
- What was the hardest timing or memory bug you hit there, and how did you find it?
- How did you decide what to run in an interrupt handler versus a task?
- What would you change about that design if the memory budget were cut in half?
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?
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
- 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.
- 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.
- 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.
- 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?