The source notes describe the Software Engineer role at Notion as product engineering: building the underlying architecture and the user-facing features of the editor and the tools around it, and working with product managers and designers to turn user needs into technical specifications. The same role description mentions peer code review and on-call rotations for diagnosing production issues.
The reported questions reflect that mix. On the implementation side, candidates report being asked how to rate-limit an API, how to find out why a React component loads slowly, how to keep data consistent in a distributed system, and how they approached refactoring a legacy codebase. On the design side, they report real-time collaboration on one document, a notification service for millions of users, relational versus NoSQL storage, and monitoring for a high-traffic service. The PracHub bank adds problems tagged to this company and role: a text editor with undo/redo, a to-do-list object model, a recursive React JSON viewer, table aggregation, top errors in a time window, and a shared calendar service.
For preparation, practice explaining why you made a technical choice, as well as what you chose. Most of the reported design prompts have more than one reasonable answer. An answer that names the trade-off, says which user-visible behavior it protects, and says what it costs will hold up better under follow-up questions than one that just names a technology.
Technical Screen
reportedCandidates report that this round is an initial technical assessment of coding and problem-solving. The reported coding questions are practical: a rate limiter for an API, finding the slow part of a React component, keeping data consistent across services. The question list in the source notes also includes standard problems, such as the first unique character in a string, the lowest common ancestor in a binary tree and checking a Hankel matrix, plus a chess game design. The PracHub bank for the role has object-modeling tasks, such as a to-do list and a text editor with undo/redo. Prepare for both kinds. Say what the input looks like before you choose a structure. Name the simple solution and its complexity before you improve it. Then write code another engineer could maintain, with clear names and small functions.
What to demonstrate
- Whether you can turn a loosely worded practical prompt, such as rate-limiting an API, into a concrete interface with stated inputs, outputs and failure behavior before writing code
- Whether your data-structure choice follows from the operations the problem needs, for example a frequency map for first-unique-character or two stacks for undo/redo
- Whether the code you write is clean and testable, and whether you check it against edge cases yourself instead of waiting for them to be pointed out
How to prepare
- Solve the standard problems from the source notes' question list (first unique character, lowest common ancestor, Hankel matrix check) and say the time and space complexity out loud before writing each one
- Implement a text editor with undo/redo and a to-do-list object model from a blank file, then add one feature to each and check how much of your design had to change
- Implement a token-bucket rate limiter as a class with an injectable clock, and write tests for burst, refill and the boundary where a request is exactly at capacity
System Design Interview
reportedCandidates report this as an in-depth discussion of system design principles and architecture. The reported design questions include real-time collaboration for multiple users on one document, a notification service that scales to millions of users, relational versus NoSQL storage, database structures for a collaborative editor, and monitoring for a high-traffic service. The source notes list these topics for the area: sharding and partitioning, caching and latency, and event-driven architectures with message queues. Ask for the scale and access pattern first. Separate the read path from the write path. Name the consistency you need and where staleness is acceptable. Then go deep on the hardest part, such as concurrent edits for the collaboration problem or fan-out and retries for notifications.
What to demonstrate
- Whether you settle requirements and scale before drawing components, and size the simplest design that meets them
- Whether you can explain how concurrent edits to one document converge, for example operational transformation versus CRDTs, and what each costs
- Whether you name the failure modes (a slow consumer, a duplicate delivery, a partition) and describe the recovery path for each
- Whether you can justify a storage choice by the queries it makes cheap and the ones it makes expensive
How to prepare
- Design real-time collaboration end to end: the operation format, how the server orders operations per document, how an offline client reconciles, and how a new client loads a snapshot plus recent operations
- Design the notification service with fan-out through a queue, per-channel workers, idempotency keys, user preferences and retry with backoff, then say what you drop first under overload
- For monitoring, write down the few service-level indicators you would alert on, where each is measured, and which dashboards or traces you would use to diagnose rather than to page
Product Vision Discussion
reportedCandidates report this round as an exploration of your understanding of product vision and alignment with company goals. The sources give no format, so prepare material that works as either a conversation or a structured exercise. Use the product for real work before the round. Form a specific opinion about one feature: who it serves, what is hard about it technically, and what you would change. Connect each opinion to an engineering consequence. The source notes also list product and technical strategy topics, such as balancing speed against technical debt and building a custom solution versus using an existing library; they do not tie them to a round, but they are useful material for this conversation.
What to demonstrate
- Whether your view of the product is concrete, tied to a specific workflow or feature rather than to general praise
- Whether you connect a technical trade-off to what a user experiences, such as latency while typing, sync conflicts or permission surprises
- Whether you can prioritize: what you would build first, what you would defer, and what signal would change your mind
How to prepare
- Use the product for a real task over several days, and write down three frictions you hit, each with a likely technical cause and a fix you could scope
- Prepare one feature proposal on a single page: the user problem, the smallest version worth shipping, the main technical risk and how you would measure whether it worked
- Rehearse a build-versus-buy answer with a real example from your own work, including the cost you accepted on the side you chose
Leadership Round
reportedCandidates report this as a final assessment of leadership and the broader business impact of engineering work. The sources do not tie specific questions to this round, but the reported behavioral questions are the closest match to its description: a disagreement with a product manager and how you resolved it, a project with a lot of ambiguity, balancing fast shipping against technical debt, your approach to mentoring junior engineers and building an inclusive team, and why you want to work at Notion. Pick stories where you made the decision. Give the business or user result along with the technical one. Keep the facts consistent with anything you said in earlier rounds.
What to demonstrate
- Whether your stories show you making and owning a decision, including the alternative you rejected and why
- Whether you can explain the business or user impact of engineering work, not only the technical outcome
- Whether you describe disagreement and ambiguity with a concrete resolution, and state accurately what other people contributed
How to prepare
- Write one story for each reported prompt (PM disagreement, ambiguity, speed versus debt, mentoring, legacy refactor), each with a measurable outcome and the decision that was yours
- For the technical-debt story, state the criterion you used to decide when debt had to be paid, not only the outcome
- Prepare a specific answer to why Notion, drawn from your product notes for the Product Vision Discussion, so the two rounds tell the same story
1 candidate reports. Individual accounts describe a particular role and hiring cycle.
Notion Data Engineer Interview Experience — DAG Coding, an Xfn Chat, and a Brutal Data Modeling Round
View report detailsPracHub editorial advice for the preparation topics above.
Answering the rate-limiter question with an algorithm name and no interface, state or failure behavior
Before coding, define what is limited (per user, per API key or per workspace), the limit and window, and what a rejected caller receives, typically HTTP 429 with a retry hint. Pick token bucket or sliding window and say why: token bucket allows controlled bursts, a sliding-window log is exact but stores every timestamp. Then say where the state lives once there is more than one server, and how an increment stays atomic there. Write it as a small class with an injectable clock so it can be tested.
Designing real-time collaboration without explaining how concurrent edits converge
A diagram of WebSockets and a database does not answer the question. Say how two users editing the same paragraph end with the same document. One option is a server that orders operations per document and transforms them (operational transformation). The other is data structures that merge without coordination (CRDTs). Explain what each costs in complexity and metadata. Cover the offline client that reconnects with queued edits, and how a new client loads a snapshot plus the operations after it.
Saying 'use strong consistency' or 'use NoSQL for scale' without naming what the data needs
For the reported data-consistency and relational-versus-NoSQL questions, name the guarantee each piece of data needs. A permission change may need read-your-writes. A view counter can be eventually consistent. Then name the mechanism that provides it: a single-leader transaction, an idempotency key on retried writes, or an outbox for events that must follow a commit. Justify a storage choice by the queries it makes cheap and the queries or joins it makes expensive.
Treating the Product Vision Discussion as a chance to praise the product
General enthusiasm is easy to say and hard to evaluate. Arrive with specific observations from using the product: a workflow that is slow, a sync or permission behavior that surprised you, a feature you would scope differently. Give the likely technical cause of each and what a first version of the fix would ship. When asked what to build, give a priority order and the signal that would change it.
Telling leadership stories in which the team did everything and your own decision is missing
For the reported prompts (PM disagreement, ambiguity, speed versus technical debt, mentoring), say what you decided, what you rejected, and what happened to users or the business as a result. Credit other people's parts accurately. Keep team size, timeline and your role the same in every telling, because the final round may go deeper on a project you already described.
Choose a category, try a prompt, then open its approach, worked solution or follow-up when you need it.
How would you design a rate-limiting mechanism for our API?
How would you design a rate-limiting mechanism for our API?
Approach
- Walk one small example through your approach before writing the whole thing.
- Name the brute-force solution and its complexity before improving on it.
- State the target complexity and say which constraint rules the naive version out.
Follow-up
- How does this change if the input no longer fits in memory?
- Which test case would catch an off-by-one here?
How do you ensure data consistency in a distributed system?
How do you ensure data consistency in a distributed system?
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?
- How does this change if the input no longer fits in memory?
Fold a deduplicated usage stream into hourly rollups
You are given one day of usage_event rows, up to 250 million, each carrying event_id, tenant_id, workspace_id, environment, sku, quantity numeric(20,6), idempotency_key, occurred_at and ingested_at. Produce usage_rollup_hourly cells keyed (tenant_id, workspace_id, sku, hour_start) with quantity_sum, event_count and source_max_ingested_at. An event counts once per (tenant_id, idempotency_key). The rollup grain has no environment column, so state your filter. One pass. Give your time and space bounds, and say what the deduplication actually costs in memory.
Approach
- Bucket on
occurred_at, neveringested_at:hour_start = date_trunc('hour', occurred_at at time zone 'UTC'). The two columns answer different questions.occurred_atsays which hour the customer is billed for;ingested_atsays how current the fold is. Using the second for the first makes late data invisible instead of correctable. - The fold is trivial and the deduplication is the entire cost, so price it before designing anything clever. An exact set over
(tenant_id, idempotency_key)at 250M entries, stored as a 16-byte 128-bit hash in an open-addressed table at 0.7 load factor, needs about 357M slots at 16 bytes each, roughly 5.7 GB. The fix is partitioning byhash(tenant_id) % Pso each shard holds 1/P of the set and no tenant's keys straddle shards. - Rule out a Bloom filter as a replacement, in the right direction: a false positive reports 'already seen' for an event never seen, so you drop a real event and lose revenue with no error raised. It is usable only as a negative pre-filter in front of the exact set, where a miss is conclusive and a hit must fall through to the real lookup.
- Accumulate in scaled integers, not binary floating point.
numeric(20,6)admits values below 10^14, so one event scaled to micro-units can reach 10^20, past int64's 9.22 x 10^18; use a 128-bit or arbitrary-precision accumulator unless you first bound the per-event maximum. binary64 represents integers exactly only to 2^53, about 9.01 x 10^15, and cannot represent 0.1 at all, so two runs that sum in different orders disagree. - Carry
source_max_ingested_at = max(ingested_at)over the events folded into each cell, and countevent_countover accepted, post-dedup events. Without that watermark there is no way to prove later what a number did and did not include, which is the first question any reconciliation asks. - State the environment filter explicitly, because the rollup grain cannot record it. A fold that quietly includes
stagingbills non-production traffic; one that quietly excludes it loses a cost signal. Production-only is the billing answer, and either way it belongs in the job name and the output metadata. Complexity: O(n) time, O(distinct dedup keys) space, dominated by the dedup set rather than by the cells.
Worked solution 25 min
- Write both key tuples down before any code: dedup key
(tenant_id, idempotency_key), cell key(tenant_id, workspace_id, sku, hour_start), withhour_startderived fromoccurred_atin UTC. - Build a 10,000-row fixture containing one event duplicated three times under the same
idempotency_key, two events sharing anidempotency_keyacross differenttenant_idvalues, one event whoseoccurred_atis two hours before itsingested_at, and onestagingevent inside an otherwise production cell. - Fold it and assert each of those four expectations separately rather than eyeballing a grand total.
- Re-run with the input shuffled and diff the output files.
- Size the dedup set for 250M keys using the load-factor arithmetic and write the number down next to the fixture.
Follow-up
- A producer retries at 23:59:59 and the retry lands at 00:00:01. The unique index on the daily-partitioned table must include the partition key. What gets double-counted, and what is the smallest change that fixes it?
- The consumer acknowledges its batch before committing the fold. Which failure loses revenue now, and which arrangement duplicates instead?
- What makes a re-run over the same day produce byte-identical rollups?
Model credential revocation so history survives the delete
tenant_api_key stores key_id, tenant_id, workspace_id, name, key_prefix, secret_hash, scopes text[], status (active, revoked, expired, compromised), auth_version, created_at, expires_at, last_used_at, revoked_at, revoked_reason. Rotation inserts a new row and revocation never deletes, because an incident review asks which credential served a request last quarter. Write the constraints that enforce: a label is unique only among a tenant's live keys, revoked_at and status can never disagree, and scopes is never empty. Then write the authentication lookup predicate, and name one column in this table that must stay out of it.
Approach
- Reach for a partial unique index rather than a plain UNIQUE:
create unique index on tenant_api_key (tenant_id, name) where revoked_at is null. Any number of revoked rows may share a label, the live namespace stays unique per tenant, and the revoked majority is not in the index at all, so it stays small on a table that only grows. - Tie the nullable timestamp to the enum so the two cannot drift:
check ((revoked_at is not null) = (status in ('revoked','compromised')))andcheck ((revoked_at is null) = (revoked_reason is null)). A revocation that records no reason is the one an incident review cannot use. - Write the emptiness check as
check (cardinality(scopes) > 0), notarray_length(scopes, 1) > 0. array_length returns NULL for an empty array, a CHECK constraint passes when its expression is NULL, so the array_length version accepts exactly the value it was written to reject. - Make the lookup a single index probe with every liveness condition inside it:
where secret_hash = $1 and revoked_at is null and (expires_at is null or expires_at > now()) and auth_version = $2, backed by a unique index on secret_hash. Nothing is filtered in application code, so there is no path that forgets a clause. - Keep last_used_at out of that predicate. It is written asynchronously and is allowed to lag by a minute, so it is a usage signal; feeding it into an authorisation decision makes the decision depend on a write that may be late, batched away or lost.
- Flag the modelling smell while you are here:
expiredis derivable fromexpires_at < now(), so storing it as a status obliges a job to keep it true and guarantees the column is wrong between the expiry instant and that job's next run. Derive it in the predicate; keep the stored status for states that are decisions rather than clock readings.
Worked solution 20 min
- Create the table with all three constraints on a scratch database and insert two revoked rows sharing (tenant_id, name); the partial index should accept both.
- Insert a second live row with that same name and confirm the violation names the partial index.
- Run
update tenant_api_key set revoked_at = now()leaving status = 'active' and confirm the CHECK rejects it; then tryinsert ... scopes = '{}'against both the cardinality and the array_length forms and note that only one rejects it. - Run
explain (analyze, buffers)on the lookup predicate for a live key and confirm an index scan on secret_hash with rows removed by filter equal to zero.
Follow-up
- Rotation issues a replacement while the old key stays live for a 30-day overlap. What does the uniqueness rule become, and what does the UI show to tell two same-named keys apart?
- A password reset bumps the principal's auth_version. No row in this table changed. How does the next request fail, and what query counts how many keys that bump just killed?
- A key turns up in a public repository. Which columns let you find it, and what do you write to the row?
Paginate a tenant's delivery export without skipping rows
A customer exports webhook_delivery: delivery_id (bigint identity), subscription_id, tenant_id, event_id, status, attempt_count, next_attempt_at, created_at, delivered_at, updated_at. The endpoint runs select ... where tenant_id = $1 order by created_at desc limit 100 offset $2, and customers report rows missing from exports taken while new deliveries are being inserted. Write the replacement query and the index that supports it, paging a tenant's deliveries newest first at constant cost per page. State why updated_at cannot be the cursor column.
Approach
- Name the defect precisely. OFFSET is a position in a result set that is recomputed on every request, so a row inserted ahead of the window shifts everything back by one and the next page starts after a row the client never received. Nothing errors and no identifier gap appears, so the loss is silent.
- Replace the position with a value predicate over a stable, unique, indexed ordering:
where tenant_id = $1 and (created_at, delivery_id) < ($2, $3) order by created_at desc, delivery_id desc limit 100. The row comparison is load-bearing: created_at alone is not unique, so ties straddling a page boundary are dropped or repeated, which is the same bug in a smaller window. - Index
(tenant_id, created_at, delivery_id). PostgreSQL scans a btree in either direction, so an all-DESC ORDER BY is served by an ASC index read backwards and no DESC modifiers are needed; they only matter when the ORDER BY mixes directions. Confirm the plan has no Sort node above the index scan, or the LIMIT stops being an early exit. - Price both forms: keyset is one index descent plus 100 adjacent leaf entries per page, constant regardless of depth, while OFFSET still produces and discards every skipped row, so page N costs time proportional to N times the page size and a deep page on a large table goes from milliseconds to seconds.
- Rule out updated_at as the cursor from the precondition, not from taste: a cursor column must never change value for a row already paged past. updated_at moves on every delivery attempt, so a row the client already emitted re-enters a later page and is exported twice. created_at and delivery_id are immutable, which is the whole qualification.
Follow-up
- The client wants a snapshot as of one instant rather than a live tail. Compare a repeatable-read transaction held open, an added
created_at <= $snapshotbound, and a materialised export table. - A retention job deletes deliveries older than 90 days. What does a client mid-walk see, and does keyset pagination help at all?
- The customer wants to resume an export from yesterday's last cursor. What must be true of the cursor for that to be safe?
How do you approach monitoring and observability for a high-traffic se…
How do you approach monitoring and observability for a high-traffic service?
Approach
- Fix the scope first: who calls this, how often, and what they do when it fails.
- Name the read and write paths separately; they rarely have the same bottleneck.
- Choose a partition key and say what query it makes expensive.
Follow-up
- How does this behave when that dependency is down for an hour?
- What would you drop to keep the system up under load?
How would you architect a notification service that needs to scale to …
How would you architect a notification service that needs to scale to millions of users?
Approach
- Choose a partition key and say what query it makes expensive.
- Name the failure you are designing for, then the recovery path.
- Name the read and write paths separately; they rarely have the same bottleneck.
Follow-up
- What breaks first when traffic grows ten times?
- What would you drop to keep the system up under load?
Design a system to handle real-time collaboration for multiple users o…
Design a system to handle real-time collaboration for multiple users on a single document.
Approach
- Fix the scope first: who calls this, how often, and what they do when it fails.
- State the consistency you need, and where you are willing to be stale.
- Name the read and write paths separately; they rarely have the same bottleneck.
Follow-up
- What breaks first when traffic grows ten times?
- What would you drop to keep the system up under load?
Build a resumable usage export the customer can reconcile against
Customers reconcile invoices against usage_event (event_id uuid, tenant_id, workspace_id, environment, sku, quantity numeric(20,6), idempotency_key, occurred_at, ingested_at, source_service, request_id), partitioned daily on ingested_at. The current export is ?page=N&per_page=1000 ordered by occurred_at, and a customer syncing hourly reports rows that never appear in their export but do appear on their invoice. Design the replacement: the ordering, the cursor's contents, the index it requires, and the rule deciding where a page stops. State what the client does on a timeout and on a cursor older than retention.
Approach
- Separate the two defects in the current shape. Offset makes the database produce and discard N * per_page rows, so page cost grows linearly and a deep page degrades from milliseconds to seconds. Concurrent inserts also shift the window between requests, so a walker skips rows with no error raised anywhere, which for a customer sync is silent data loss.
- Order by ingestion, not by occurrence. During a replay events arrive hours out of occurred_at order, so a consumer holding an occurred_at high-water mark can never see a late event that falls below it; (ingested_at, event_id) is the only ordering under which 'everything after my cursor' is a complete statement.
- Page with a row comparison: where tenant_id = $1 and (ingested_at, event_id) > ($2, $3) order by ingested_at, event_id limit $4, backed by an index on (tenant_id, ingested_at, event_id). That seeks directly to the resume point, so every page costs the same regardless of depth.
- Trail the head of the table. With ingested_at defaulting to now(), which is transaction start time, a long insert transaction receives an earlier timestamp and becomes visible after a reader has already passed it. Cap each page at ingested_at <= now() - delta, with delta larger than the longest write transaction as bounded by statement_timeout and idle_in_transaction_session_timeout, or the export skips exactly the rows written under load.
- Make the cursor opaque and self-describing: base64 of the timestamp, the event id and a fingerprint of the filters, rejected when the filters differ from the current request. Return 410 with a
cursor_expiredcode once the cursor's partition has been dropped, so the client restarts from a known time instead of resuming into a hole. - Keep a page a pure GET with no server-side consumption, so a timeout is resolved by retrying the identical cursor.
Worked solution 30 min
- Construct the failing case on paper: an event with occurred_at at 09:00 ingested at 14:00, and a consumer that read up to 10:00 at 11:00.
- Write the keyset query with the row comparison and the exact index it needs, then say which column of the index each predicate uses.
- Add the trailing-head predicate and pick delta from a named timeout setting rather than a round number.
- Define the cursor's encoded contents and the two error cases: filter mismatch and expired partition, with their status codes.
- Write the client's algorithm in four lines: request, persist cursor after processing the page, retry the same cursor on timeout, restart from a time on 410.
Follow-up
- The customer asks for a total count alongside the first page. What do you offer instead, and why is an exact count both expensive here and wrong by the time it is read?
- How would you let a customer re-read a window they have already consumed without giving up the forward-only cursor?
Webhook workers leak until OOM and drop in-flight deliveries
webhook-delivery workers grow from 400 MB to a 2 GB limit over about 36 hours, are OOM-killed, restart, and repeat. Each restart abandons in-flight attempts, so webhook_delivery rows sit in in_flight until their leases expire and the backlog spikes. The live set measured after a forced full collection also grows. The fleet serves tens of thousands of subscriptions, several thousand of which have been failing for weeks. Give an ordered checklist, the measurement separating retention from fragmentation, and the fix.
Approach
- Separate the two failure shapes with one measurement: track resident set size against the live set after a forced full collection. A live set that climbs monotonically is retention; a flat live set under a rising RSS is fragmentation, off-heap or native allocation, or an allocator that never returns pages. The stated symptom puts this in the first category, which rules out allocator tuning as a fix.
- Characterise the curve rather than the total. Growth linear in uptime implies an unbounded structure keyed by something that keeps arriving; step growth implies buffering a large object. Correlate the slope against event rate and separately against the count of distinct subscriptions seen, because those two diverge and only one of them will fit.
- Diff two heap snapshots an hour apart by retained size grouped by dominant root, not by allocation count, which is dominated by short-lived objects and will point at the wrong thing.
- Expect a per-subscription map with no eviction: circuit-breaker or backoff state created on first failure and never removed, so the retained set grows with endpoints that have ever failed, and the several thousand permanently dead endpoints hold theirs forever.
- Fix in two places. Bound the in-memory structure with a size-capped LRU or a TTL keyed on last use, and move state that must survive a restart onto the subscription or webhook_delivery row, since the worker holding it in memory is exactly why a restart loses it.
- Repair the second-order damage separately, because it will outlive the leak: workers claim by compare-and-set with leased_until, so a bounded lease returns in_flight rows to pending on a known schedule, and a graceful shutdown releases leases instead of waiting them out.
Follow-up
- The backlog spike after a restart is itself a thundering herd against customer endpoints. What stops the recovery from becoming a second incident?
- Suppose the live set had been flat while RSS still climbed. Name two causes and the measurement that separates them.
- How would you size the LRU, and what does a miss on an evicted circuit-breaker entry cost a customer whose endpoint is down?
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.
Prepare, practise & reflect
One practical outcome each day. Spend longer where you need it.
0 / 7 done01Technical Screen: data-structure fundamentals
- Solve first unique character index with a frequency map, and state why two passes over the string are still linear
- Solve lowest common ancestor in a binary tree recursively. Then state what changes if the tree is a binary search tree, or if nodes have parent pointers
- Solve the Hankel matrix check: every anti-diagonal must be constant, so compare each cell a[i][j] with its upper-right neighbor a[i-1][j+1]. Note that comparing with the upper-left neighbor tests the Toeplitz property instead. List the edge cases: a single row, a single column, a non-square matrix
- Before writing each solution, say its complexity and the input size it handles
Deliverable: Three solved problems, each with its complexity stated before the code and a list of edge cases you tested.
Practice prompt ↗Practice prompt ↗Worked solution ↗02Technical Screen: practical implementation and object modeling
- Build a text editor with undo/redo using two stacks of operations, and state why a new edit clears the redo stack
- Design a to-do-list object model, then add sharing or due dates and note which classes had to change
- Implement a token-bucket rate limiter with an injectable clock and tests for burst, refill and exact-capacity requests
- Work through the guide's worked coding exercise, Fold a deduplicated usage stream into hourly rollups, and check your deduplication key against its expected result
Deliverable: Working editor, to-do model and rate limiter, each with tests, plus a note on what the added feature forced you to redesign.
Practice prompt ↗Practice prompt ↗03Data modeling and consistency
- Answer the reported relational-versus-NoSQL question for document data. Write out the access patterns first, then the store each one favors
- Write a short answer on data consistency in a distributed system that names at least two guarantees (such as read-your-writes and eventual consistency) and a mechanism for each
- Complete the worked SQL exercise on credential revocation and the keyset pagination drill, and say why OFFSET skips rows on a growing table
Deliverable: A one-page data-modeling note covering storage choice, consistency guarantees and pagination, backed by the two SQL drills.
Practice prompt ↗Practice prompt ↗04System Design Interview: real-time collaboration
- Design real-time collaboration on a single document: operation format, per-document ordering, OT versus CRDT, presence, and offline reconnection
- Answer the reported question on database structures for a collaborative editor: how blocks or operations are stored, and how a document loads quickly
- Stress the design: two users editing the same text, a client offline for a day, and a document with very large history
Deliverable: A design diagram with a written explanation of how concurrent edits converge and how a client recovers after reconnecting.
Practice prompt ↗Practice prompt ↗Worked solution ↗05System Design Interview: notifications, observability and debugging
- Design the notification service: fan-out through a queue, per-channel workers, user preferences, idempotency keys, retries with backoff and a dead-letter path
- Define the monitoring for a high-traffic service: the indicators you alert on, where each is measured, and what you use to diagnose rather than to page
- Work through the guide's worked design exercise on a resumable usage export, and the webhook memory-leak debugging drill
Deliverable: A notification-service design with failure handling, an alerting list for one service, and an ordered debugging checklist.
Practice prompt ↗Practice prompt ↗06Product Vision Discussion
- Use the product for a real task and write down three frictions, each with a likely technical cause and a fix you could scope
- Write a one-page feature proposal: user problem, smallest shippable version, main technical risk, and how you would measure success
- Prepare answers to the reported questions on shipping speed versus technical debt and building a custom solution versus using a library, each with an example from your own work
Deliverable: Product notes with three frictions, one scoped feature proposal, and two trade-off answers you have said out loud.
Practice prompt ↗Practice prompt ↗07Leadership Round and full rehearsal
- Write one story for each reported behavioral prompt: PM disagreement, ambiguity, speed versus debt, mentoring and inclusion, legacy refactor, and why Notion
- For each story, mark the decision that was yours, the alternative you rejected, and the user or business result
- Rehearse the reversed-decision drill in this guide as a model for a story that includes the measurement that changed your mind
- Run a mock with a peer: one coding problem from day 1 or 2, one design problem from day 4 or 5, and two behavioral stories. Note where your facts shifted between tellings
Deliverable: A story bank covering every reported behavioral prompt, and a mock-session list of the answers that need another pass.
Practice prompt ↗Worked solution ↗Expand any day for tasks and deliverables. Your progress is saved on this device.
The reported behavioral questions for this role cover a disagreement with a product manager, a project with a lot of ambiguity, balancing fast shipping against technical debt, mentoring junior engineers and building an inclusive team, refactoring a legacy codebase, and why you want to work at Notion. For each one, state the situation briefly. Spend most of the answer on the decision you made and the alternative you rejected. Close with a measurable result and what you would change.
Why do you want to build at Notion specifically?
Why do you want to build at Notion specifically?
Approach
- Pick a story where you made the decision, not one where you watched it.
- Name the disagreement and how you resolved it with evidence.
- State the situation in two sentences and spend the rest on the reasoning.
Follow-up
- How did you know your change caused the improvement?
- What did you decide not to do, and why?
Describe a time you had to refactor a legacy codebase; what were your …
Describe a time you had to refactor a legacy codebase; what were your primary considerations?
Approach
- Close with what you would do differently, concretely.
- Pick a story where you made the decision, not one where you watched it.
- Name the disagreement and how you resolved it with evidence.
Follow-up
- What did you decide not to do, and why?
- How did you know your change caused the improvement?
Reverse a webhook ordering decision after measuring its cost
You argued for strict per-subscription ordering in webhook-delivery, which means one in-flight attempt per subscription. It shipped. Three months later a single unresponsive endpoint holds one subscription's queue at a six-hour backlog, and two customers report events arriving out of order anyway once their own retries are counted. Describe a decision you reversed: what you originally optimised for, the measurement that changed your mind, what the reversal cost in engineering time and customer change, and how you told the people who had already built on the original guarantee.
Approach
- State the original decision as a trade you made knowingly. Ordering across a network requires a single in-flight attempt per subscription, and its price is head-of-line blocking whenever one endpoint is slow. 'We priced it wrong' is a much stronger opening than 'we did not realise', and it is usually the true one.
- Bring the measurement that flipped it, not the anecdote: backlog age at the ninety-ninth percentile per subscription, the share of subscriptions where one slow endpoint gated an otherwise healthy queue, and the delivery throughput lost to serialisation. A reversal justified by complaints is indistinguishable from a reversal justified by fatigue.
- Name what you learned about the guarantee itself, which is the engineering content of this story. At-least-once delivery means a retried event already arrives after newer ones and the consumer already must be idempotent, so a guarantee the customer has to defend against anyway was never worth what it cost to provide.
- Describe the migration, because reversing a published contract is the hard half and the part candidates skip. Parallel attempts behind a per-subscription flag, a monotonically increasing sequence number added to the envelope so order-sensitive consumers can sort or discard, documentation that states at-least-once and unordered in those words, and a deprecation measured in quarters because the client is a pinned SDK inside a build pipeline you cannot see or redeploy.
- Give the cost in the two currencies that matter: engineer-weeks, and how many customers had to change code. Then say who you told before it shipped rather than in a changelog afterwards, and which large customer you left on the old behaviour and for how long.
- Close with the signal you now weight differently, stated as something you would do earlier next time: measuring the blocking cost on the slowest decile of endpoints before committing to the guarantee, rather than after a customer noticed.
Follow-up
- A customer insists they need ordering. What do you offer them that is not global serialisation?
- How did you choose the deprecation window given that you cannot see or redeploy the clients?
- What would have to be true for you to reverse back?
- 01
Tell me about a time you had a disagreement with a product manager; how did you resolve it?
- 02
Describe a project where you had to navigate significant ambiguity.
- 03
How do you balance the need for shipping features quickly versus maintaining technical debt?
- 04
What is your approach to mentoring junior engineers and fostering an inclusive team environment?
- 05
Describe a time you had to refactor a legacy codebase; what were your primary considerations?
- 06
Why do you want to build at Notion specifically?
Is this an official Notion interview guide?
No. It is PracHub's own research and practice material for the Software Engineer role at Notion. Rounds and questions reflect what candidates have reported, not a process Notion has published, and they change over time. Confirm the current format and scope with your recruiter.
PracHub interview research ↗Which rounds do candidates report for the Notion Software Engineer loop?
Candidates report four: a Technical Screen, a System Design Interview, a Product Vision Discussion and a Leadership Round. The sources do not tie specific questions to specific rounds, so this guide groups the reported questions by category. Ask your recruiter what each round covers and whether you will write code in it.
PracHub Software Engineer practice ↗How long should I prepare for the interview process?
The source notes suggest two to four weeks of focused preparation. How much you need depends on your starting point. If you have not solved coding problems from a blank file recently, give the Technical Screen material extra days. If you have not led a design discussion recently, spend more time on the collaboration and notification designs. The 7-day plan in this guide is a minimum pass over every round, not a replacement for that preparation.
PracHub interview research ↗Are the coding questions algorithm puzzles or practical problems?
Both appear in the reported material. The coding questions candidates report are practical: rate-limiting an API, speeding up a slow React component, keeping data consistent in a distributed system. The question list in the source notes also includes standard problems, such as first unique character, lowest common ancestor and a Hankel matrix check. The PracHub bank for this role adds object-modeling tasks such as a to-do list and a text editor with undo/redo. Prepare for both, and practice explaining your design choices as you code.
PracHub Software Engineer practice ↗How should I prepare for the Product Vision Discussion?
Candidates describe this round as covering your understanding of product vision and alignment with company goals. The sources give no format. Use the product for real work beforehand. Form specific opinions about a few features, each with a likely technical cause and a scoped fix. Be ready to connect engineering trade-offs, such as shipping speed versus technical debt, to what a user experiences.
PracHub Software Engineer practice ↗What is the timeline from the first screen to an offer?
Candidate-reported figures put the process at roughly three to five weeks, and one source note gives three to six. Timelines depend on scheduling and team. Ask your recruiter for the expected sequence and when decisions are communicated.
PracHub interview research ↗Sources & methodology 3 sources ↗
Official role evidence, timestamped platform data and clearly labeled preparation advice.
- 01PracHub interview research ↗
PracHub editorial research into this company and role, maintained with this guide. Candidate-reported, not an employer publication.
platform · Accessed 2026-09-24 - 02PracHub Software Engineer practice ↗
Cross-company practice questions for this role.
platform · Accessed 2026-09-24 - 03PracHub interview preparation framework ↗
The framework the preparation plan follows.
platform · Accessed 2026-09-24