Interview guides · Ericson Manufacturing Company
Ericson Manufacturing Company Software Engineer Interview Questions & Guide 2026
Ericson Manufacturing Company software engineer prep: algorithm drills where every complexity claim has to name the assumption under it.
The 60-second brief
A Software Engineer at Ericson Manufacturing Company plays a vital role in bridging the gap between sophisticated industrial requirements and robust technical execution. You will be responsible for designing, developing, and maintaining the software systems that underpin the company's manufacturing operations. Your work directly affects the efficiency and reliability of its production environments and keeps its technical infrastructure in step with modern industrial demands.
The loop does not sample the job evenly, and arguing about that in the room costs you. Daily work is mostly incremental change inside code someone else wrote, while the loop samples narrow slices of it; prepare for the slices and save the realism argument for your questions at the end.
Focus areas
- Model units, lots and state machines precisely
- Fold late, out-of-order scan events by event time
- Make every external side effect safe to retry
Overview
This position is ideal for engineers who enjoy working at the intersection of core computer science principles and practical, real-world application. You will be challenged to solve complex problems that require both deep technical proficiency and an ability to think clearly under pressure. Whether you are optimizing database performance or architecting software solutions, your contributions will be central to the continued operational excellence of Ericson Manufacturing Company.
The environment at Ericson Manufacturing Company values logical precision and a structured approach to problem-solving. Be prepared to articulate your thought process clearly during all stages of the interview.
The Ericson Manufacturing Company interview process
Ericson Manufacturing Company candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Initial Aptitude Test
Most of the time lost in this format is not lost to thinking. It goes to a standard-library call you half-remember, an off-by-one in a loop bound, and a debugging loop that mutates code at random until something passes. When output is wrong, stop re-reading the whole function: take the smallest input that reproduces it and walk the state through by hand, printing intermediates if the environment allows. Guessing at a fix without a failing case you understand is how a five-minute bug becomes twenty, and the clock does not pause while you do it.
What to demonstrate
- Whether you reach the right structure without a detour, and can write it from memory rather than only recall that one exists
- Whether overflow is considered where the language has fixed-width integers, since a signed 32-bit value stops at 2,147,483,647 and then wraps in Java, is undefined behaviour in C++, and does not arise in Python, whose integers grow instead
- Whether recursion depth is treated as a constraint on large inputs, given that CPython's default limit is 1000 frames and a deep recursion can exhaust the stack in any language where an iterative version would not
- Whether a failing case is isolated and explained before any edit is made to the code
How to prepare
- From an empty file and with no references open, implement the pieces you lean on most: a heap push and pop, an iterative DFS with an explicit stack, and a binary search whose midpoint is written lo + (hi - lo) / 2, which avoids the overflow that (lo + hi) / 2 can hit in a fixed-width integer type
- Time yourself on the ten library calls you look up most, such as sorting with a custom comparator, splitting and joining strings, and finding the next key at or above a value in an ordered map, until the lookup is gone
- Take a solution you know is broken and, before touching it, write one sentence naming the input, the expected value and the actual value. Repeat until you do it without deciding to.
Technical Evaluations
Input bounds are the part of the prompt most often skimmed, and they usually contain the answer. They tell you which complexity class is admissible, which narrows the search before you have thought about the problem itself. As a rough planning figure, a compiled language does on the order of 10^8 simple operations per second and an interpreted one roughly an order of magnitude less. So n up to about twenty admits enumerating subsets, a few thousand admits a quadratic pass, and a million admits neither: you need near-linear, or linear with a log factor. If the bounds are missing, ask for them.
What to demonstrate
- Whether the approach is justified by the stated input size rather than by whichever pattern you recognised first
- Whether you ask about the properties that change the algorithm: whether the input arrives sorted, whether duplicates occur, whether values are bounded integers, whether it all fits in memory
- Whether you can name the bottleneck in your own solution and what would remove it, even when you deliberately leave it in place
- Whether a claimed speedup is real, since memoising a recursion only helps when subproblems genuinely overlap and the state can be keyed cheaply
How to prepare
- For each algorithm you rely on, write down the largest n it handles in roughly a second, then check two of those figures by timing them in the language you will actually type in
- For two weeks, write one line naming your target complexity and the bound that justifies it before you write any code, then compare that line with what you ended up submitting
- Practise the conversion backwards: given a required O(n log n), list the mechanisms that get you there (sorting, a heap, an ordered map, divide and conquer) and choose by what the problem needs to query, not by what you used last
Managerial Conversations
When a round has no standard shape, it is often there because something is still open: an area no earlier conversation reached, a round where the signal came out mixed, or a decision someone is not ready to make alone. Work out which by going back over what each earlier round actually covered rather than how it felt, and arrive able to give evidence on that point without being asked twice. Weak answers replay the loop's earlier material at the same depth. Strong ones go a level deeper and stay consistent with what you already said.
What to demonstrate
- Whether your account of a project matches the one you gave earlier in the loop, since what you said before may be available to whoever runs this round
- Whether you can go a level deeper on something already covered, reaching the decision and its alternatives rather than repeating the summary
- Whether you state your own uncertainty accurately, including parts of a system you did not build and decisions you inherited, instead of claiming even ownership across all of it
- Whether you can answer a question you handled poorly earlier by naming what you missed, rather than delivering a polished second version as if the first had not happened
How to prepare
- Reconstruct the loop on one page: for each round, the questions you were asked and the answer you actually gave, not the better one you thought of afterwards. The gaps on that page are your best available guess at why this round exists.
- Take the two claims you made earlier that carry the most weight and assemble the backing for each: the measurement, the date, what broke, the decision you would make differently now.
- Write down the three facts about your work that must not drift between tellings, such as team size, timeline and your own role, and check your stories against that list rather than trusting recall under pressure
Where candidates lose points
Judging delivery windows and cut-offs in UTC
A delivery appointment, a receiving cut-off and a same-day promise are all statements about local wall-clock time at a specific stop, so evaluating them in UTC misclassifies on-time performance for any leg that crosses a zone, and misses cut-offs by an hour on the two days a year a region changes offset. The subtlety that catches experienced engineers is that converting a future local time to a UTC instant at write time bakes in the offset rules as they were known then: when a jurisdiction changes its rules, an appointment stored that way silently moves, which is why a future window is better stored as local wall time plus an IANA zone identifier and resolved at read time, while a past event is correctly stored as an instant. Duration arithmetic needs the same care, because a transit that spans a DST boundary is genuinely 23 or 25 hours long in local terms even though the elapsed instant count is unchanged. Storing only a date with no zone - common on partner feeds - is the version of this that cannot be fixed downstream, so the zone has to be captured at ingestion.
Holding a database transaction open across a physical or third-party operation
It is natural to open a transaction, lock the position, call the rating or tendering API, and commit on the response, and it works perfectly until the partner's p99 goes from 200 milliseconds to thirty seconds. At that point every request holding a lock on a hot item-node pair queues behind it, the connection pool fills with transactions that are waiting on the network rather than on the database, and an unrelated service sharing the pool fails at the same moment. In this domain the effect is amplified because demand concentrates on a few hot rows during a promotion or a seasonal peak, exactly when partner latency is also degraded. The structural fix is to keep transactions short and local - commit the state change together with an outbox row, let a relay perform the external call, and reconcile asynchronously - accepting at-least-once delivery and making the effect idempotent rather than trying to stretch a transaction over something the database cannot roll back.
Not asking what the system looks like if it dies halfway through
For any multi-step write, say what state remains if the process stops between step two and step three, and what brings it back: a single transaction, a saga with compensating actions, an outbox, or a reconciliation job. Partial failure is routine at any real call volume, so 'that shouldn't happen' is an answer with nothing behind it.
Choosing a schema before the access patterns are known
Write the queries first, with their filters, sort orders, cardinalities and which ones sit on the latency-critical path, then design tables and indexes to serve them. An index nothing queries still costs write throughput and storage, and a hot query with no supporting index becomes a full scan that only hurts once the table is big.
Questions & practice
Find the busiest ten-minute window per reader after deduplication
Medium · sliding window · event time · deduplication
One day of observation_event from fixed readers: source_id (reader), subject_id (container), occurred_at, received_at, dedupe_key. 300 million rows, partner and device replays resend whole windows, and some events arrive six hours after they occurred. For each reader, return the 10-minute event-time window containing the most distinct containers, with the window's start. Target linear work per reader after sorting. State the window semantics you chose, and what the job does with an event that lands after its window was already reported.
Approach
- Deduplicate before windowing, on
dedupe_key, and keep the two concerns separate. A replayed batch is a no-op only if the duplicate is removed first; deduplicating inside the window logic counts a resend as traffic and reports a congestion peak that never happened. Dedupe is a hash set per reader-day or a sort-unique on the key — the same sort you need anyway. - Partition by
source_idand sort each partition byoccurred_at, never byreceived_at. Arrival order is an artefact of device connectivity and partner batch cadence; a reader that went offline for six hours would otherwise show its entire backlog as one impossible burst. Sorting dominates: O(p log p) per reader of size p, O(n log n) overall. - Two pointers over the sorted partition with a hash multiset
container -> countand a separatedistinctinteger. Advance the right pointer, incrementing the count and bumpingdistinctonly on a 0-to-1 transition; advance the left pointer whileoccurred_at[left] <= occurred_at[right] - 600s, decrementing and loweringdistinctonly on a 1-to-0 transition, and erasing the key from the map so the map's size anddistinctnever diverge. Each element enters and leaves once: O(p) amortised after the sort, O(window occupancy) space. - Justify the candidate set rather than scanning every possible start. With half-open windows
[t, t+600), sliding a window right until its left edge coincides with an event can only add events and never removes one, so the maximum is attained at some event'soccurred_at; checking the p windows anchored at events is sufficient and exhaustive. State the half-open convention explicitly — an event exactly att+600belongs to the next window, and flip-flopping on that produces off-by-one disagreements between this job and whatever reads its output. - Handle lateness by choosing a policy, not by hoping. Either run over a closed event-time batch with a stated lateness allowance — the day plus six hours, so the six-hour tail is inside the batch and the day-late tail is not — or emit under a watermark and restate the affected windows when a late event lands. Both are defensible; what is not defensible is silently dropping the late event, because that is how the busiest window quietly becomes the one with the best connectivity.
Follow-up
- Two events for one container arrive from two readers a second apart. Is that congestion, a misread, or one container passing two portals?
- You need this continuously rather than as a daily batch. What changes, and where does the memory go?
- A device's clock is three hours fast. What does that do to this result, and how would you detect it from
clock_offset_ms?
Find the hottest write keys under a fixed memory budget
Medium · top-k · heap · streaming sketches
One day of writes to inventory_movement is 200 million rows; each carries (item_id, node_id). Distinct pairs reach 60 million. Return the 200 pairs with the most writes so they can be moved behind a single writer before the next peak. First give the exact answer assuming you may hold 60 million counters, then give an answer under a 200 MB budget, and state the error guarantee that second answer carries. Both must be one pass over the day's rows unless you argue for a second.
Approach
- Exact version: one hash pass to build
(item_id, node_id) -> int64, then a single scan of the map maintaining a min-heap of size K = 200, pushing and popping when the incoming count exceeds the heap root. O(n + d log K) with n = 2e8 and d = 6e7, versus O(d log d) if you sort all 60 million entries — a factor of roughly log d / log K, about 3.4x, for no benefit. The heap must be a min-heap: the root is the weakest survivor, so eviction is O(log K). - Cost the map honestly. 60 million entries at a 16-byte key, an int64 count and per-entry overhead is 2-3 GB in most runtimes, which is the whole reason for the second version rather than a theoretical aside.
- Bounded version: Misra-Gries with m counters. Keep at most m keys with counts; on a key already present increment it; on a new key when fewer than m are held, insert at 1; otherwise decrement every held counter and drop those reaching zero. With m = 1,000,000 at roughly 40 bytes per counter, that is about 40 MB, comfortably inside 200 MB alongside the heap.
- State the guarantee as a number, not as 'approximate'. Misra-Gries returns an estimate satisfying f - n/(m+1) <= f_hat <= f, so with n = 2e8 and m = 1e6 every count is an undercount by at most 200. If the 200th hottest pair has hundreds of thousands of writes, a 200-count error cannot reorder the top of the list — and if the counts near rank 200 are within 200 of each other, the ranking there was never meaningful for the decision being made. The sketch also merges: run one per shard, combine, prune back to m, and the bound holds with n the combined length.
- If exactness is genuinely required, argue for two passes rather than more memory: the Misra-Gries pass yields a candidate superset of size m, and a second pass counts only those candidates exactly in O(m) memory. That is the standard way to get an exact top-K under a memory bound, and the cost is one extra read of the day's rows.
Worked solution (25 min)
- Implement the exact path first with a hash map and a size-200 min-heap, and record its peak resident memory on a scaled-down input.
- Implement Misra-Gries with an explicit decrement step, and assert after every insert that the held-counter count never exceeds m.
- Generate a synthetic stream with a known Zipf-ish head: 200 planted heavy keys plus a long tail, so the true top-K is known.
- Run both, diff the two top-200 lists, and measure the largest per-key undercount against the n/(m+1) bound.
- Shard the stream into eight parts, run eight sketches, merge and prune, and confirm the merged result still respects the bound.
Expected result: Both paths return the same 200 keys on the planted input; every Misra-Gries estimate is at or below the true count and never short by more than n/(m+1); the merged eight-shard sketch matches the single-stream sketch's guarantee.
Verify it yourself
- No estimate ever exceeds the true count — an overestimate means the decrement step is wrong.
- Peak memory of the bounded path is flat as the number of distinct keys grows, while the exact path's grows linearly.
- Shuffling the stream changes individual estimates but never violates the bound.
Follow-up
- The decision downstream is which keys get a single writer. Does a rank error at position 190 change that decision, and what does the answer tell you about how much accuracy to buy?
- Write volume on one key is bursty — a promotion concentrates it into twenty minutes. Does a daily top-K find it, and what window would?
- How do you keep the sketch across a worker restart without replaying the day?
Trace a suspect lot forward to every shipped order line
Medium · graph traversal · genealogy · indexing
lot_edge(parent_lot_id, child_lot_id) has 400 million rows with btree indexes on both (parent_lot_id, child_lot_id) and (child_lot_id, parent_lot_id). lot_shipment(lot_id, order_line_id) maps lots to shipped lines. Given one suspect received lot, return every downstream order_line_id. The reachable subgraph can reach 2 million lots and 12 levels; the full edge table does not fit in memory. Give the traversal, its complexity in terms of the reachable subgraph, and what you do if the data contains a cycle.
Approach
- Genealogy is a directed acyclic graph, not a tree: a production run consumes several lots and one lot splits across many shipments, so a node is reached by several paths and a tree traversal revisits it combinatorially. Breadth-first from the suspect lot over the
(parent_lot_id, ...)index, with a hash set of visited lot ids, is O(V' + E') where V' and E' count only the reachable subgraph — 2 million 8-byte ids is 16 MB of payload and a few tens of MB with open-addressing overhead, which is why the visited set is affordable and the edge table's 400 million rows never enter memory. - Batch each level into one indexed read. Issue
WHERE parent_lot_id = ANY($1)with a few thousand parents per call instead of a query per node: at 12 levels and 2 million nodes, per-node round trips dominate the wall clock entirely, and the recall's deadline is a real-world response time, not a query-plan aesthetic. The batch size is the tuning knob between round-trip count and per-statement planning cost. - Keep the visited set even though the schema says DAG. Rework and repack loops — material returned into an earlier lot — do occur, and an unguarded traversal on a cyclic edge set does not terminate; a set membership test is O(1) and turns a hang into a correct answer plus a data-quality alert. Also cap the traversal by level and by node count, and fail loudly on the cap rather than returning a partial set silently.
- Join to
lot_shipmentonce at the end over every visited lot id, not over the leaves and not per node. A lot is routinely both shipped and consumed: half a lot goes out on an order line while the other half is repacked into a child lot, so it has alot_shipmentrow and outgoing edges at the same time and is not a leaf. Restricting the join to nodes with no outgoing edges drops exactly those order lines, which is under-recall — the one failure mode this trace is not allowed to have. One bulk join against an index onlot_idover the whole visited set is a single pass; per-node lookups multiply the round-trip problem by the number of visited lots. - State the semantics honestly: reachability answers which shipped lines could contain material from the suspect lot, and without propagating edge quantities it is a superset of physical containment. For a recall that is the correct direction of error — over-recall, never under-recall — and saying so is the point, because a trace that returns quickly and incompletely will be believed. The upstream question, which received lots are in a suspect shipped unit, is the same traversal over the reverse index, which is why both indexes exist.
Follow-up
- A recursive CTE would express this in ten lines. What is its cost here if the index on the traversal column is missing, and how would you notice?
- You need the quantity of suspect material in each downstream line, not just reachability. What has to be on the edge, and what does that do to the traversal?
- The traversal must run while a production run is writing new edges. What does a consistent answer mean here, and what do you cut it at?
Fold late, out-of-order scan events into one status per leg
Medium · window functions · event time · monotonic fold
observation_event holds event_id, subject_type, subject_id, observed_status, occurred_at, received_at, dedupe_key (UNIQUE) and processing_status. A lookup table maps observed_status to status_rank SMALLINT along a progress lattice. Write one query returning the current status of every shipment leg that received an event in the last hour, such that a picked_up event arriving after a delivered event never changes the answer. Then explain exactly which part of your ordering makes a shuffled replay of the same batch produce identical output.
Approach
- Separate the two jobs. Deduplication is the UNIQUE on dedupe_key at ingest, because partner feeds resend whole windows; ordering is the fold. Either alone leaves a defect: dedupe without a lattice still lets a stale event win, and a lattice without dedupe double-counts a replayed window in anything that aggregates.
- Select the affected legs in one CTE by received_at >= now() - interval '1 hour', then fold over every event for those legs, not only the recent ones. Restricting the fold's input to the recent window is the failure: a lone late picked_up would be the only row considered and the leg would regress.
- Fold as a maximum over the lattice: SELECT DISTINCT ON (subject_id) subject_id, observed_status FROM ... ORDER BY subject_id, status_rank DESC, occurred_at DESC, event_id DESC. The equivalent form is ROW_NUMBER() OVER (PARTITION BY subject_id ORDER BY status_rank DESC, occurred_at DESC, event_id DESC) = 1.
- The determinism comes from the final event_id tiebreak. Rank and occurred_at alone leave two equal-rank events at the same instant to be resolved by heap order, which changes when rows are re-inserted, so the shuffled replay differs in exactly the cases nobody tests. With a total order over the tuple the fold is a max, which is commutative and associative and therefore order-insensitive.
- Do not advance a leg on occurred_at alone. The timestamp is device-reported and can be wrong, which is why the lattice rank leads the ORDER BY and why clock_offset_ms is worth storing: a future-dated event with a low rank then cannot promote itself past a delivered.
- Keep superseded rows and mark processing_status = 'superseded' rather than deleting them. The discarded event is usually the one that explains a delivery dispute, and deleting it makes the trace unanswerable.
Worked solution (30 min)
- Build a fixture for one leg with picked_up at T0, in_transit at T1, delivered at T2, then insert the picked_up row last so arrival order contradicts event order.
- Write the CTE selecting affected subject_ids by received_at, then the DISTINCT ON fold joined to the rank lookup.
- Run it and confirm delivered is returned, then shuffle the insert order of the same batch into a fresh table and re-run.
- Add a second delivered event at the identical occurred_at from a different source and confirm the event_id tiebreak still picks one deterministically.
Expected result: One row per leg carrying the highest-ranked status ever observed, identical across every insert ordering of the same batch, with the late picked_up present in the table and marked superseded rather than absent.
Verify it yourself
- Replay the batch in five random orders and diff the result sets; they must be byte-identical.
- Remove the event_id tiebreak and confirm the equal-timestamp case becomes unstable across replays.
- Restrict the fold input to received_at in the last hour and confirm the leg regresses to picked_up, reproducing the trap.
- Confirm every input event is still present with a processing_status, so no evidence was discarded.
Follow-up
- An event arrives a week late for a leg that is already invoiced. Does the fold apply it, reject it as late, or apply it and emit a correction?
- The stream is partitioned by source_id rather than by subject_id. What ordering guarantee do you actually have, and what breaks?
- How would you materialise this fold so the customer-facing status read is not running a window function per request?
Stop two allocators promising the same unit of stock
Medium · isolation levels · lock contention · compare-and-set
inventory_position is keyed (item_id, node_id, lot_id, state) and holds qty BIGINT, which is on-hand stock and is moved only by the movement ledger; available_qty BIGINT, maintained on the same row; and version. The declared invariant is available_qty = qty - SUM(allocation.qty) over that key in status held or committed, enforced by CHECK (available_qty >= 0) and CHECK (available_qty <= qty). allocation holds allocation_id, order_line_id, item_id, node_id, lot_id, qty, tier, status (held, committed, consumed, released, expired) and expires_at. Twelve allocator threads try to claim the last eight units of one item at one node inside the same 50 ms. Give the exact statements that let at most eight units be claimed, then say what changes if identical code runs under PostgreSQL REPEATABLE READ versus InnoDB REPEATABLE READ, and what a retried allocate for the same order line must do.
Approach
- Collapse the check and the write into one statement whose WHERE clause is the business rule: UPDATE inventory_position SET available_qty = available_qty - :q, version = version + 1 WHERE item_id = :i AND node_id = :n AND lot_id = :l AND state = 'on_hand' AND available_qty >= :q. qty is deliberately untouched: a hold is a promise, not a physical move, so only a movement may change on-hand and the ledger-equals-position invariant survives. Rowcount zero is a refusal, not an error, and the allocation row is inserted in the same transaction only when the rowcount is one.
- If the deployment carries no available_qty column and availability must be derived, the alternative is SELECT ... FOR UPDATE on the position row followed by the SUM over held and committed allocations inside the same transaction. That is correct but holds the row lock for two statements instead of one, which matters on a key taking hundreds of writes a second.
- Isolation is not a substitute for the atomic step, and it changes the error contract. Under PostgreSQL READ COMMITTED the losing UPDATE blocks, then re-evaluates its predicate against the newly committed row version and returns rowcount 0, which the handler already handles. Under PostgreSQL REPEATABLE READ the same statement instead raises SQLSTATE 40001 and the whole transaction must be retried, so a handler that only tests rowcount fails closed on an exception it never expected. InnoDB REPEATABLE READ does neither: a locking read or UPDATE performs a current read of the latest committed row, so the loser blocks and then sees rowcount 0, with a deadlock rather than a serialization failure as its exceptional case.
- Make the retry a no-op with a partial unique index: UNIQUE (order_line_id, node_id, lot_id) WHERE status IN ('held','committed'). Then a retried allocate conflicts instead of double-claiming, and the handler reads the existing allocation and returns it.
- Never source the availability number from a replica or a cache for this path. Replication lag is largest during the write burst that made the key hot, so the guarantee fails precisely when it is load-bearing.
- For the hottest item-node pairs, shed load without overselling: route allocations for that key through a single writer with a bounded queue and refuse with a conservative not-available when the queue is full. Splitting the position into N sub-rows cuts contention N-fold but strands the remainder across sub-rows and produces false out-of-stock when the last units are spread thin.
Follow-up
- One order line needs two units from two different nodes, atomically. What do you do when those positions live in different shards and no distributed transaction is available?
- Soft-tier allocations expire. Show the sweeper statement, and say what stops it from releasing an allocation that is being committed at that instant.
- Under sustained contention your CAS retries. What is the retry budget, and what does the caller see when it is exhausted?
Solve a pattern recognition puzzle within a strict time limit.
Medium
Approach
- Clarify what is being asked and what a complete answer contains.
- Work from the requirement backwards to the design.
- State your assumptions explicitly before working the problem.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Describe your approach to solving complex arithmetic problems under pressure.
Medium
Approach
- Say what you would check first and why it is the highest-information step.
- Work from the requirement backwards to the design.
- State your assumptions explicitly before working the problem.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
How would you structure a logical argument regarding the societal impact of social media?
Medium
Approach
- State your assumptions explicitly before working the problem.
- Say what you would check first and why it is the highest-information step.
- Clarify what is being asked and what a complete answer contains.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Fold late, duplicated scan events into leg status
Medium · event time · partitioning · deduplication · late data
Observations arrive from handhelds, fixed readers and partner feeds: 9,000 per second average, 40,000 at peak. Partner feeds resend whole windows nightly and offline devices deliver six hours late. Each event carries dedupe_key, subject_id, observed_status, occurred_at and received_at. Design ingestion and the fold that maintains shipment_leg.status: the stream partition key, the deduplication mechanism, the ordering rule, and the handling of an event that is a week late or stamped in the future. Size observation_event's storage and partitioning.
Approach
- Separate the two defences, because each alone leaves a defect. Deduplicate on dedupe_key with a unique constraint or a keyed store, then fold with a monotonic status lattice. Deduplication alone still lets a stale picked_up overwrite delivered; monotonic folding alone still double-counts a resent window in any per-event aggregate such as dwell or scan productivity.
- Partition the stream by subject_id, the leg, so every event for one leg lands in one partition and is handled by one consumer. Broker ordering is per-partition only, so keying by source or device puts two events for one leg on different partitions to be processed concurrently, and the lattice would then be applied by two writers racing. Keying by leg spreads well because a single leg's event rate is physically bounded by scanning; the residual risk is a partition carrying several thousand legs of one large load, which is a throughput problem to absorb, not an ordering one.
- Write the fold as: order candidates by occurred_at, compare status_rank, take the maximum, never assign by arrival. Keep superseded events with processing_status superseded instead of discarding them, because the discarded event is usually the one that explains a dispute. Since occurred_at is device-reported and sometimes wrong, the rank lattice plus clock_offset_ms is what makes the rule safe - an event whose occurred_at is in the future beyond tolerance still folds on its rank but is flagged and not trusted for timing.
- Set the lateness policy in hours, not seconds. Hold a per-partition watermark at received_at minus the allowance; events behind it are still applied to the leg, because the lattice makes that safe, but any aggregate already published from them - on-time performance, dwell, carrier scorecards - must be restated rather than silently mutated, so a restatement path is part of the design. Beyond the retention horizon the event is stored with processing_status rejected_late and folded into nothing.
- Size the store from the average, not the peak: 9,000 per second is about 780 million rows per day, roughly 150 GB per day at 200 bytes. Daily range partitions on occurred_at give 30 partitions for a 30-day hot window rather than hundreds, which keeps planning time sane. A late event writes into an older partition, so partitions cannot be detached on age alone - they close only after the lateness allowance. Index (subject_id, occurred_at) for the fold and the trace; a block-range index on occurred_at degrades here because insert order stops correlating with event time.
Worked solution (35 min)
- Write the dedupe key and the store that enforces it, and set its retention against the partner's replay window.
- Write the status lattice as explicit ranks and the fold rule, then replay the same eight events in three different orders and confirm one identical final status.
- Push three events through the rule - six hours late, a week late, and occurred_at two days in the future - and name the processing_status each ends in.
- Compute rows per day, bytes per day, partition count at daily granularity, and state which query the chosen index actually serves.
Expected result: Deduplication on dedupe_key plus a fold taking the maximum status_rank ordered by occurred_at, keyed by leg so ordering holds within a partition; superseded events retained; a lateness allowance measured in hours with an explicit restatement path for published aggregates; and roughly 780 million rows and 150 GB per day into daily partitions on occurred_at that close only after the allowance, indexed on (subject_id, occurred_at).
Verify it yourself
- Replaying any permutation of the same event set yields the same leg status.
- Replaying an entire partner window changes no status and writes no duplicate row.
- A late event can raise a status but can never lower one.
- The partition key is justified against the ordering requirement, not merely against even spread.
Follow-up
- A partner replays a 24-hour window every night. What does that cost in the dedupe store, and how long must a dedupe key be retained to stay correct?
- On-time performance was published yesterday and a late pickup event arrives today. Restate it or leave it, and what does the consumer of that number need to know?
- Where does the fold's state live, and what happens when a consumer restarts in the middle of a partition?
Orchestrator memory climbs a gigabyte a day at flat throughput
Medium · memory leak · timers · heap analysis
The fulfilment orchestrator's resident memory rises about 900 MB a day and old-generation occupancy after each full collection rises monotonically; a restart resets both. Transition throughput and latency are unchanged, and saga state itself lives in the database. The process keeps an in-heap timer wheel with one timeout entry per forward step of each order line, and lines legitimately sit in one state for days. Diagnose in order, prove the hypothesis with arithmetic before changing code, and give the fix.
Approach
- Distinguish a leak from a large working set first, because the remedies are opposite. Plot occupancy immediately after each full collection, not resident memory: a monotone rise across days is retention, while a rise that plateaus is a cache doing its job. Live sagas legitimately number in the millions here, so the working set is genuinely large and the question is only whether it is bounded.
- Diff two heap dumps taken two hours apart by retained size per dominator rather than guessing at the suspect. The growth will show as timer entries that transitively pin whole saga objects; the reference path from the GC root through the wheel's bucket array to the entry to the saga is what names the owner, and without that path you are guessing.
- Prove the hypothesis numerically before touching code. 900 MB a day divided by roughly 1.5 KB retained per entry is about 600,000 entries a day, near seven a second. Compare that to entries created per second minus entries removed per second. If the rates do not match within an order of magnitude, the dominator is something else and the timer wheel is a bystander.
- Name the defect precisely: the entry is registered when a step starts and is only released when its timeout fires, so a step that completes early leaves an orphan that survives until its deadline, and a step whose line is cancelled or re-sourced leaves one that may never be cleaned at all. Age cannot be used to identify orphans, because a line sitting in one state for days is normal.
- Fix by cancelling on transition, keyed by (order_line_id, step), so the entry is removed by the same code path that advances the saga. Then move the timer out of the heap entirely: persist due_at on the saga row and let a sweeper poll WHERE due_at < now() AND status IN (open states) behind an index on due_at partial to those states. That bounds memory by the poll batch instead of by open sagas, and fixes the separate bug that an in-heap wheel silently loses every pending timeout on restart or deploy.
- Add a bound so the next version of this is loud. Cap the in-flight timer structure, emit a metric on entries created versus removed, and alert on the difference rather than on memory, which only moves after the damage is done.
Follow-up
- The sweeper now polls millions of open sagas. What does the index look like and how do you keep the poll from scanning the whole table?
- Two orchestrator instances poll the same due sagas. How do you stop both from firing the same timeout?
- How would you have detected the lost-timeouts-on-restart bug, given that it produced no error and no memory symptom?
Suggested practice checklist
For someone fluent in a dynamic language who has shipped real work but has never had to say what the runtime is doing underneath. The week is built on measuring and deliberately breaking things, because the questions that expose this background are the ones where the interviewer asks why a second time.
Day 1 — Measure before reasoning
- Take a slow piece of your own code, write down in advance where you believe the time goes, then profile it and record how wrong the guess was. The cost is usually an allocation you did not notice or an accidental quadratic membership test.
- Replace one list membership test inside a loop with a set and measure at a thousand, ten thousand and a hundred thousand elements, confirming the shape of the curve rather than only that it got faster.
- Write down the three quantities you can now measure instead of assert: wall time, peak memory, and call count for the function you suspected.
Deliverable: A before-and-after profile of real code plus a written note on the size of the gap between the guess and the measurement.
Day 2 — References, copies, and the bugs they produce
- Write the function with a mutable default argument, call it three times, and explain the accumulating result: the default is evaluated once when the function is defined, so every call shares one object.
- Build a nested structure, take a shallow copy, mutate an inner element, and show that both views changed, because a shallow copy duplicates the container and not the elements. Then fix it with a deep copy and state the cost you just accepted.
- Write two functions, one mutating its argument in place and one rebinding the local name, and predict the caller's view of each before running it. That single distinction produces most of the bugs that pass their tests.
Deliverable: Three small programs whose output you predicted correctly before running, each with a one-line statement of the rule underneath.
Day 3 — Types, once, in a language that checks them
- Port one module you have already written, roughly a hundred lines, into a statically typed language, and record every place the compiler demanded an answer your original had left implicit: a value that can be absent, a numeric width, a case never handled.
- Write the same signature in both languages and state what the static one guarantees before the program runs and what it does not, since it will not save you from a wrong algorithm or an index out of range.
- Write the difference between an interface satisfied by declaration and one satisfied structurally, with one case each where the other approach would miss the mistake.
Deliverable: One module in two languages plus a list of the questions the type checker forced you to answer.
Day 4 — Concurrency, starting with what actually runs at the same time
- Run the same CPU-bound function across four threads and four processes and measure both. Under the default CPython build the threaded version will not speed up, because only one thread executes bytecode at a time; the process version will. Check which build you are on first, since free-threaded builds remove that lock and change the result.
- Then run a blocking I/O workload across four threads and measure it speeding up, because the interpreter releases that lock around blocking calls, which is why treating threads as useless is wrong as a general claim.
- Build the lost update: two threads each incrementing a shared counter a hundred thousand times, and show a final value below the expected sum, because an increment is a load, an add and a store and the thread can be suspended between them. Fix it with a lock and then measure what the lock costs.
Deliverable: Three measurements, threads against processes on CPU work, threads on I/O work, and a demonstrated lost update, each with the mechanism written underneath.
Day 5 — Debugging as a procedure rather than an instinct
- Work one real failure as a bisection: find a revision or an input size where it is good and one where it is bad, halve repeatedly, and state the two assumptions bisection needs, that the property changes exactly once across the range and that the test is reliable.
- Minimise one failing input to the smallest version that still fails, and record how many rounds it took.
- Keep a hypothesis log for one bug in three columns, what I believe, what would disprove it, what I observed, and stop yourself the first time you are about to change two things at once.
Deliverable: One bug worked to root cause with a written hypothesis log and a minimised reproducing input.
Day 6 — Tests that catch the bug you are about to write
- Implement an LRU cache with a capacity bound, then write the three test cases that would catch an off-by-one in eviction: insert exactly capacity items and assert nothing was evicted, insert one more and assert the least recently used key is the one gone, and read an old key just before that insert so the eviction victim changes.
- Add a property test comparing your implementation against a deliberately slow reference, an ordered list scanned linearly, over a few thousand random operation sequences, because a slow reference finds the cases you would not have thought to write.
- Write one numeric test that fails under exact equality and passes with a tolerance, and state why the tolerance has to be relative rather than absolute once the magnitudes grow.
Deliverable: An LRU implementation with three boundary tests, one property test against a slow reference, and one tolerance-based numeric test.
Day 7 — Debug something broken, out loud
- Have someone plant three defects in a two-hundred-line program, an off-by-one, a shared mutable state bug, and a wrong error-handling path, then find them while narrating, under a fixed rule: state the hypothesis before touching anything.
- Time each one and record which tool found it, reading, a printed value, a debugger, or a test, because the question asked in interviews is how you would find it rather than what it was.
- Write the sentence you will use when you do not yet know the cause, one that names the next measurement instead of offering a guess.
Deliverable: A recorded debugging session with time-to-find per defect and the method that found each.
Know where your numbers came from
Bring the two or three numbers the story rests on and know how they were collected. A p99 whose timer starts inside your handler excludes the time a request spent queued, so it can sit flat while users wait longer. Give the window, the percentile and what the measurement left out, or drop the number.
What is your view on the ethical responsibilities of software engineers regarding modern technology?
Medium · behavioural and engineering judgement
Approach
- Name the disagreement and how you resolved it with evidence.
- Close with what you would do differently, concretely.
- Give the blast radius: what could have broken, and what you measured.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
How do you handle feedback on your code or design decisions?
Medium · behavioural and engineering judgement
Approach
- Close with what you would do differently, concretely.
- State the situation in two sentences and spend the rest on the reasoning.
- Give the blast radius: what could have broken, and what you measured.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
Describe a time you had to explain a complex technical concept to a non-technical stakeholder.
Medium · behavioural and engineering judgement
Approach
- Pick a story where you made the decision, not one where you watched it.
- Close with what you would do differently, concretely.
- Give the blast radius: what could have broken, and what you measured.
Follow-up
- What would you do differently if you ran that again?
- What did you decide not to do, and why?