notion prep
summer 2026 · swe internship
Working through each stage of the Notion internship interview process. The main focus is the debugging virtual onsite — a large TypeScript codebase, a live Notion-like editor, and logic bugs to squash.
4 problems · 70 minutes · scored out of 600. Each module escalates — clear M1–M3 cleanly first, then tackle M4. Code quality is scored alongside correctness.
- Numbers, strings, basic arrays
- Single loop at most
- Step-by-step instructions given
- No algorithms or pattern-spotting
- Split, compare, reverse strings
- Iterate, modify, concat arrays
- Up to 2 nested loops
- 3–5 basic concepts combined
- 2D arrays — traverse, transpose, pivot
- Hashmaps with string/int keys
- Decompose into sub-functions
- Must meet execution time limits
- Greedy, two pointers, divide & conquer
- Hashmaps + sets for optimization
- Discrete math fundamentals
- No Dijkstra, Kruskal, FFT, or DP
Score M1–M3 first. Earlier modules carry more weight — a clean M1–M3 beats a half-finished M4.
CodeSignal scores code quality too — clean names, no debug cruft, readable structure.
The 70-minute window is real. Timed practice is the only way to build the right muscle for it.
M4 patterns: two pointers, sliding window, frequency hashmaps, binary search on the answer.
A short call with a recruiter (usually Shivani Patel or Chris). It's mostly a vibe check — confirming you're eligible, interested, and not a robot. Non-technical. If you pass, you get the take-home.
Be yourself. This is mostly eligibility + enthusiasm, not technical screening.
Have a clear "why Notion" — they care about cultural fit and genuine interest in the product.
Know your resume and be ready to talk briefly about past work.
Ask smart questions at the end — what teams are hiring, what the timeline looks like, etc.
A take-home project, usually involving the Notion API or a small feature build. Notion recreates this each year. Spend the full 2–3 hours — readability and craftsmanship matter as much as the solution.
Readability is everything. Clean variable names, logical decomposition, comments only where necessary.
Polish the README — make sure it's well organized, explains setup, and covers edge cases you thought about.
The short-answer questions in the README are a big deal. For "what would you improve with more time?" — actually rip apart your code and be specific about tradeoffs.
Write tests. You'll regret it if you don't.
Make it actually work — test your code before submitting.
A live coding interview: 45 minutes to solve a problem, 15 minutes to ask your interviewer questions. Historically involves OOP / class design and time complexity optimization. Notion creates original questions each year.
Think about time and space complexity out loud. They want to see you reason about performance.
Start with a brute force solution, articulate why it's suboptimal, then optimize. Showing the thought process matters more than jumping straight to optimal.
Past questions have involved class implementation and binary search — practice OOP design problems.
The bar isn't just solving the problem — they care about engineering fundamentals and how you communicate tradeoffs.
Related LC problem: 981. Time Based Key-Value Store — OOP + binary search.
A large, multi-file TypeScript codebase you've never seen before.
A live Notion-like page editor you can interact with alongside the code.
Logic bugs only — off by one, wrong condition, missing null check, etc.
Bugs may or may not have pre-written unit tests. Ask your interviewer.
React components + backend code powering the editor.
45 minutes — time management is critical.
Ask: "Are there unit tests for this bug, or should I just navigate the codebase?"
Clarify: current behavior vs expected behavior — spell it out before diving in.
Don't be afraid to ask what Notion concepts mean — same as on the job.
console.log freely — use your debugging tools, don't just stare.
After fixing: ask if they want unit tests or a discussion of how to prevent the bug.
Over-communicate — narrate what you're looking at and why.
// basic types
let name: string = "youdahe"
let count: number = 0
let done: boolean = false
// interface
interface User {
id: string
name: string
age?: number // optional
}
// type alias
type Status = "active" | "inactive"
// generic
function wrap<T>(val: T): T[] {
return [val]
}
// Partial — all props optional
type P = Partial<User>
// Pick — subset of props
type N = Pick<User, "id" | "name">
// Omit — exclude props
type O = Omit<User, "age">
// Record — key/value map
type M = Record<string, number>
// Optional chaining
user?.address?.city
// Nullish coalescing
const name = user.name ?? "anon"
// Non-null assertion
const el = document.getElementById("x")!
const nums: number[] = [1, 2, 3] // map / filter / reduce const doubled = nums.map(n => n * 2) const evens = nums.filter(n => n % 2 === 0) const sum = nums.reduce((acc, n) => acc + n, 0) // find / findIndex const found = nums.find(n => n > 1) const idx = nums.findIndex(n => n > 1) // spread const copy = [...nums, 4, 5] // destructuring const [first, ...rest] = nums
// async/await
async function fetchUser(id: string) {
const res = await fetch(`/api/users/${id}`)
if (!res.ok) throw new Error("failed")
return res.json() as Promise<User>
}
// type guard
function isString(v: unknown): v is string {
return typeof v === "string"
}
// as const
const ROLES = ["admin", "user"] as const
type Role = typeof ROLES[number]
describe("MyComponent", () => {
let instance: MyClass
beforeEach(() => {
instance = new MyClass()
})
afterEach(() => {
jest.clearAllMocks()
})
it("does the thing", () => {
const result = instance.doThing()
expect(result).toBe(true)
})
it("handles edge case", () => {
expect(() => instance.bad()).toThrow()
})
})
// equality expect(x).toBe(y) // === (primitives) expect(x).toEqual(y) // deep equality expect(x).not.toBe(null) // truthiness expect(x).toBeTruthy() expect(x).toBeFalsy() expect(x).toBeNull() expect(x).toBeUndefined() // arrays / strings expect(arr).toHaveLength(3) expect(arr).toContain(item) expect(str).toMatch(/pattern/) // numbers expect(n).toBeGreaterThan(0) expect(n).toBeLessThanOrEqual(10)
// mock a function
const mockFn = jest.fn()
mockFn.mockReturnValue(42)
mockFn.mockResolvedValue({ id: "1" })
expect(mockFn).toHaveBeenCalled()
expect(mockFn).toHaveBeenCalledWith("arg")
expect(mockFn).toHaveBeenCalledTimes(2)
// mock a module
jest.mock("../api", () => ({
fetchUser: jest.fn().mockResolvedValue({
id: "1", name: "test"
})
}))
// spy on method
const spy = jest.spyOn(obj, "method")
// async/await
it("loads data", async () => {
const data = await fetchUser("1")
expect(data.name).toBe("youdahe")
})
// resolves/rejects matchers
it("resolves correctly", async () => {
await expect(fetchUser("1")).resolves
.toMatchObject({ id: "1" })
})
it("throws on bad id", async () => {
await expect(fetchUser("")).rejects
.toThrow("failed")
})
// useState
const [count, setCount] = useState(0)
setCount(prev => prev + 1) // functional update
// useEffect — runs after render
useEffect(() => {
document.title = `Count: ${count}`
return () => { /* cleanup */ }
}, [count]) // dep array controls when it fires
// missing dep = stale closure bug
// extra dep = unnecessary re-runs
// useCallback — memoize function ref
const handleClick = useCallback(() => {
doSomething(id)
}, [id])
// useMemo — memoize computed value
const sorted = useMemo(() =>
items.sort(compareFn)
, [items])
// useRef — mutable, no re-render
const inputRef = useRef<HTMLInputElement>(null)
inputRef.current?.focus()
// also useful for previous value tracking
Off by one: i < arr.length vs i <= arr.length. Watch slice bounds, substring indices, loop ranges.
Null/undefined: Accessing a property on something that could be null. Look for ?.chaining that should be there but isn't.
Stale closure: An async callback or event handler captures an old value of state. Fixed by using functional updates or putting the value in a ref.
Wrong comparison: == vs ===, comparing objects by reference instead of value.
Missing return: A function falls through without returning a value in a branch.
Mutation side effects: Mutating an array/object directly instead of returning a new one — causes React not to re-render.
Wrong dependency array: useEffect or useMemo missing a dependency, so it runs on stale data.
A standard behavioral interview — usually 45 minutes with the eng manager of the team you'd be joining. Use the STAR method (Situation, Task, Action, Result) for every story. Notion cares about curiosity, collaboration, and shipping quality work.
STAR every story — Situation, Task, Action, Result. Keep it tight.
Prepare 5–6 strong stories that can flex to different questions (conflict, failure, impact, collaboration, learning).
Notion hires people who are curious and love the product — have genuine answers about why Notion specifically.
Don't be arrogant about tradeoffs — discuss them thoughtfully, not like you know better than everyone.
Asking good questions at the end matters — shows genuine interest in the team and role.