InterviewLegend

Design a Mission-Critical Component

Commonly asked
Alex Chen24 min readUpdated 36 real interview reports
In this lesson
  • Model "a radar tracker for a moving ship" as Detection, Track, Sensor, Associator and Tracker classes plus a fixed-size pool
  • Gate detections, predict and correct each track, and run its lifecycle (birth, confirm, coast, delete) against misses and clutter
  • Bound work per scan, cap the pool with an eviction rule, name dropout, false returns and own-ship maneuver yourself

Understanding the round

When Anduril runs a design round for a software engineer, it's usually one round, and it's usually object-oriented design of a real physical thing. The shard-the-database, size-the-cache exercise that "system design" brings to mind rarely comes up. You take a component that lives on a vehicle or a sensor and model it in classes. The prompt candidates report most is "design a radar tracker for a moving ship." Its cousins keep the shape and swap the nouns: model a sensor-fusion tracker, implement a doubly linked list from scratch, or walk through a computer-vision perception pipeline. Nobody is checking whether you can serve a billion users. The interviewer wants to see a messy physical problem turn into a clean set of classes, with the physics underneath reasoned through and the failure modes handled. A real sensor throws those at you every second.

A radar tracker suits this round because it drags both halves of the skill into the open. The physics can't be dodged. The ship carries the radar and the ship is moving, so a contact sitting still in the water looks like it's moving unless you correct for your own motion first. The modeling can't be dodged either. A Detection is a raw, identity-less measurement. A Track is a hypothesis about one real object over time. Something has to decide which detection belongs to which track when the sensor hands you a scatter of noisy returns. Then the failures arrive. Returns drop out, clutter creates false ones, two contacts cross paths. The weak version of this answer is one giant update function looping over raw points, which is CRUD-app thinking pointed at a radar.

The Anduril angle
Anduril writes software the way it ships hardware, against a real physical system. So this round is graded on domain judgment and clean modeling. How many boxes you draw barely matters. The autonomy and Lattice stacks run close to the sensor, in C++ and Rust on edge computers wired to real hardware. The interviewer wants a messy physical thing turned into a small set of classes with honest responsibilities. Here that thing is a radar watching contacts from the deck of a ship that is itself moving. Expect the prompt to arrive underspecified on purpose, then escalate. Before you name a single class, ask what the sensor gives you, how fast scans arrive, and what happens when a contact drops out. Reason about own-ship motion, coordinate frames and measurement noise out loud. A candidate who forgets the ship is moving is modeling the wrong problem. And treat failure as a first-class case. On a live sensor, a missed return, a false return and two crossing contacts are the normal operating condition.

Pinning the requirements first

Don't name classes yet. A minute spent bounding the problem is what keeps the design from sprawling. Say the functional surface plainly. Once per scan the radar hands you a set of detections. Each is a measured position with a timestamp and some sense of how strong the return was, and none says which object it came from. The tracker keeps a set of tracks, one per object it believes is really out there. On every scan it decides which detections update which tracks, starts new tracks for detections that match nothing, and ages out tracks that stopped getting returns. It does this while the ship carrying the radar is moving, and it has to finish each scan before the next one arrives. That's the whole core. It's enough to model something real.

Then separate what you'll handle now from what you'll name and defer. Tracker math runs deep. Try to derive a Kalman filter's covariance update or a globally optimal assignment in the first pass, and you'll run out of time before the structure is clean. Model the skeleton so those pieces have an obvious place to live, then say so. In a good design, "swap the nearest-neighbor match for a real cost-based assignment later" means replacing one class. In a bad one, it means rewriting the whole scan loop. Say the estimator can start as a simple constant-velocity predictor and grow into a proper filter. Say association can start greedy and grow into a global assignment, and that both have a named home. Saying out loud what you're deferring is the reliability instinct the round looks for.

  • What the round grades: modeling, not scale. Clean class responsibilities, physical reasoning, and failure modes handled. No QPS math, no sharding, no load balancers. If you catch yourself estimating throughput across a fleet, you've misread the prompt, which is about one component on one sensor.
  • The ship is moving: correct own motion first. The radar sits on a platform that translates and rotates. A still contact looks like it's moving until you transform detections out of the sensor frame into a stable world frame using the ship's own nav state. Forget this and you model the wrong problem.
  • Detections are identity-less: association is the crux. The sensor gives you a scatter of unlabeled measurements per scan. Deciding which detection belongs to which track, inside a gate so the work stays bounded, is the hardest and most graded decision in the design.
  • Objects come and go: birth, confirm, coast, delete. A new return that matches nothing may be a real object or clutter. So a track is born tentative and confirmed after enough hits. It coasts through short dropouts on its own predicted motion and is deleted only after missing too long.

The physics before the classes

Spend the first real minutes on the domain. Once the physics is straight, the classes fall out of it. Start with coordinate frames. The radar reports where a return is relative to itself, in the sensor frame, but the ship it's bolted to is moving through the water and turning. So the same object measured on two scans lands at two different sensor-frame positions even if it never moved. Before you track anything, transform each detection into a stable world frame using the ship's position and heading at the moment of the scan. Skip that and every stationary buoy looks like a fast-moving contact. It's the most common way this design goes wrong.

Next comes the predict-and-correct cycle, and it's the reason a track carries a velocity as well as a position. Objects keep moving between scans. So the tracker advances each existing track along its own estimated motion to the new scan's time. Then it corrects that guess with whatever detection it can associate. Prediction is how a track survives a scan with no return. You coast it forward on its last known velocity. Correction keeps the estimate honest as real measurements come in, and every scan runs both steps.

Gating is the last piece, and it keeps association both tractable and safe. Don't compare every detection against every track. Draw a gate around each track's predicted position, sized by how uncertain that prediction is. Only detections inside the gate can update that track. Gating does two jobs. It bounds the work, since a track only considers the handful of detections near it. And it rejects the physically impossible. A detection that would need an object to jump an implausible distance in one scan falls outside the gate, and it's treated as a new object or as clutter. Say the gate out loud early. Most of the association logic follows from it.

Finding the classes

With the physics straight, surface the classes by underlining the nouns and keeping the ones that carry state or behavior. Sensor, Detection, Track, Associator, Tracker and a track pool all pass that test. "Scan" and "gate" turn out to be behavior. Give each survivor a single responsibility and check that no two are secretly doing the same job.

A Detection is a small immutable value: a measured position, a timestamp and a return strength, with no identity attached. The sensor produces it and association consumes it. It stays identity-less on purpose, because deciding its identity is the tracker's job. A Track is the opposite, a stateful hypothesis about one real object over time. It holds the estimated position and velocity, the time of its last update, its detection count, how many scans it has been missing, and its lifecycle stage. The Sensor is the source of detections and, in a full design, owns the frame the returns arrive in. The Associator owns one decision, matching detections to tracks inside their gates, and nothing else. The Tracker is the coordinator. It holds the track pool, runs the per-scan cycle, and wires the other pieces together without doing any of their jobs.

The track pool is where the Anduril flavor shows. On an embedded flight computer you don't want the tracker allocating memory mid-scan. Nor do you want the track count growing without bound when the sensor stares at a noisy sea full of false returns. So the pool is fixed capacity, sized to the most objects the system is built to hold at once. When it's full and a new object appears, the tracker evicts the weakest existing track by a defined rule. It doesn't allocate. A growable list looks fine on a laptop and stutters in the field.

cpp
// A Detection is an IMMUTABLE measurement from one scan: where the return
// was, when, and how strong. It carries NO identity. Deciding which track it
// belongs to, if any, is the tracker's job, not the sensor's.
struct Detection {
Vec2 position; // world frame, AFTER own-ship correction
double timestamp; // seconds, from the sensor clock
double strength; // return amplitude / SNR, feeds the quality score
};
// A Track is a stateful hypothesis about ONE real object over time. It owns
// its estimate and its place in the lifecycle, and it knows how to move
// itself forward, because prediction is what lets it survive a missed scan.
enum class Status { Tentative, Confirmed, Coasting, Deleted };
class Track {
Vec2 position_; // estimated position, world frame
Vec2 velocity_; // estimated velocity, so we can predict forward
double last_update_; // time of the last associated detection
int hits_ = 0; // detections folded in (drives Tentative -> Confirmed)
int misses_ = 0; // consecutive scans with no return (drives Coasting -> Deleted)
Status status_ = Status::Tentative;
public:
// Where should this object be at time t, on its own motion alone?
Vec2 predict(double t) const {
double dt = t - last_update_;
return position_ + velocity_ * dt;
}
double gate_radius() const; // sized by how uncertain the estimate is
void correct(const Detection& d); // fold a matched detection into the estimate
void coast(); // no return this scan: age it, maybe delete
Status status() const { return status_; }
};

A Track knows how to predict itself forward, so the coordinator never reaches inside to do physics for it. The estimator can grow from a constant-velocity guess into a full filter by changing only correct and gate_radius. A Detection knows nothing about tracks, so association can offer the same measurement to several candidate tracks without any of them owning it early. And the lifecycle lives on the Track as an explicit status plus two counters. Promoting a tentative track to confirmed, or coasting a confirmed one through a dropout, is a small state change in one place. Nothing gets scattered through the scan loop as a special case.

The class relationships

Once the classes have names, what's left is the wiring between them and the order of one scan. Draw it. Color marks role. Blue is a source of truth from the outside world: the sensor and the ship's own motion. Amber is the identity-less measurement flowing in. Violet is the coordinator and the two services it drives. Green is the state the design owns, the pool and its tracks. Follow the numbered edges for one full tracking cycle. Predict every track forward, gate and associate the new detections, correct the matches and birth the leftovers, then coast and cull.

Loading diagram...

The scan cycle, and where each rule lives

Everything converges on one method, the tracker's handling of a single scan. Written out, it shows every class pulling its weight and no rule left without a home. The order is fixed. It matters. First move the raw detections out of the sensor frame into the world frame using the ship's motion, so you track real space and not the deck. Then predict every existing track forward to the new scan's time, so association compares detections against where each object should be now. A last-seen position is already stale. Then associate inside the gates. Then apply the results: correct the tracks that matched, birth new tracks for unmatched detections, and coast the tracks that got no return. Finally cull the tracks that have coasted past the limit. The method reads like the tracking loop because each object already knows its own job.

cpp
// One radar scan is one tracking cycle. The ORDER is the design: transform,
// predict, associate, then apply births, hits, and coasts, then cull.
void Tracker::on_scan(const std::vector<Detection>& raw,
const OwnShip& ship, double now) {
// 0. Own-ship correction: move detections into a stable world frame so a
// still object does not look like it is moving just because we are.
std::vector<Detection> dets = to_world_frame(raw, ship);
// 1. PREDICT: advance every live track to the scan time on its own velocity, so
// association gates around where each object should be, not where it was.
for (Track& t : pool_.live()) estimator_.predict(t, now);
// 2. ASSOCIATE: match detections to tracks, but ONLY inside each gate.
Assignment a = associator_.match(pool_.live(), dets);
// 3. APPLY the outcome for each of the three groups.
for (auto& [track, det] : a.matched) estimator_.correct(*track, det); // refine + confirm
for (Detection& det : a.unmatched_dets) pool_.birth(det); // new, Tentative
for (Track* t : a.unmatched_tracks) t->coast(); // no return: age it
// 4. CULL: delete tracks that have coasted past the miss limit.
pool_.delete_stale();
}
// Gating: a detection is eligible for a track only if it falls inside the
// track's gate, a region around the prediction sized by its uncertainty. This
// BOUNDS the work and rejects physically impossible jumps outright.
bool in_gate(const Track& t, const Detection& d, double now) {
Vec2 predicted = t.predict(now);
double dist = (d.position - predicted).norm();
return dist <= t.gate_radius(); // outside the gate: cannot be this object
}

Tell the interviewer that association is the seam where the design stays clean or collapses, which is why it sits behind its own interface. The version above implies a greedy nearest-neighbor match, each detection to its closest in-gate track. That's an honest starting point. The interviewer may push on two contacts crossing, where greedy matching can hand both detections to one track and starve the other. Then you upgrade to a global assignment that minimizes total cost across the gate. Association is isolated, so that upgrade replaces one class and touches nothing else. Then say why prediction runs before association. Gate around a stale position and a fast contact escapes its own gate every scan. Gate around the predicted position and it stays matched. That's the physical reasoning the round wants to hear.

Real-time constraints and failure modes

A tracker that works in a clean demo and falls apart on a real sensor is a failing answer. Spend real time on the constraints and the ways it breaks. Start with timing. It shapes the data structures. The tracker has to finish one scan before the next arrives. So the work per scan has to be bounded and predictable, and fast on average isn't enough. That's why the pool is fixed size, why gating caps how many detections each track considers, and why nothing allocates memory mid-cycle. A latency spike from an unbounded allocation or an all-pairs match is worse than a nuisance here. It's a scan the sensor moved past while you were still on the last one.

Then walk the failure modes out loud, because on a live sensor they're constant. A real object stops returning for a scan or two, from occlusion or a weak return. Its track must coast on predicted motion through short dropouts and be deleted only after a sustained absence, never on a single miss. Clutter produces false detections that match nothing. So a new track is born tentative and must survive several scans of confirmation before it's trusted, which keeps a noisy sea from spawning phantom tracks. Two contacts cross, so association must resolve the ambiguity by total cost or the two tracks swap identities. And the ship maneuvers hard, so the own-ship correction must use the nav state at each scan's time. Otherwise a turn smears every stationary contact into motion. Name these before the interviewer does, and you read as senior.

  • Bounded work per scan: finish before the next. The tracker must complete a scan before the next arrives, so the design caps the work. That means a fixed-size pool, gating that limits comparisons per track, and no mid-cycle allocation. Predictable latency beats fast-on-average when a spike means a missed scan.
  • Dropout: coast, do not delete. A real object can go quiet for a scan or two, so a track predicts itself forward through short gaps. It's deleted only after a sustained run of misses. Dropping a confirmed track on a single missed return loses a real object the operator was watching.
  • Clutter and false returns: confirm before you trust. Noise creates detections that match nothing. So a new track is born tentative and must collect several hits before it's confirmed. That confirmation gate stops a noisy scene from spawning a swarm of phantom tracks.
  • Crossing contacts: assign by total cost. When two objects cross, greedy matching can give both detections to one track. The other starves into a false delete or an identity swap. A global assignment that minimizes total in-gate cost resolves the ambiguity correctly.
The Anduril angle
The instinct that reads as an Anduril hire is to design for the bad day. Say out loud why each choice exists. The pool is fixed because the target is an embedded computer that can't allocate mid-scan. A track coasts through a dropout because real contacts go quiet all the time. Confirmation exists because the sea throws false returns constantly. Association resolves crossing tracks by cost because identity swaps are how a tracker embarrasses itself in the field. Then say how you'd prove it. Replay recorded sensor logs with injected dropouts and clutter, and check that no confirmed track is lost and no phantom is born. Reason about the tracker at three in the morning in heavy sea clutter, past the clean single-contact case. That's the judgment the bar looks for, whether the component is a radar tracker, a perception pipeline or a link between two vehicles.

The runnable core: a fixed-capacity track pool

The interviewer often compresses the whole design into one implementable class. For a tracker the natural one is the track pool itself. It's a fixed-capacity manager that births tracks, refreshes the ones that got a return, evicts the weakest when full and a new object arrives, and expires the ones that coasted too long. It's the full design in miniature, since the whole real-time posture lives in that one class. The bounded capacity, the eviction rule and the expiry sweep keep the tracker predictable on real hardware. They're also where a candidate either states a clean invariant or hand-waves.

The problem below strips the physics away and keeps the bookkeeping. Bookkeeping is what runs. A track here is keyed by a known id and carries only its last-seen time. That's enough to define eviction (drop the stalest track when full) and expiry (delete tracks that coasted past a limit). The exercise is handling the eviction tie-break and the expiry boundary cleanly, the same way the duplicate case is the exercise in a min-stack.

1

Track pool: fixed capacity with birth, refresh, evict, and expire

Medium
Problem
A radar tracker keeps a bounded number of live tracks, one per contact it believes is real, in a fixed-size pool. Implement the manager that governs the pool:

- TrackManager(capacity) creates a pool that holds at most capacity tracks.
- update(id, time) folds a detection for contact id seen at scan time time. If a track for id already exists, refresh its last-seen time to time and return -1. If it does not exist and the pool has a free slot, create a new track with that last-seen time and return -1. If it does not exist and the pool is full, evict the stalest track (the smallest last-seen time, breaking ties by the smallest id). Then create the new track in its place and return the evicted id.
- expire(now, maxAge) deletes every track whose last-seen time is older than maxAge before now, meaning now - lastSeen > maxAge, modeling a track that coasted too long with no return. Returns the number of tracks deleted.
- active() returns the ids of the tracks currently held, sorted ascending.
- contains(id) returns whether a track for id is currently held.

The eviction rule and the expiry boundary carry the grade. A full pool must make room by dropping the right track. A coasting track must be culled at exactly the right age, on the right scan.

Input encoding. A test case is a JSON array of operations. The first entry is ["TrackManager", [capacity]]; each later entry is [method, [args]]. The expected array holds each operation's return, with null for the constructor.

Input: TrackManager(3); update(10,1); update(20,2); update(30,3); update(40,4); active()

Output: update(40,4) returns 10; active() returns [20,30,40]

The pool fills with 10, 20, 30. When 40 arrives the pool is full, so the stalest track (id 10, last seen at time 1) is evicted and 40 takes its place.

Input: TrackManager(5); update(7,10); update(8,12); update(9,20); expire(25,10); active()

Output: expire(25,10) returns 2; active() returns [9]

At now=25 with maxAge=10, any track last seen before time 15 is too old. Tracks 7 (last 10) and 8 (last 12) are deleted; track 9 (last 20) survives.

How this uses the pattern

Keep the tracks in a map from id to last-seen time. The update call is a map lookup with three branches, and the eviction case scans the map for the minimum (lastSeen, id) pair. The expire call is a filtered sweep, and active sorts the keys.

Track pool: fixed capacity with birth, refresh, evict, and expire

Loading...
Click "Run" to execute your code

Test Cases

Input: [["TrackManager",[3]],["update",[10,1]],["update",[20,2]],["update",[30,3]],["contains",[20]],["update",[40,4]],["active",[]],["contains",[10]]]Expected: [null,-1,-1,-1,true,10,[20,30,40],false]
Input: [["TrackManager",[2]],["update",[1,1]],["update",[2,2]],["update",[1,3]],["update",[3,4]],["active",[]]]Expected: [null,-1,-1,-1,2,[1,3]]
Input: [["TrackManager",[5]],["update",[7,10]],["update",[8,12]],["update",[9,20]],["expire",[25,10]],["active",[]]]Expected: [null,-1,-1,-1,2,[9]]
Input: [["TrackManager",[2]],["update",[5,1]],["update",[3,1]],["update",[9,2]],["active",[]]]Expected: [null,-1,-1,3,[5,9]]
Input: [["TrackManager",[3]],["update",[1,5]],["update",[2,5]],["expire",[15,10]],["expire",[16,10]],["active",[]]]Expected: [null,-1,-1,0,2,[]]
The Anduril angle
This is the real-time core of the radar-tracker design, reduced to what runs. Anduril cares that the pool is bounded and that eviction and expiry are defined precisely. On an embedded computer, an unbounded track list is a latency spike waiting to happen. Adding and removing from a map is the easy part. The interviewer watches whether the eviction tie-break (smallest last-seen, then smallest id) and the expiry boundary (strictly greater than maxAge) survive the test cases. State the eviction invariant before you code it. Then drive the boundary cases yourself: a full pool, a tie in staleness, and a track sitting exactly at the age limit.
Take it further
A natural Anduril extension puts the physics back. The manager no longer gets an id from the sensor, because real detections are identity-less. It has to decide which detection belongs to which track by gating around each track's predicted position and choosing a global lowest-cost assignment. Add the lifecycle too. A track is born tentative, promoted to confirmed after several hits, coasted on its predicted motion through short dropouts, and deleted only after a sustained run of misses. Be ready to say what that costs. Association goes from a map lookup to an assignment over the in-gate pairs. Eviction should prefer dropping tentative tracks over confirmed ones, so clutter never displaces a real object.

What the round rewards

The bar on this round is clean domain modeling, sound physical reasoning, and failure modes handled. Scale isn't on it. Whichever component you draw (radar tracker, sensor-fusion tracker, a doubly linked list, a perception pipeline), the moves are the same. Clarify the underspecified prompt out loud before you model, so scope is agreed and you solve the real problem. For a tracker, that means asking what the sensor gives you and remembering the ship is moving. Reason about the physics first (coordinate frames, prediction, gating) so the classes fall out of the domain. Give every class one responsibility and keep the coordinator thin, so the estimator and the associator can each be upgraded without touching the scan loop. Choose bounded, real-time-honest structures, like a fixed pool with a defined eviction rule. And name the failure modes yourself: dropout, clutter, crossing contacts, an own-ship maneuver. On a real sensor those are the design itself.

Key Takeaways

  • ✓Anduril's design round is object-oriented modeling of a real physical component. The canonical prompt is "design a radar tracker for a moving ship." A sensor-fusion tracker, a from-scratch doubly linked list and a perception pipeline are the same exercise. The grade is clean class responsibilities, physical reasoning and failure modes, so never reach for sharding or capacity math.
  • ✓Reason about the physics before the classes. Transform detections out of the sensor frame into a stable world frame using own-ship motion. Predict every track forward on its own velocity before associating, and gate each track so association is bounded and impossible jumps are rejected. The classes (Detection, Track, Sensor, Associator, Tracker, a track pool) fall out of that domain cleanly.
  • ✓Everything converges on one scan cycle. Transform, predict, associate inside the gates, then correct matches, birth unmatched detections, coast unmatched tracks, and cull the stale ones. Keep the Tracker a thin coordinator and hide association and estimation behind their own seams. Then a greedy match becomes a global assignment, and a constant-velocity guess becomes a real filter, by replacing one class.
  • ✓Design for the bad day. Use a fixed-capacity pool with a defined eviction rule so the tracker is predictable on embedded hardware. Coast a track through short dropouts instead of deleting it on one miss. Confirm tentative tracks so clutter can't spawn phantoms, and resolve crossing contacts by total assignment cost. The runnable core is that bounded pool, where the eviction tie-break and the expiry boundary are where the grading happens.
Had your Anduril interview?Tell us what they asked. Reviewed before it goes in, never with your name.
Anduril interview guide

Know what Anduril asks before you walk in.

9 coding · 2 system design · 14 behavioral questions

Get Premium$49 a year · Cancel anytime
Or start freeNo card.

Get their questions first.

InterviewLegend is independent and not affiliated with or endorsed by any company it covers.