Interview guides · Behaviour Interactive
Behaviour Interactive Software Engineer Interview Questions & Guide 2026
Code reading practice for Behaviour Interactive software engineer prep: work out what unfamiliar code does before judging it.
The 60-second brief
A Software Engineer at Behaviour Interactive plays a foundational role in bringing immersive gaming experiences to life. As one of the most prominent independent game developers, Behaviour Interactive relies on its engineering team to blend creative vision with technical precision. You will be responsible for building, optimizing, and maintaining the systems that power high-profile, globally recognized titles.
If the seat owns a service boundary, scope your preparation toward failure behaviour rather than topology. Retrying over an at-least-once channel produces duplicates by construction, so a retry policy is only as safe as the idempotency key underneath it.
Focus areas
- Prevent negative balances with conditional single-statement debits
- Make cross-service trades atomic through escrow sagas
- Audit item supply from append-only ledgers
Overview
The impact of this role extends from the underlying engine architecture to the fine-tuning of player-facing features. Whether you are working on core gameplay mechanics, infrastructure, or performance optimization, your contributions directly influence the quality and stability of the player experience. You will work within cross-functional teams, collaborating closely with designers, artists, and producers to solve complex problems in a fast-paced, high-stakes environment.
This role is both challenging and rewarding for engineers who thrive on technical complexity and creative problem-solving. You will often encounter unique constraints related to real-time rendering, network synchronization, or cross-platform compatibility. Success here requires not only a mastery of modern programming languages and game engines but also a genuine passion for the evolving landscape of interactive entertainment.
The Behaviour Interactive interview process
Behaviour Interactive candidates report 3 rounds · ≈ 3-5 weeks. The stages below are what candidates describe, not a published process.
Technical Screening
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.
Team Fit Evaluation
This round is a resourcing decision in the shape of a conversation: how large a piece of work can be handed to you with a one-line brief and no check-in for two weeks. The manager is listening for the seams in your story, the places where you settled something yourself and the places you went back for a ruling. Most candidates describe the system and skip the decisions, which reads as having been present rather than responsible. Say who wanted the work, which option you rejected and why, and where you would have stopped and escalated.
What to demonstrate
- Whether you can point at a design decision that was yours rather than the team's, and name the alternative you turned down and the constraint that killed it
- What you treat as yours to settle against what you take to someone else, and how long you sit on a blocker before raising it
- Whether the scope you claim survives a follow-up into the unglamorous part of it: the data migration, the backfill, the rollout to users who were already on the old path
- Whether you asked what problem the request was solving before building what was literally asked for, and what changed in the design once you had the answer
How to prepare
- Write out the brief for your last two projects exactly as it reached you, usually one sentence in a ticket, then list every question you had to answer yourself before code could be written. That list is most of what this round is asking for.
- For one decision in each project, write down the rejected option, what you were trading against, and the piece of evidence that would have flipped you. A tradeoff you cannot argue in reverse was not really a decision.
- Have an escalation ready: a blocker you took to your manager, what you had already tried, and the specific thing you were asking them to decide. If every example is something you handled alone, that reads as someone who does not ask.
Final Discussion
Where the day includes a partner from product, design or data, that conversation is weighted like the technical ones and prepared for least. They are deciding one thing: whether having you in the room makes their decisions cheaper. That means options with costs attached, not implementation detail and not "it depends". An estimate someone can plan against — a range, the assumption that would push it to the high end, and what you would drop to hit the low one — is worth more than a confident single number, which everyone present already knows is wrong.
What to demonstrate
- Whether an estimate comes as a range with the assumption most likely to break it, and states what a specific scope cut would actually buy
- Whether a technical constraint is handed over as a choice with consequences on their side, rather than as a verdict they have no standing to argue with
- Whether you establish what decision is on the table before proposing anything
- Whether risk is raised while it can still change the plan, with the trigger that would confirm it, instead of reported afterwards as a slip
How to prepare
- Take a project that shipped late and write the two-sentence warning you could have given three weeks earlier, naming what you would have needed decided at that point
- Rehearse one estimate out loud until it arrives in three parts: the range, the single assumption that would blow it, and the smallest thing you would cut to protect the date
- Rewrite an objection you have actually made — the "we can't do that" version — as two options with their costs, so the choice ends up with the person who owns it
Where candidates lose points
Read-modify-write on a balance or a stack quantity - SELECT the current value, check it in application code, then UPDATE to the computed new value.
The gap between the read and the write is a lost-update window of a few milliseconds. It survives every code review and every load test run at baseline concurrency, then fires reliably when a seasonal event puts two requests on the same wallet inside that window, or when one player has two clients open. The fix is one statement - UPDATE ... SET qty = qty - :n WHERE qty >= :n - plus an affected-row check; the trap is that the broken version returns the correct answer in every single-threaded test you will write.
Caching inventory or entitlement at the edge or in the client without a version token, so a spend is validated against a stale copy.
Cache invalidation races the spend directly: a player with two sessions can spend the same stack twice inside the TTL, and a revoked entitlement stays usable until it expires. The symptom looks like a duplication bug in the inventory service, so investigation starts in the wrong place. Serving the token (a row version or ETag) and requiring the client to present it on the mutating call turns a stale read into a rejected write instead of a double spend.
Optimising an axis nobody named
Ask which resource is actually scarce here: wall-clock latency, throughput, memory footprint, cost per request, or engineering time. Shaving a constant factor off an in-memory step is wasted effort when the same function makes a blocking remote call inside the loop.
Saying 'eventually consistent' without naming the anomaly a user would see
Describe the concrete symptom you are choosing to accept: the author reloads and their own comment is missing for two seconds, or two devices show different balances for a minute. The class of consistency model is a technical label; the tolerable anomaly is the actual product decision.
Questions & practice
Describe how you would approach a debugging task within a complex game loop.
Medium · data structures and algorithms
Approach
- Restate the input: its shape, its size, and what is guaranteed about it.
- Walk one small example through your approach before writing the whole thing.
- Name the brute-force solution and its complexity before improving on it.
Follow-up
- Which test case would catch an off-by-one here?
- How does this change if the input no longer fits in memory?
Find the mode of an array.
Medium · data structures and algorithms
Approach
- Walk one small example through your approach before writing the whole thing.
- Choose the data structure from the access pattern, not from familiarity.
- State the target complexity and say which constraint rules the naive version out.
Follow-up
- Which test case would catch an off-by-one here?
- How does this change if the input no longer fits in memory?
Find if a pair of integers in an array results in a specific sum.
Medium · data structures and algorithms
Approach
- Choose the data structure from the access pattern, not from familiarity.
- Walk one small example through your approach before writing the whole thing.
- State the target complexity and say which constraint rules the naive version out.
Follow-up
- Which test case would catch an off-by-one here?
- What is the worst case, and how likely is it on real data?
Merge cheapest active listings across shards without full scans
Medium · k-way merge · heap · sharding
market_listing is sharded by seller_account_id across 64 shards. A buyer opens the page for one item_def_id and wants the K = 50 cheapest status='active' listings by ask_price_minor, ties broken by created_at. Each shard can return its own listings for that def already sorted, in pages. Catalog load is concentrated: a popular def may have a million active rows shard-wide. Return the global cheapest K while reading as few rows as possible, state the number of rows you read, and name the index that makes each shard's page cheap.
Approach
- k-way merge driven by a min-heap holding one entry per shard, keyed on the full tuple (ask_price_minor, created_at, listing_id) so the ordering is total and the same query returns the same page twice. Heapify 64 heads in O(S), then K pops at O(log S) each: O(S + K log S), roughly 64 plus 50 times 6 comparisons.
- Rows read is at most S + K, here 114, against the million active rows for that def. Page each shard lazily, requesting its next row only when its current entry is popped, and batch the request into pages of roughly K/S plus margin so the round trips amortise.
- The per-shard query needs a partial composite index on (item_def_id, ask_price_minor, created_at) WHERE status = 'active'. Without it, each shard sorts a million rows to hand you one, and the partial predicate keeps the index off the sold, cancelled and expired rows that dominate the table over time.
- Name the other shape too: over a single unsorted stream of n rows, use a bounded max-heap of size K for O(n log K) time and O(K) space, rejecting an incoming row in O(1) when its price is worse than the heap root. Quickselect is O(n) average but needs all n resident and gives up streaming.
- State the concurrency caveat rather than pretending the result is a snapshot: the shards are read at slightly different instants, so a listing can sell between two shard reads. Revalidate the chosen listing's status transition at purchase time; the merge result is a view, not a lock.
Worked solution (25 min)
- Take four shards with sorted prices in minor units: s0 = [120, 130, 400], s1 = [125, 126, 127], s2 = [500], s3 = [118, 900], and K = 5.
- Seed the heap with the four heads (118, 120, 125, 500) and record the read count as 4.
- Pop five times, advancing only the popped shard and incrementing the read count on each advance that returns a row.
- Sort the full nine-row union independently and compare the first five entries.
Expected result: The cheapest five are 118, 120, 125, 126, 127. The merge reads 8 rows out of the 9 that exist: 4 heads plus 4 successors, with the final pop draining s1 and triggering no read. The heap never holds more than 4 entries.
Verify it yourself
- Rows read never exceeds S + K, which is 9 here and 114 at the stated production numbers.
- Heap size never exceeds the number of shards that still have rows.
- The merged output is identical, element for element, to the first K of the fully sorted union.
Follow-up
- The buyer asks for page 20 rather than page 1. What does deep offset cost in a k-way merge, and what do you change to make it flat?
- One shard times out. Do you return 49 listings, serve a cached page, or fail the request, and how does the answer differ for a browse page versus a purchase confirmation?
Design the unique constraint that stops a reward duplicating
Medium · idempotency · at-least-once · partial index · reconciliation
grant_receipt(grant_id UUID PK, account_id BIGINT, idempotency_key TEXT NOT NULL UNIQUE, source_type TEXT, source_ref_id UUID, payload_hash BYTEA, status TEXT CHECK (status IN ('pending','applied','reversed','rejected')), created_at TIMESTAMPTZ, applied_at TIMESTAMPTZ NULL). purchase_order(order_id UUID PK, platform TEXT, receipt_id TEXT, capture_status TEXT, fulfil_status TEXT, fulfil_grant_id UUID NULL, captured_at TIMESTAMPTZ NULL). Match rewards arrive over an at-least-once queue; store receipts can be re-presented by a reinstalled client. Specify how idempotency_key is derived for a match reward and for an order fulfilment, what the insert does on a key collision, and write the sweep that finds captured payments never fulfilled.
Approach
- Derive the key from server state only: for a match reward,
'match:' || match_id || ':' || account_id || ':' || slot_index, taken from the signedmatch_resultrow; for fulfilment,'order:' || platform || ':' || receipt_id, taken from the validated receipt. Nothing the client sends enters the key, so a client that regenerates a request id on retry cannot mint a second reward. - Explain what one global
UNIQUE (idempotency_key)buys: the source is encoded in the string, so a single index covers every source type, and the database rather than application code is the thing enforcing at-most-once. A broker that redelivers on consumer failure is at-least-once by construction, so the sink has to dedupe. - Handle the collision correctly. On 23505, read the existing row and compare
payload_hash: equal means a genuine retry, so return the originalgrant_idand the original contents; different means a key reused with different contents and must be rejected, not quietly honoured with the old payload. - Know the
ON CONFLICTsharp edge:DO NOTHING RETURNING grant_idreturns zero rows on conflict and does not hand back the existing row. Either follow it with a SELECT, or use a no-opDO UPDATEto force RETURNING at the cost of a row lock and a dead tuple on every retry. - Write the sweep for the opposite failure — captured but never fulfilled — with a grace window so it does not race an in-flight fulfilment, and a partial index on
(captured_at) WHERE capture_status='captured' AND fulfil_status='unfulfilled'so it costs the size of the matching set rather than the table. - Say what the sweep does on a hit: re-drive the same grant with the same derived key, which is safe precisely because the key is idempotent and a genuine double-run collides instead of double-granting.
Worked solution (25 min)
- Write the key derivation for both source types as literal string expressions over named columns.
- Insert the same grant twice with identical payloads and confirm the second returns the original
grant_idand applies nothing. - Insert again with the same key and a different payload, and confirm it is rejected rather than returning the old grant.
- Write the captured-but-unfulfilled sweep with its grace window, create the matching partial index, and
EXPLAINit. - Simulate a crash between the grant write and the queue acknowledgement, replay the message, and confirm nothing duplicates.
Expected result: Keys of the form 'match:'||match_id||':'||account_id||':'||slot_index and 'order:'||platform||':'||receipt_id; a collision path that compares payload_hash before returning the original; and SELECT order_id, account_id, sku_id, captured_at FROM purchase_order WHERE capture_status='captured' AND fulfil_status='unfulfilled' AND captured_at < now() - interval '5 minutes' ORDER BY captured_at LIMIT 500, backed by a partial index with that predicate.
Verify it yourself
- Replaying one grant 100 times leaves exactly one
grant_receiptrow and onetxn_idincurrency_ledger. - The same-key, different-payload replay is rejected rather than returning the earlier grant.
EXPLAINon the sweep shows an index scan on the partial index, not a sequential scan of the order table.- The sweep returns zero rows in steady state and a bounded set immediately after an induced fulfilment failure.
Follow-up
- A reward is granted, then the match's
integrity_flagflips tovoided. How do you take it back, and what does the player see? - Two reward slots on one match happen to resolve to identical items. Does your key still separate them?
- The queue redelivers a batch of 50,000 after a consumer OOM. What does your database do for the next minute?
Serve the cheapest active listings for a hot definition
Medium · partial index · denormalisation · query planning
market_listing(listing_id UUID PK, seller_account_id BIGINT, item_instance_id UUID, item_def_id INT, quantity INT, ask_price_minor BIGINT, currency_code TEXT, status TEXT CHECK (status IN ('active','reserved','sold','cancelled','expired')), expires_at TIMESTAMPTZ, created_at TIMESTAMPTZ). The table retains every historical listing and roughly 2% of rows are active. The top 1% of item_def_id values carry most of the read traffic. Write the query returning the twenty cheapest active, unexpired listings for one item_def_id, and the index that serves it. A colleague has already added (item_def_id, ask_price_minor) and reports that the hot definitions are still slow — explain why and fix it.
Approach
- Write the query so its shape matches an index: equality on
item_def_id,status = 'active',expires_at > now(),ORDER BY ask_price_minor ASC, created_at ASC,LIMIT 20. - Diagnose the existing index honestly: only ~2% of rows are active, so on a hot definition the scan walks a long run of sold, cancelled and expired entries, checking each against the heap, before it accumulates twenty live rows. The LIMIT does not bound the work — the density of matching rows does, and on the hot head that density is worst.
- Fix it by moving the status filter into the index predicate:
CREATE INDEX ON market_listing (item_def_id, ask_price_minor, created_at) WHERE status = 'active';. Non-active rows are physically absent, the scan stops after twenty entries, and the index is roughly 2% of the size so it stays resident. - Know what cannot go in that predicate:
expires_at > now()is rejected because a partial index predicate must be immutable andnow()is STABLE. Keep expiry as a runtime filter, or run a sweep that flips expired rows tostatus='expired'so the partial index sheds them on the next write. - Justify the denormalised
item_def_idon the listing row: it makes this a single-table read with no join toitem_instance. It is safe to copy precisely because an instance's def id is immutable — a denormalised copy of a mutable column needs a trigger or a scheduled backfill, and this one needs neither. - Confirm the ORDER BY columns and directions match the index so the planner takes the ordering from the index instead of sorting the matched set.
Follow-up
- A sale flips one of these listings to
soldwhile a buyer is reading the page. What does the buyer see, for how long, and what happens when they click buy? - Write volume on the hot 1% of definitions is 100x the tail. How does that change your partitioning or your choice to keep one table?
Can you explain the differences between REST and SOAP architectures?
Medium
Approach
- Say what you would check first and why it is the highest-information step.
- State your assumptions explicitly before working the problem.
- Clarify what is being asked and what a complete answer contains.
Follow-up
- How would you know your answer was wrong?
- What assumption would you test first?
How do you approach searching through an array using two pointers?
Medium
Approach
- Clarify what is being asked and what a complete answer contains.
- State your assumptions explicitly before working the problem.
- Work from the requirement backwards to the design.
Follow-up
- What assumption would you test first?
- How would you know your answer was wrong?
Serve inventory reads without enabling a stale double spend
Easy · caching · invalidation · optimistic-concurrency · read-heavy
Every session start loads an account's stacks from item_instance (instance_id, owner_account_id, item_def_id, quantity, bind_state, escrow_id, row_version, consumed_at), typically 100 to 10,000 live stacks, at 3,000 logins per second, while writes arrive in spikes. Reads must be cheap, but a player running two clients must not consume the same stack twice against a cached copy. Give the cache design, its key and invalidation rule, and the mutating-call contract that turns a stale read into a rejected write instead of a double spend.
Approach
- Size it before designing it: an index on (owner_account_id) WHERE consumed_at IS NULL, and a 10,000-stack account at roughly 80 bytes a row is under a megabyte serialized. One blob per account is cacheable; the open question is correctness, not capacity.
- Key the cache by account plus a monotonically increasing inventory_version that every write bumps. Writing a new key rather than deleting an old one removes the delete-then-populate race, and the old entry ages out on TTL.
- Refuse to let the cache be the authority for a mutation. Serve each stack's row_version to the client and require it back: UPDATE item_instance SET quantity = quantity - :n, row_version = row_version + 1 WHERE instance_id = :i AND row_version = :v AND escrow_id IS NULL AND quantity >= :n. Zero rows is a 409 and the client refetches, so a stale read costs a round trip rather than a duplicate.
- Add single-flight on miss: a 20-50x write spike invalidates hot accounts repeatedly, and without a per-key loader lease every concurrent login rebuilds the same blob.
- Treat TTL as a memory bound, never as the correctness mechanism. An entitlement revoked mid-TTL stays usable exactly as long as the TTL, which is why the version token and not the expiry is what protects the write.
Worked solution (20 min)
- Compute the cached payload size at the 10,000-stack tail and the resulting memory for a hot set of 1 million accounts.
- Write the version-bump rule and show why key rotation avoids the invalidate-then-repopulate race.
- Write the conditional consume statement and define the client contract for a 409.
- Trace two clients consuming the same stack inside one TTL and show which one loses and what it sees.
Expected result: A per-account blob keyed by (account_id, inventory_version) with single-flight loading and a short TTL for memory bounding, plus a compare-and-set consume guarded on row_version, escrow_id IS NULL and quantity, where zero affected rows returns 409 and forces a refetch.
Verify it yourself
- Two simultaneous consumes of the same stack yield one success and one 409, with total quantity unchanged by the failed one.
- A write immediately following a read is never served the pre-write blob under the new version key.
- Reading from a lagging replica can only produce a rejected write, never an accepted duplicate.
- An escrowed stack is unusable by the owner even when the cached copy predates the lock.
Follow-up
- The read now comes from a replica. What does replication lag do to your version token, and is the resulting failure safe?
- Where must the escrow lock be visible for the marketplace to trust this path?
- At what stack count does caching one blob per account stop beating per-stack entries?
Reward consumer lag climbs while one partition stops advancing
Medium · poison message · at-least-once delivery · idempotency
Match-reward grants are consumed from a partitioned log. Consumer group lag on partition 7 grows linearly for forty minutes while every other partition stays at zero. The consumer process exits and restarts roughly every 30 s, and each restart triggers a group rebalance that briefly stalls all partitions. After you redeploy, support reports a burst of duplicated rewards. grant_receipt declares idempotency_key TEXT NOT NULL UNIQUE. Give the ordered diagnostic checklist, the root cause of both symptoms, and the fix.
Approach
- Read the crash, not the lag: pull the consumer's stack trace at exit and the offset it was processing, then fetch the record at that offset and reproduce locally. One partition stalled behind a crash-looping consumer is head-of-line blocking on a single record, not a throughput shortfall.
- Confirm the shape before theorising. In a partition-ordered log a poison record stops that partition's committed offset advancing at all, so its lag rises at exactly the production rate while other partitions are untouched. If every partition had degraded together you would be looking at capacity or a downstream dependency instead.
- Account for the second-order damage: each crash-restart rejoins the consumer group and triggers a rebalance, which stalls partitions that have nothing wrong with them. That is why the blast radius looks wider than one record and why the incident reads as a group-wide problem.
- Explain the duplicate burst separately. The consumer wrote the grant and died before committing its offset, so redelivery is guaranteed by at-least-once semantics — that part is correct behaviour. Duplicates prove the dedupe never fired, so inspect what idempotency_key is actually built from: any component that is client-supplied or regenerated per attempt, such as a fresh UUID or a wall-clock stamp, makes a collision impossible.
- Fix both. Make the sink idempotent by deriving the key from match_id, the result signature and the reward slot index, and compare payload_hash so a key replayed with different contents is rejected rather than honoured. Bound the poison record with an attempt counter plus a dead-letter destination so the partition advances and the record is quarantined for inspection.
- State the cost: dead-lettering restores availability but breaks per-key ordering for that account, so any downstream consumer that depends on ordering must either tolerate the gap or re-resolve state from the ledger.
Follow-up
- What changes if the broker acknowledges per message rather than per partition offset? Does the poison record still block anything, and what does it do instead?
- The grant was written but the offset was not committed. What should the redelivered attempt return, and how does the caller tell that apart from a genuinely new grant?
Suggested practice checklist
For someone who has spent the last few years shipping features and reading other people's code, and who has not solved a timed problem from a blank file in a long time. Five days rebuild the primitives and the patterns that sit on them, working from invariants rather than remembered solutions, and the last two attach that back to the rest of the loop.
Day 1 — Rebuild the primitives by implementing them
- Implement a dynamic array with doubling growth and an operation counter, then change the growth rule to add a fixed sixteen slots instead, and time both for n of ten thousand, a hundred thousand and a million. The fixed-increment version resizes n/16 times at O(n) each, so its total work is quadratic; doubling is what makes append amortised constant.
- Implement a hash map with separate chaining and a load-factor resize, then insert ten thousand keys engineered to land in one bucket and record what happens to lookup time, so that average-case O(1) becomes a claim with a stated precondition rather than a reflex.
- For dynamic-array append and hash-map insert, write down which cost is amortised rather than worst-case, which single operation pays the whole bill, and what a system with a hard per-operation deadline would have to do instead.
Deliverable: Two working implementations plus a timing table showing the input at which each structure's advertised complexity stops holding.
Day 2 — Arrays under an invariant: two pointers, sliding window, binary search
- Solve longest-subarray-with-sum-at-most-K using a sliding window, then run it on an input containing negative numbers and watch it return the wrong answer: extending the window only moves the sum monotonically when every element is non-negative, and that precondition is the whole reason the technique works.
- Write the binary search that finds the first index satisfying a predicate rather than an exact value, put the loop invariant above the loop in a comment, and verify termination on the two inputs that break careless versions: the empty range, and a range where every element satisfies the predicate.
- Compute the midpoint as lo + (hi - lo) / 2 and write one line on why the obvious (lo + hi) / 2 is a genuine defect in a fixed-width integer type and a non-issue in a language with arbitrary-precision integers.
Deliverable: Three solved problems, each with its invariant written above the loop, plus one recorded input on which the sliding window is provably wrong.
Day 3 — Sorting, heaps, and the greedy argument that has to be proved
- Solve one top-k problem three ways, by full sort, by a size-k heap, and by quickselect, then write the values of n and k at which each becomes the right choice, along with quickselect's quadratic worst case and why a randomised pivot makes that unlikely rather than impossible.
- Implement bottom-up heapify and count sift-down steps to confirm it does linear work rather than n log n, because most nodes sit near the bottom of the tree and therefore move only a short distance.
- Take interval scheduling by earliest finishing time and write the exchange argument out in full: given any optimal schedule, swapping in the earliest-finishing interval keeps it feasible and no smaller. Then construct the weighted variant where that same greedy fails and name what has to replace it.
Deliverable: A three-way top-k comparison with measured crossover points, one written exchange argument, and one counterexample to a greedy rule that looks almost identical.
Day 4 — Recursion, memoisation, and the step to a table
- Take one problem with overlapping subproblems, such as edit distance or coin change, instrument the plain recursion with a call counter to show the blow-up, then add memoisation and re-count.
- Convert the memoised version to a bottom-up table and state the two properties you relied on: each subproblem's result depends only on its arguments, and the dependencies form a DAG you can enumerate in order.
- Rewrite one deep recursion with an explicit stack, then find the input length at which the original hits the interpreter's frame limit, which defaults to about a thousand frames in CPython, so you know when the rewrite is required rather than decorative.
Deliverable: One problem in three forms, naive, memoised and tabulated, with call counts for each and the input length at which recursion depth becomes the binding constraint.
Day 5 — Graphs, where most of the work is choosing the traversal
- Implement BFS and DFS over one adjacency list, then answer for each which finds a shortest path in an unweighted graph and which you would use to detect a cycle in a directed graph, including why the in-progress versus finished distinction matters for the second.
- Implement topological sort by in-degree, feed it a graph containing a cycle, and confirm the failure signature is that fewer than V nodes come out rather than an exception, then note that the order it produces is one of several valid ones.
- Run a shortest-path search on a graph with a single negative edge weight and show the wrong answer, then write the precondition Dijkstra actually needs, non-negative weights, because it finalises a node's distance the first time that node is popped, and name the algorithm you would switch to and its own limit.
Deliverable: A small graph library with BFS, DFS and topological sort, plus two inputs that produce documented wrong answers under the wrong algorithm choice.
Day 6 — One day for everything that is not an algorithm
- Sketch one system only to the depth a coding-heavy loop tends to reach: the endpoints, what the service stores, and the single query pattern that decides the schema. Stop at twenty-five minutes.
- Prepare the project answer for an interviewer who codes, which means rehearsing the two levels they push to: the specific thing you built, and why you chose that approach over the alternative they will name. Open with a number and be ready to say what it excludes.
- Prepare the answer to what you would do differently, choosing a real technical mistake with a specific fix rather than a complaint about process or staffing.
Deliverable: One design sketch at endpoint-and-schema depth, plus a project answer rehearsed to two levels of follow-up.
Day 7 — Solve out loud, under time
- Do three timed problems at twenty-five minutes each in a plain editor with no autocomplete and no execution until the end, then tally separately the failures that were syntax and the ones that were approach, because those two numbers call for different fixes.
- Narrate one solution from the first sentence, stating the approach and its complexity before writing any code, and rehearse the sentence you will use when you realise mid-solution that the approach is wrong.
- Re-solve from blank the two problems you were slowest on this week and compare the times against the day they first appeared.
Deliverable: A recording of one fully narrated solution and a tally that separates syntax failures from approach failures.
'We rewrote it in a better framework' is not a story
A migration is a cost you chose to pay, not an achievement. The story is what the old system made expensive, what you measured before committing, what kept serving traffic during the cutover, and what you would have done if the numbers had come back flat. Without those, a rewrite reads as taste.
What is your experience working with teams of varying sizes?
Medium · behavioural and engineering judgement
Approach
- Give the blast radius: what could have broken, and what you measured.
- Name the disagreement and how you resolved it with evidence.
- Close with what you would do differently, concretely.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?
What attracts you to the projects and culture at Behaviour Interactive?
Medium · behavioural and engineering judgement
Approach
- Close with what you would do differently, concretely.
- Name the disagreement and how you resolved it with evidence.
- Pick a story where you made the decision, not one where you watched it.
Follow-up
- What would you do differently if you ran that again?
- How did you know your change caused the improvement?
How do you manage your professional expectations regarding your role and growth?
Medium · behavioural and engineering judgement
Approach
- Close with what you would do differently, concretely.
- Name the disagreement and how you resolved it with evidence.
- Pick a story where you made the decision, not one where you watched it.
Follow-up
- How did you know your change caused the improvement?
- What would you do differently if you ran that again?