Interview guides · Artemis
Artemis Software Engineer Interview Guide
Artemis Software Engineer interview guide: take-home and pairing rounds, with prep for refactoring, debugging, pagination and behavioral questions.
The 60-second brief
Reports for this loop do not name an Artemis product or domain, and this guide does not guess at one. What candidates describe is a product engineering role: building and maintaining features on web applications, working with product managers and designers, and owning code through automated tests, production monitoring and on-call rotations. For preparation, treat it as general web application and API engineering, where the code you submit as a take-home becomes the working material for the rest of the loop.
This guide is for engineers applying to Artemis as a Software Engineer. Candidates describe the experience bar as a few years of professional work, proficiency in at least one mainstream language (Python, JavaScript or TypeScript, Go and Java are the ones listed), comfort with REST API design and Git, and an ability to work closely with other people. The guide covers the three reported stages, the reported extension, debugging and migration scenarios, the behavioral prompts, and a seven-day plan that has you build a small project of your own and rehearse changing it live.
Candidates report three stages over about 3-5 weeks: a Recruiter Screen, a Take-Home Assignment, and Pairing Interviews in which you extend the take-home you submitted.
Focus areas
- Pair Programming
- Take-Home Assignment
- Practical Coding
Overview
The distinctive feature of this loop is that one piece of code carries it. You write a short, practical project at home, and the pairing interviews then work on that same codebase: adding a user roles feature to the data model, replacing fetch-everything with cursor-based pagination, merging duplicated components into one module, testing what happens when a third-party API returns a 500, and making a data-fetching function handle asynchronous work better. Candidates also report debugging scenarios (a crash on one specific input string, a memory leak to profile), a request to implement a debounce function for a search input, and a schema migration that must not cause downtime.
That shape changes how you should prepare. Candidates describe practical questions rather than standard algorithm drills, so hours spent on puzzle sets pay off less than hours spent building a small project, then changing it under observation. The project you submit has to be something you can navigate quickly and defend: you may be asked why you chose a framework, how you would raise test coverage in a module you wrote, or what you would change before deploying it. Candidates also report a tip to keep the take-home within its stated requirements, because extra machinery makes live refactoring harder.
The listed stack is deliberately broad: one modern language of your choice, REST API design, Git, and, as nice-to-haves, cloud platforms, Docker, Kubernetes and CI/CD pipelines. Less common scenarios candidates mention include resolving Git merge conflicts live, profiling the code you just wrote, containerising the submission, and discussing event-driven designs or availability. Pick the language where you can refactor and debug fastest, because the pairing sessions reward fluency in your own codebase more than breadth.
The behavioral prompts are conversational and concern disagreement, shifting requirements, shipping speed against code quality, feedback, and something you learned for enjoyment. Reports describe the pairing format as collaborative, with candidates asked to talk through their reasoning, so rehearse narrating out loud while you type. The seven-day plan alternates building, changing and explaining code for that reason.
The Artemis interview process
Candidates report three stages over about 3-5 weeks. The take-home is the common thread: it is written once, then reused in the pairing interviews.
Recruiter Screen
Candidates describe the Recruiter Screen as an opening conversation about your background, your career goals and what the role involves. It is also the moment candidates are advised to raise scheduling needs and location or working-arrangement preferences, since arrangements are reported to vary by team. Treat it as the place to learn the shape of the rest of the loop before you start building anything.
What to demonstrate
- How your background and career goals line up with a product engineering role that involves web applications and APIs
- Whether the role specifics and working arrangement suit you and the team
- How clearly you describe your own past projects, which sets up the take-home conversation later
How to prepare
- Prepare a short walk through your career that ends on one web application or API you built and maintained, with the language and tools you used.
- Decide which of the listed languages (Python, JavaScript or TypeScript, Go, Java) you will use, and ask whether the take-home allows free choice of language and framework.
- Ask how the take-home is submitted and how the three pairing interviews are structured, and state your location and scheduling constraints early.
Take-Home Assignment
The Take-Home Assignment is reported as a short, practical project that becomes the codebase for everything after it. Candidates are told to treat it like production work: a README that explains how to run it, sensible tests that pass, readable structure, and no more machinery than the requirements call for. Plan your time for the pairing sessions as well as the submission: whatever you write, you must be able to change quickly while someone watches.
What to demonstrate
- Code organization: how you separate concerns, lay out directories and modularize logic
- Testing practice: unit and integration tests that cover the core business logic
- Readability that lets another engineer follow the code without a long explanation
How to prepare
- Write the requirements as a checklist before coding and build only that; keep an optional-improvements list in the README instead of implementing it.
- Test the core logic with unit tests and add at least one integration test that stubs an external API failing with a server error, then be ready to explain how you would improve coverage of any module.
- Prepare spoken answers for why you picked your framework, how the code is structured, and what you would change before deploying it tomorrow.
- If you have time, add a Dockerfile or a CI workflow; candidates describe these as less common extras, so they come after clean structure and passing tests.
Pairing Interviews
The Pairing Interviews are three sessions in which you share your screen and work with an Artemis engineer on the take-home you submitted. Candidates report change requests such as a user roles feature and its data model, cursor-based pagination, merging duplicated components, an integration test for a failing third-party API, and more efficient asynchronous fetching. They also report debugging and design scenarios: a crash on a specific string, a memory leak, a debounce function, a zero-downtime schema migration, and explaining a design pattern to someone playing a junior engineer. Think out loud, ask for a hint when stuck, and discuss trade-offs before typing.
What to demonstrate
- Communication: explaining your reasoning while moving through the codebase
- Refactoring existing code to accommodate new feature requests
- Debugging a defect that appears during live development
- Discussing how the application would scale, covering API shape, caching, indexing and asynchronous work
How to prepare
- Reread your own submission the day before and rehearse changing it: add a field with a migration, paginate a list endpoint, extract a shared module, and add a failing-then-passing test.
- Practice debugging out loud: reproduce first, reduce to the smallest failing input, state a hypothesis, then fix and add a regression test.
- Prepare short answers on what breaks first under heavy load in your project, and how a cache, an index or a background queue would help.
- Review Git merge conflict resolution and one profiler in your language, since candidates describe both as less common but possible.
Where candidates lose points
Coding silently during a pairing session and surfacing only when the code is finished
State your plan before typing, name the trade-off between two options, and say what you expect a run or a test to show. Candidates report that thinking out loud and asking for a hint when stuck are expected in the pairing format.
Submitting a take-home with extra layers that you then struggle to change live
Build exactly the listed requirements, keep the structure flat enough to navigate quickly, and put extra ideas in a README section on next steps. You will be asked to add roles, paginate and merge duplicated code in that same project, so every abstraction must earn its place.
Being unable to explain your own take-home: why this framework, where the tests are thin, what breaks at load
The day before the pairing sessions, reread the whole submission and write three answers: the architecture and why, the weakest-tested module and how you would fix it, and the first component that fails at ten thousand concurrent users, with the cache, index or queue you would add.
Debugging a crash or a leak by editing code at random instead of working a sequence
For the crash on a specific string, reproduce it, shrink the input, read the stack trace, form a hypothesis, fix it and add a regression test. For the leak, take two heap snapshots or memory profiles under steady load, compare what grows, and look at unbounded caches, listeners that are never removed and closures holding large objects.
Treating the migration, pagination and API-test scenarios as one-step changes
Describe a migration as additive steps: add the nullable column, deploy code that writes both, backfill in batches, switch reads, then remove the old path later. Paginate with a stable key and an opaque cursor, not an offset, and test the failing-third-party case with a stub that returns a 500 and an assertion on what the user sees.
Questions & practice
"How would you modify this function to handle asynchronous data fetching more efficiently?"
Medium · Take-Home Extension & Refactoring
Approach
- Read the function for sequential awaits on independent requests, typically an await inside a loop. That waterfall makes total latency the sum of the calls. Running them concurrently with Promise.all (JS) or asyncio.gather (Python) brings it close to the slowest single call.
- Bound the concurrency with a semaphore (p-limit, asyncio.Semaphore) so a large list does not overwhelm the upstream or trip rate limits. Better still, use a batch endpoint that takes many ids in one request if one exists.
- Pick failure semantics on purpose. Promise.all rejects on the first rejection but does not cancel the other requests. Promise.allSettled returns every outcome. asyncio.gather(return_exceptions=True) does the same in Python. Decide whether partial results are acceptable.
- Remove waste: dedupe in-flight requests by caching the promise per key, add timeouts, and cancel requests whose results are no longer needed with AbortController.
- Measure before and after. Confirm the requests really are independent: if one call needs another's result, that dependency has to stay sequential.
Follow-up
- The upstream allows only a fixed number of requests per second. How does your code respect that?
- How do you cancel outstanding requests when the user navigates away?
- Where would you cache these results, and how would you invalidate them?
"Let's pair on implementing a debounce function for the search input you built."
Medium · Pair Programming & Debugging
Approach
- State the semantics first. Debounce runs the function only after a set wait has passed with no new calls, so every call resets the timer and the last arguments win. Throttle, by contrast, runs at most once per interval.
- Core implementation: a closure holds the timer id, and the returned function calls clearTimeout(timer) and then timer = setTimeout(() => fn.apply(this, args), wait). Use a regular function rather than an arrow function for the wrapper so
thisis preserved. - Add the useful extras: cancel() and flush() methods, and leading or trailing options. In React, create the debounced function once (useMemo or useRef) instead of on every render, and cancel it in the useEffect cleanup on unmount.
- For the search box specifically, debouncing cuts the number of requests but does not fix out-of-order responses. Abort the previous fetch with AbortController or drop responses whose query no longer matches the input. Skip empty or whitespace-only queries.
- Test with fake timers (vi.useFakeTimers or jest.useFakeTimers plus advanceTimersByTime). Assert that a burst of calls produces exactly one invocation, with the last arguments.
Follow-up
- How would you add a maxWait so results still update during continuous typing?
- Implement throttle and explain when you would choose it over debounce.
- A response for an older query arrives after the newer one. What does the user see, and how do you prevent it?
Identify the heaviest tenants in a five-minute window under memory pressure
Medium · top-k · heavy hitters · streaming
The edge service handles about 3,000 requests per second across roughly 50,000 tenants, peaking near 9,000. Expose the 50 heaviest tenants by request count over the trailing five minutes so limits can be tightened before one tenant's backfill starves the fleet. You may not retain five minutes of raw records. Give the exact solution and its memory, then the bounded-memory approximation with its error stated as a formula, and say which you would ship and at what tenant cardinality that choice changes.
Approach
- Do the exact version first, because it is affordable at this cardinality: a ring of 300 one-second counters per tenant, advanced lazily, is 1,200 bytes of counters per tenant and roughly 60 to 90 MB for 50,000 tenants with overhead. Carry a running total and subtract the bucket you overwrite so a window read is O(1) rather than 300 adds.
- Extract the top 50 with a size-k min-heap over the tenant sums: O(d log k) for d tenants, against O(d log d) to sort them all. Maintaining the heap continuously instead of on query requires a tenant-to-heap-index map, because incrementing a count already inside the heap means sifting from a known position, and without that map you rebuild the heap on every request.
- State the approximation precisely rather than gesturing at sketches. Misra-Gries with m counters retains every item whose true count exceeds N/(m+1), and each retained count underestimates the truth by at most N/(m+1). With m = 1,000 and N = 900,000 requests in the window the error is roughly 900 requests, which is fine for spotting a tenant sending 50,000 and useless for ranking two tenants 200 apart.
- Say what breaks when the window slides: Misra-Gries and Space-Saving are insert-only and cannot be decremented as records age out. The workable construction is one summary per sub-window, say ten seconds, with 30 summaries merged at query time, and the merged error is the sum of the per-summary errors, so the bound degrades linearly in the number of sub-windows.
Follow-up
- The heaviest tenant is heavy because of one export job rather than user traffic. Should the limiter treat those as the same tenant?
- Two tenants sit tied at the boundary of the top 50. Does your answer flap, and does the flapping matter?
"We need to update the database schema without causing downtime. How should we approach writing this migration
Medium · Pair Programming & Debugging
"We need to update the database schema without causing downtime. How should we approach writing this migration?"
Approach
- Name the constraint: during a rolling deploy, old and new code run against the same schema at the same time. Every step must therefore stay compatible with both versions. This is the expand/contract (parallel change) approach.
- Walk through a concrete case, such as renaming a column. Add the new nullable column, deploy code that writes to both, backfill, switch reads to the new column, stop writing the old one, and drop it in a later release.
- Know which DDL takes heavy locks (PostgreSQL examples). ADD COLUMN without a default is metadata-only, and since PostgreSQL 11 a constant default is too. Use CREATE INDEX CONCURRENTLY, which cannot run inside a transaction block, so the migration tool's wrapping transaction must be disabled. Add NOT NULL through a CHECK constraint created NOT VALID and then validated.
- Set lock_timeout on migration sessions so a blocked ALTER fails fast instead of queuing every query behind it. Backfill in small primary-key-range batches, each in its own short transaction, and make the backfill safe to re-run.
- Keep a rollback for each step. Run migrations separately from the application deploy, and rehearse on a production-sized copy to see real lock and run times.
Follow-up
- How do you rename a column that several services read?
- The backfill on a large table is slowing live queries. What do you change?
- After the contract step has run, how do you roll back?
Stop tag and share joins from fanning out a page
Medium · join fan-out · semi-joins · many-to-many
resource_tag is (resource_id, tag_id) with PK (resource_id, tag_id); resource_share is (resource_id, shared_with_user_id, permission). The tagged-and-shared listing inner-joins resource to both, filters tenant_id, tag_id = ANY($2) and shared_with_user_id = $3, orders by updated_at DESC and takes 50. Pages come back with fewer than 50 distinct resources and the total in the header is far too high. Explain the row multiplication, rewrite both the page query and the count query so each is correct, and name the index each one needs. PostgreSQL 16.
Approach
- Do the arithmetic against the predicates that are actually there. An inner join emits one row per matching child row, and both joins are filtered: tag_id = ANY($2) admits only the requested tags, shared_with_user_id = $3 admits one user's share rows. So a resource holding three of the requested tags and shared with $3 once yields three rows, not one — the multiplier is its count of matching tags times its share rows for that single user, and that second factor is 1 unless the table admits duplicate (resource_id, shared_with_user_id) pairs. LIMIT 50 then limits rows rather than resources, and COUNT(*) counts pairs — the header is the product, not the population.
- Reject DISTINCT as the fix. It deduplicates after the product has been built, so the planner must materialise and sort the fanned-out set before the LIMIT can apply, and it leaves any SUM or AVG in the same select list wrong.
- Rewrite both filters as semi-joins, keeping resource as the only row source: AND EXISTS (SELECT 1 FROM resource_tag rt WHERE rt.resource_id = r.resource_id AND rt.tag_id = ANY($2)) and the same shape against resource_share. A semi-join stops at the first match per resource and preserves the driving index order, so ORDER BY updated_at DESC, resource_id DESC LIMIT 50 still stops after 50 rows.
- Count with the same predicates and no join at all: SELECT count(*) FROM resource r WHERE r.tenant_id = $1 AND r.status = 'active' AND EXISTS (...) AND EXISTS (...). Nothing multiplies a resource, so the number is the population.
Follow-up
- The filter changes from 'any of these tags' to 'all of these tags'. Rewrite it and state what it costs relative to the ANY form.
- A resource can be shared with the same user twice under different permissions. Does your count change, and should it?
"We want to add a new 'user roles' feature to the project you submitted. How should we update the data model t
Medium · Take-Home Extension & Refactoring
"We want to add a new 'user roles' feature to the project you submitted. How should we update the data model to support this?"
Approach
- Clarify the shape of roles first: one global role per user, several roles per user, or roles scoped to a resource such as an organization or project. Also ask whether the role set is fixed or editable by admins. Each answer leads to a different schema.
- If there is exactly one global role and the set is fixed, a NOT NULL role column on users with a default and a CHECK or enum constraint is enough. For many roles per user, add a roles table and a user_roles join table with a composite primary key (user_id, role_id), foreign keys and an index on role_id.
- If permissions need to change without code changes, go one level further to RBAC: roles -> role_permissions -> permissions. Code then checks a permission such as 'project:edit', not a role name. Scoped roles become a memberships(user_id, scope_id, role) table.
- Keep every authorization decision in one policy function or middleware instead of scattered role == 'admin' checks. That keeps the new feature to a schema change plus one module.
- Migration and tests: create the tables, backfill existing users to a default role, then enforce. Test every role against every protected endpoint, including the 'last admin removes their own role' edge case.
Follow-up
- If roles are stored in a session or JWT, how do you make a revoked role take effect before the token expires?
- How would the schema change if a user can be an admin in one project and a viewer in another?
- How do you stop the last remaining admin from being demoted or deleted?
"Your current implementation fetches all records at once. Let's pair on implementing cursor-based pagination."
Medium · Take-Home Extension & Refactoring
Approach
- Say why offset pagination falls short here. OFFSET n still reads and discards n rows, so deep pages get slower. Rows inserted or deleted between requests also shift the window, which causes duplicates or skipped items.
- Use keyset pagination on a total order: ORDER BY created_at DESC, id DESC LIMIT :size + 1 with WHERE (created_at, id) < (:last_created_at, :last_id). The unique id breaks ties. If the database lacks row-value comparison, expand it to created_at < x OR (created_at = x AND id < y).
- Back the query with a composite index on (created_at, id) that matches the sort, so each page is an index range scan whatever its depth.
- Fetch size + 1 rows to compute has_more without a COUNT. Return next_cursor as an opaque base64 encoding of the last row's (created_at, id), and reject a malformed cursor with a 400. Clamp the page size on the server.
- On the client, append pages, dedupe by id and stop on an empty or final page. The trade-off: no jumping to page N, and an exact total count costs a separate query.
Follow-up
- How would you add a 'previous page' cursor?
- What breaks if the list is sorted by a mutable field such as score or updated_at?
- What should happen if a client changes the filters but sends an old cursor?
"I see some duplicated logic in these two components. Can we refactor them into a single, reusable module?"
Medium · Take-Home Extension & Refactoring
Approach
- Before moving any code, pin the current behaviour of both components with characterization tests. The refactor then passes or fails on evidence.
- Diff the two carefully and separate logic that is genuinely the same from code that only looks alike. Real differences become parameters or callbacks. Coincidental similarity is a reason to leave the code alone.
- Extract the shared part as the narrowest unit that works: a pure function, a custom hook or a small module. Migrate one caller at a time and run the tests after each step, in small commits.
- Watch for the wrong abstraction. If the shared module needs boolean flags for each caller's quirks, composition (passing in the differing piece) is usually cleaner than branching inside it.
- Check what does not show in a quick diff: side effects, error handling, loading states and naming. Make sure the extracted module's name describes what it does, not where it came from.
Follow-up
- When would you deliberately leave this duplication in place?
- A third caller needs slightly different behaviour. How do you support it without adding a flag?
- Where in the project structure does the shared module live, and why?
"Let's write an integration test for the edge case where the third-party API returns a 500 error."
Medium · Take-Home Extension & Refactoring
Approach
- Decide the intended behaviour before writing the test. Options include retrying a 5xx a bounded number of times with backoff, then returning a clear error or a fallback to the caller. The test asserts that decision, so it has to exist first.
- Stub at the HTTP boundary, not inside your own code. Run the real client against a fake server or an interception library (nock for Node, responses for Python requests, respx for httpx, or WireMock). That is what makes it an integration test rather than a unit test with a mocked function.
- Assert observable outcomes: the status and body your endpoint returns, how many upstream calls were made (for example, 500 twice then 200 when testing retries), no partial database writes, and the log line or metric you emit.
- Keep it deterministic and fast. Inject the clock or backoff delay so the test does not sleep, block real network access, and reset the stubs between tests.
- Cover the neighbours too: a timeout, a 429 with Retry-After, a 200 with malformed JSON, and a 4xx that must not be retried.
Follow-up
- How do you test the timeout path without slowing the suite?
- Is it safe to retry a POST that returned 500? What would make it safe?
- How do you keep your stub in step with the real API's responses over time?
"What is a technical concept or tool you learned recently purely for fun, and why did it interest you?"
Medium · Behavioral & Culture Fit
Approach
- Pick something you actually used hands-on, not a term you only read about. A small project or experiment gives you concrete details to discuss when pressed.
- Explain it at two depths. First one plain sentence on what it is, then the mechanism underneath. For example: 'CRDTs let replicas merge edits without coordination because every merge operation is commutative, associative and idempotent.'
- Say why it caught your interest: the problem it solves, what surprised you, or how it changed the way you think about something you already do.
- Name its limits and trade-offs and when you would not use it. That shows understanding rather than enthusiasm alone.
- Tie it back briefly to everyday engineering, such as a bug class it avoids or a tool you now reach for, without claiming it belongs in every codebase. The answer falls apart when it rests on a buzzword you cannot explain one level deeper.
Follow-up
- What broke or surprised you when you tried it?
- When would you not use it?
- Explain it to a teammate who has never seen it, using an example from your take-home.
"The application is crashing when we input this specific string. Let's step through the code together to find
Medium · Pair Programming & Debugging
"The application is crashing when we input this specific string. Let's step through the code together to find out why."
Approach
- Reproduce it with the exact string, capture the full stack trace, and turn the reproduction into a failing test before changing anything.
- Shrink the input by halving the string until the smallest version that still crashes is left. Then name the trigger. Usual suspects: an empty string, multi-byte Unicode or emoji (JS length counts UTF-16 code units, so slicing can split a surrogate pair), quotes or regex metacharacters, very long input, and leading or trailing whitespace.
- Set a breakpoint at the top frame of the trace and step backwards through the assumptions. Look for an index past the end, a regex match or parse that returned null, None or NaN and was used anyway, or user input built into a regex or query without escaping.
- Fix the broken assumption where it lives, and add validation or normalization at the input boundary as well. Keep the failing test as a regression test, and search for other callers with the same assumption.
- Narrate each hypothesis and what the next step will confirm or rule out, so your pair can follow the search.
Follow-up
- It crashes in production but not locally with the same string. What do you check next?
- Should the fix reject this input or handle it, and who decides?
- What kind of test would have caught this before it shipped?
"We have a memory leak in this module. Walk me through how you would profile and diagnose the issue."
Medium · Pair Programming & Debugging
Approach
- Confirm it is a leak first. Under a steady, repeatable load, memory after garbage collection should keep climbing rather than level off. A cache warming up or a one-time high-water mark is not a leak. Build a loop that exercises only this module.
- Use the runtime's heap tools and compare two snapshots taken some iterations apart. Node: heap snapshots in Chrome DevTools via --inspect, inspecting the retainers of growing objects. Python: tracemalloc snapshots with snapshot.compare_to(old, 'lineno'). JVM: a heap dump in Eclipse MAT. Go: pprof heap profiles.
- Check the usual causes. A map or cache keyed per request that is never evicted. Event listeners, subscriptions or setInterval timers that are never removed. Closures holding large objects. Module-level lists that only grow. Connections or file handles that are never closed.
- Fix at the owner: a bounded LRU with a size cap or TTL, WeakMap or weakref where the cache should not keep keys alive, and explicit cleanup (removeEventListener, clearInterval, a useEffect cleanup function).
- Verify with the same load loop: the post-GC baseline should now stay flat. Then add a memory metric or test so the leak cannot quietly return.
Follow-up
- RSS stays high even after the leak is fixed. How do you tell fragmentation from a leak?
- How would you find this in production without restarting the service?
- How do you guard against the regression in CI?
"I'm going to play the role of a junior engineer. Can you explain to me why you chose this specific design pat
Medium · Pair Programming & Debugging
"I'm going to play the role of a junior engineer. Can you explain to me why you chose this specific design pattern?"
Approach
- Lead with the problem the pattern solved in your own take-home, not the textbook name. For example: 'I put storage behind a repository because the handlers were mixing SQL with business rules.'
- Show the alternative and what it would cost: the conditional chain or direct database calls you avoided, and which change is now cheaper, such as swapping storage or testing against an in-memory fake.
- Be honest about the cost: an extra layer of indirection and more files. Say when the pattern would be over-engineering and what would make you remove it.
- Check understanding instead of lecturing. Ask the 'junior' where a new feature would go, use plain words, and point at real lines of code.
- Know the common patterns well enough to explain the change each one isolates: repository (persistence), adapter (a third-party interface), strategy (interchangeable behaviour), observer (event fan-out), factory and dependency injection (construction and wiring).
Follow-up
- The junior engineer suggests removing the pattern because it adds files. How do you respond?
- Where in your project would this pattern be overkill?
- Show the benefit with a concrete code change rather than a diagram.
Suggested practice checklist
The plan follows the loop: build a small project the way the take-home asks, then spend the remaining days changing it the way the pairing interviews do. Behavioral rehearsal sits on the last day, after the technical stories are fresh.
Day 1 — Map the loop and pick your stack
- Write the three reported stages in order (Recruiter Screen, Take-Home Assignment, Pairing Interviews) and note which scenarios you expect to meet in the third.
- Choose one language from the listed set (Python, JavaScript or TypeScript, Go, Java) and a web framework you can use without looking things up.
- Write a two-minute career summary that ends on a web application or API you shipped, and a list of questions for the recruiter about the take-home format and language choice.
Deliverable: A one-page loop map, a chosen language and framework, a spoken career summary and a list of recruiter questions.
Day 2 — Build a take-home style project
- Create a small REST API with a list view and a simple create or update endpoint on a topic of your choice, built only from a requirements checklist you write first.
- Add unit tests for the core logic, one integration test, and a README covering how to run it, how it is structured and what you would improve next.
- Optionally add a Dockerfile or a CI workflow that runs the tests.
Deliverable: A runnable repository with a requirements checklist, passing tests and a README with a next-steps section.
Day 3 — Data model changes and cursor pagination
- Add a user roles feature to your project: design the tables (a role table and a user-to-role link versus a single role column), say why, write the migration, and write one permission check.
- Replace the fetch-everything list with cursor-based pagination on a stable sort key plus a unique tiebreaker such as created time and id, with an opaque cursor, and test the first page, last page and an insert between pages.
Deliverable: A migration, a roles design explanation and a paginated endpoint with tests for the boundary cases.
Day 4 — Refactor, test the failing API, and fetch asynchronously
- Introduce deliberate duplication into two components, then extract a single shared module and keep the original tests passing; practice explaining each step aloud.
- Write an integration test where a stubbed third-party API returns a 500, and decide what your application should return and log.
- Rewrite sequential data fetching to run concurrently with a cap on parallel calls and per-call error handling, and compare the behaviour with the original.
Deliverable: A merged shared module, a passing 500-error integration test and a concurrent fetch with a documented error policy.
Day 5 — Debugging, memory and the debounce function
- Plant a crash that depends on one input string, then debug it aloud from reproduction to regression test.
- Plant a memory leak, such as a cache with no eviction or listeners that are never removed, and diagnose it by comparing two memory snapshots in your language's profiler.
- Implement a debounce function for a search input with trailing and cancel behaviour and test it with fake timers.
Deliverable: A written debugging log for the crash, a before-and-after memory comparison for the leak, and a tested debounce function.
Day 6 — Migrations, design rationale and scale
- Write a migration plan for adding a required column to a populated table without downtime: additive step, dual write, batched backfill, read switch, cleanup, and the rollback at each step.
- Explain one design pattern from your project as if to a junior engineer: the problem, the alternative you rejected and when you would not use it.
- List what breaks first in your project at ten thousand concurrent users, and sketch the cache, index or background job you would add.
Deliverable: A migration plan with rollbacks, a one-minute spoken design explanation and a ranked bottleneck list with a fix for each.
Day 7 — Behavioral stories and a full mock pairing
- Write one story each for a technical disagreement, a mid-project requirements change, a quality-versus-speed trade-off and a piece of feedback that changed how you work, in first person with the outcome stated.
- Prepare a genuine answer on something technical you learned for enjoyment.
- Run a mock session: open your project cold, ask a friend to request a new feature and a bug fix, and narrate everything as you go.
Deliverable: Four written stories, one answer about a topic you learned for fun, and a recorded or observed mock pairing session with a list of fixes.
Behavioral prompts for the Artemis loop
Candidates report behavioral prompts covering technical disagreement, changing requirements, speed against code quality, feedback, and something technical learned for enjoyment. The pairing-heavy format means the stories should show how you work with other engineers: how you argue a position, take feedback and keep a shared codebase healthy. Use first-person stories with a specific decision and a result.
- "What is a technical concept or tool you learned recently purely for fun, and why did it interest you?"
- Tell me about a time a code review or a pairing session changed your approach to a problem.
"Tell me about a time you disagreed with a technical decision made by your team. How did you handle it?"
Medium · Behavioral & Culture Fit
Approach
- Choose a real technical decision, such as a data store, an API shape, a framework or a rollout plan, where you held a position and the team leaned another way. A style-preference disagreement gives the story nothing to prove.
- Spend the middle of the story on your reasoning: what you wanted, what the other side wanted, and what evidence you brought, such as a benchmark, a prototype, an incident history or a cost estimate. For example, "I wrote a short comparison of both options and ran the one risky query against production-sized data."
- State how it resolved. Either the evidence moved the team, or you were persuaded, or you committed to the decision after being heard and made it succeed. All three are credible; a story where everyone eventually agreed with you is not.
- Close with what changed afterwards: a decision record, a revisit date, or a different way of raising concerns early. The mistake that sinks this answer is describing colleagues as wrong or uninformed rather than describing the trade-off.
Follow-up
- What if the team had still chosen the other option after you made your case?
- Was there a point where you realised you were wrong, or partly wrong?
- How did you keep working with that person afterwards?
"Describe a project where the requirements changed drastically mid-flight. How did you adapt?"
Medium · Behavioral & Culture Fit
Approach
- Pick a project where the change was large enough to invalidate existing work, such as a new user type, a different data source or a cancelled feature, and name what was already built when it happened.
- Show your triage: what survived, what you cut, what you rebuilt, and how you decided. Mention concrete moves like freezing in-flight branches, splitting remaining work into small shippable pieces, and writing tests around the code you kept so you could change it safely.
- Show communication with the product manager and designers: you explained the cost of the change in terms of scope and risk, offered a smaller first version, and agreed on what would not ship.
- Finish with what you learned about making the next change cheaper, for example narrower interfaces, feature flags or migrations that can be reversed. Avoid blaming product for the change or presenting the whole experience as stress with no decisions in it.
Follow-up
- What did you throw away, and how did you decide it was not worth keeping?
- How did you explain the cost of the change to the people who asked for it?
- What would you build differently so the next requirement change hurts less?
"How do you balance the need to ship a feature quickly with the desire to write perfect, technically debt-free
Medium · Behavioral & Culture Fit
"How do you balance the need to ship a feature quickly with the desire to write perfect, technically debt-free code?"
Approach
- Answer with a principle, then back it with an example. A workable principle is to set the quality bar by blast radius and reversibility: payment logic, schema changes and public API contracts get full care, while an internal view that is easy to replace can ship with known shortcuts.
- Separate debt you take on deliberately from sloppiness. Deliberate debt is written down with a ticket, an owner and a trigger for repaying it. A non-negotiable floor applies either way: tests around core logic, names that make sense, and migrations that can be rolled back.
- Give one example where you shipped with a known shortcut, say what you recorded, and say whether and when you repaid it. For the take-home context, connect to the idea of keeping scope to the requirements and listing improvements instead of building them speculatively.
- Avoid the two bad extremes: claiming you never compromise, which sounds unrealistic, or claiming speed always wins, which ignores the people who maintain the code after you.
Follow-up
- Tell me about a shortcut you took that you later regretted.
- How do you decide a change is below your minimum bar and must not ship?
- How would you explain this trade-off to a product manager who wants the feature this week?
"Tell me about a piece of feedback you received recently that changed the way you work."
Medium · Behavioral & Culture Fit
Approach
- Choose feedback that was specific and mildly uncomfortable, such as pull requests that were too large to review, blockers raised too late, or a habit of working silently during pairing. Avoid disguised strengths like working too hard.
- Tell it in sequence: what was said, how you reacted in the moment, what you changed in your habits, and the visible result. For example, "I began splitting changes under one concern each, and review turnaround on my pull requests improved."
- Show that the change stuck by naming how you now check it, perhaps asking reviewers one targeted question per pull request or inviting feedback after a pairing session.
- Do not pick feedback you privately rejected, and do not turn the story into a complaint about the person who gave it. For a loop built on pairing, receptiveness is the point of the story.
Follow-up
- How did you react when you first heard it?
- How do you ask for feedback now, and from whom?
- Is there feedback you have received that you decided not to act on, and why?