Design Google Drive
The web app at drive.google.com (and Dropbox on the web): browse folders that hold thousands of files, select and drag them around, upload and download in bulk, share with exact roles, and see changes made on other devices. The hard part is that the client holds a live, partial copy of a huge tree of files whose permissions, locations and contents change under it, and every click must still feel like a desktop file manager.
The question
Design the web client for a cloud file storage product like Google Drive. People keep their files and folders there, open them from any device, and share them with others.
Focus on the frontend: how the app shows a folder with thousands of files, how people select, move, upload, download and share them, and how the page stays correct when the same files change on another device. Walk me through the client architecture, the data it keeps, and the API it needs.
Real-life examples: Google Drive on the web (drive.google.com), Dropbox on the web, Microsoft OneDrive on the web, Box web app
Requirements: a file manager that lives in a tab
Google Drive on the web is a file manager: a left rail with My Drive, Shared with me, Recent, Starred, Trash and storage; a big area in the middle that lists the files of one folder; a toolbar on top. You click a folder to open it, double-click a PDF to preview it, drag a few files onto another folder to move them, and press Share to give a colleague access. Dropbox, OneDrive and Box on the web look and behave much the same.
The interviewer wants the client of that product, not the Docs editor that opens when you double-click a Google Doc. Our job is everything around it: browsing, selecting, moving, uploading, downloading, sharing, searching, previewing and staying in sync.
Questions to ask
Each question below changes a decision later in this guide. The middle column is what we assume.
| Question | Answer we assume | What it changes |
|---|---|---|
| How big does one folder get? | Most hold under 100 items. Big shared team folders hold 10,000 to 50,000. | We can't load a folder in one request or render it as plain DOM. Page tokens plus virtualization. |
| Can a file live in two folders? | No. One parent per item, plus shortcuts that point to an item from another place (Drive's model since 2020). | The store is a tree, not a graph, which keeps moves and permissions simple. |
| Personal files only, or team spaces too? | Both: My Drive (owned by a person) and shared drives (owned by a team). | Two kinds of root, different roles, different rules for moving between them. |
| Can other people change my files while I look? | Yes: colleagues, my phone, the desktop sync app. | We need a change feed and a way to patch the open folder without a reload. |
| How large are uploads? | From 1 KB notes to 50 GB videos, and whole folders of 5,000 files. | We reuse the resumable uploader (see the file uploader guide) and add an app-wide upload panel. |
| Which roles can people get? | Viewer, Commenter, Editor, plus the owner. Shared drives use Manager, Content manager, Contributor, Commenter and Viewer. | Every button in the UI must be checked against what the person may do on that item. |
| Offline? | Read recent folders offline; full offline editing is out of scope. | Folder listings cached in IndexedDB, with a clear offline banner. |
| Devices? | Desktop browsers first. Mobile web gets a simpler view; the native apps are separate. | Mouse, keyboard and screen reader are all first-class. Touch is second. |
Functional requirements
- Browse: open a folder and see its items in a list or a grid, sorted by name, last modified, or size, with folders first. A folder tree in the left rail and breadcrumbs on top.
- Select: click, Ctrl or Cmd-click, Shift-click ranges, drag a rectangle (marquee) over items, select all, and do all of it from the keyboard. An actions bar appears when something is selected.
- Organize: create folders, rename, move by drag and drop or a Move to dialog, add shortcuts, star, and undo from a toast.
- Upload and download: files and whole folders by picker or drop, a panel with progress that survives navigation, and multi-file downloads as a zip made on the server.
- Share: invite people and groups as Viewer, Commenter or Editor, set general access (restricted or anyone with the link), see inherited access, and request access to items you can't open.
- Search: a search box with suggestions while you type, and filters for type, owner, and modified date.
- Preview: open images, PDFs and videos in an overlay and step through files with the arrow keys.
- Stay in sync: show changes made on other devices and by other people within a few seconds.
- Trash: delete moves items to the trash, restore puts them back, and items left in the trash are deleted for good after 30 days.
Non-functional requirements
| Quality | Target | Why this number |
|---|---|---|
| First load | App shell and first folder visible in under 2.5 s on a mid-range laptop (Largest Contentful Paint) | 2.5 s is the good threshold for LCP. A file manager people open many times a day must open fast. |
| Opening a folder | Under 200 ms if cached or prefetched, under 1 s cold | Folder hopping is the most common action. Cached folders should feel instant. |
| Interaction | Selection, hover and drag feedback in one frame; INP under 200 ms | A file manager is judged by how it feels under the mouse. |
| Large folders | Scroll a 50,000-item folder at 60 fps with under 150 rows in the DOM | 50,000 rows of DOM would take seconds to build and hundreds of MB of memory. |
| Freshness | Changes from elsewhere show within about 5 s while the tab is visible | Fast enough that two people in a meeting see the same folder. |
| Correctness | Never show a button the person can't use; never lose a move or upload silently | Permission errors after a click feel broken. Silent loss of files is the worst failure. |
| Accessibility | Every action works with the keyboard and a screen reader, including drag and drop | Drag is a pointer-only gesture. WCAG 2.2 asks for keyboard access (2.1.1) and a single-pointer alternative to dragging (2.5.7). |
Out of scope
- The Docs, Sheets and Slides editors. They open in their own app and have their own guide.
- The desktop sync client that mirrors Drive to a folder on disk. A web page can't watch the disk.
- Server storage, deduplication and virus scanning. We design the contract with them, not their insides.
- Admin consoles, retention policies and data loss prevention rules. We only respect what they return.
Check your understanding · Exercise 1
Which answers from the interviewer would change the client design the most? Select all that apply.
What makes Drive hard
A list of files looks simple. Five properties of this product make it one of the harder frontend designs, and every later chapter answers one of them.
- The tree is huge and the client sees a small window of it. A person may have 200,000 items across 3,000 folders, plus everything shared with them. The client holds a few pages of a few folders and must still show correct paths, counts and breadcrumbs.
- The same item shows up in many places at once. One PDF can be a row in the open folder, a node in the left tree, a hit in the search dropdown, an entry in Recent, and the file in the preview overlay. Rename it once and all five must change together.
- It is a desktop app inside a page. People expect Shift-click ranges, rubber-band selection, drag and drop with drop targets, keyboard shortcuts, and right-click menus. All of it must also work without a mouse.
- Permissions shape every pixel. A viewer must not see a Rename button, and an editor of a folder is an editor of everything inside it. The UI is a function of the item and of the person's access to it.
- The data changes under you. A colleague moves a file out of the open folder, or your phone uploads a photo. The client must patch its copy and handle the moments where your action and theirs collide.
Before we design anything, let's draw the screen so we can name its parts. Every part in this wireframe maps to a component later in the architecture chapter.
Notice what is not on screen: most of the tree is collapsed, most rows of a big folder are below the fold, and the upload panel and toast appear only when needed. That is the theme of the whole design: load and render only what the person can see, but keep one consistent model of everything the client has seen so far.
The file system model on the client
Everything else rests on how the client represents files and folders. Get it wrong and rename, move, breadcrumbs and sharing each grow their own bugs.
The naive model: nested folders as nested JSON
The first idea is to mirror what you see: a folder object with a children array, each child either a file or another folder with its own children. It works for a demo, then breaks in three ways. First, a folder with 30,000 items arrives in pages, so children is never complete and you can't tell "empty" from "not loaded".
Second, the same file appears in the folder view, in Recent and in search results, so you end up with three copies and a rename updates only one. Third, a move means cutting an object out of one deep array and splicing it into another. A change from the server that says "file X now has parent Y" forces you to search the whole nested tree to find X.
How many parents can an item have?
Before the store, settle the shape of the data. Google Drive once allowed an item to sit in several folders at once. Since 30 September 2020 every item has exactly one location, and Google then migrated the old extra locations to shortcuts: small items that point to a target item. Dropbox and OneDrive also give each item one location, and the interviewer may push on why.
Decision How should items relate to folders?
What we pick: One parent plus shortcuts. It keeps the data a tree, which makes paths, moves and inherited permissions each have one clear answer. Identity is a stable id, never a path: a path is something we compute for display by walking up the parents.
A normalized store keyed by id
The client keeps one flat map of every item it has seen, keyed by id (this is normalization: see the normalization lesson). A folder listing is not a list of items. It is a separate record that holds the ids in display order for one folder and one sort, plus the page token for the next page and a flag for whether the list is complete.
Recent, Starred, Shared with me and each search query are lists of the same kind. The item itself lives in one place, so a rename patches one record and every view that shows it re-renders.
type ItemId = string;
type Item = {
id: ItemId;
name: string;
kind: "folder" | "file" | "shortcut";
mimeType: string;
parentId: ItemId | null; // null for a root
driveId: string; // "my-drive" or a shared drive id
targetId?: ItemId; // only for shortcuts
size?: number;
modifiedTime: string;
trashed: boolean;
capabilities: Capabilities; // what this person may do here
};
// One per folder + sort, and one per view like "recent" or a search
type Listing = {
ids: ItemId[];
nextPageToken: string | null; // null means complete
sort: "name" | "modified" | "size";
fetchedAt: number;
};
parentId; a shortcut is its own item with a targetId. The store is flat: the tree is something we read out of it, and a listing holds only ids.My Drive and shared drives
There is not one tree but several. My Drive is the tree owned by one person. Each shared drive is a tree owned by a team or company: files there belong to the organization, so they stay when a member leaves. People get roles on the whole shared drive (Manager, Content manager, Contributor, Commenter, Viewer).
"Shared with me" is not a tree at all: it is a flat list of items whose parents you may not be allowed to see. That is why every item carries a driveId and why parentId may point to a folder the client can't load. Breadcrumbs stop at the highest folder you can access, and the UI says so instead of failing.
Moving between trees has rules. Moving a file from My Drive into a shared drive makes the organization its owner, so the UI warns first. Moving items out of a shared drive needs the Manager role by default, more than editing inside it. The client does not invent these rules: it reads them from per-item capabilities the server returns (more in the sharing chapter).
The folder tree, loaded lazily
The left rail shows a tree of folders. Loading the whole tree up front would mean thousands of folders before the first paint. Instead, each tree node loads its child folders only when it is expanded, with a query for folders only (in the Drive API, a q filter like 'FOLDER_ID' in parents and mimeType = 'application/vnd.google-apps.folder'). The tree is just another view on the same store: a node's children are a listing of folder ids.
When you open a deep folder from a link, the client fetches its ancestors by walking parentId upward, one small request each or one batched request. Then it can draw breadcrumbs and expand the tree down to the folder. The tree itself follows the WAI-ARIA tree view pattern: Up and Down move, Right expands, Left collapses.
Check your understanding · Exercise 2
A file is renamed on another device. In a normalized store, what has to change on this client? Select all that apply.
Browsing: list, grid, and folders with 50,000 items
Opening a folder is the most common action. It must be fast for a folder of 40 items and still work for a team folder of 50,000 scanned invoices.
The naive approach: fetch everything, sort on the client, render every row
Fetch the whole folder, sort it in JavaScript, render a tr per item. For 50,000 items at about 600 bytes of JSON each, the response is around 30 MB before compression, and parsing it blocks the main thread. Rendering 50,000 rows with icons, names, owners and dates creates several hundred thousand DOM nodes; layout alone takes seconds and memory climbs past 500 MB. And sorting on the client only works if the client has every item, which is exactly what we can't afford.
Decision How should the client load a big folder?
What we pick: Page tokens, with the server doing the sort. The first page is 100 items (about 60 KB), so it paints fast; later pages can hold a few hundred. We prefetch the next page when the person scrolls within about 50 rows of the end. Because only the server has every item, only the server can sort them, so a sort change starts a new listing rather than re-sorting what we have.
Sorting with folders first
Drive and Dropbox both show folders above files by default. With server paging, that must be part of the server's order, not a client trick: in the Drive API the client passes orderBy=folder,name (folders first, then by name), or folder,modifiedTime desc. If the client moved folders to the top after loading page 1, a folder that sorts into page 40 would pop in at the top much later.
Name sorting must also be locale-aware and treat numbers naturally ("file 2" before "file 10"). On the client that is Intl.Collator with { numeric: true }; the Drive API offers a name_natural sort key for the same rule. A listing is keyed by folder id plus sort, so switching back to an earlier sort is instant if we still have it cached.
Rendering: virtualize the list and the grid
Even with paging, a person who scrolls through 20 pages has 2,000 items loaded. We render only the rows in view plus a buffer (this is virtualization: see the virtualization lesson and the virtualized list guide). In list view every row is 48 px tall, so a 900 px viewport shows about 19 rows; with 20 rows of buffer on each side we keep about 60 rows in the DOM whatever the folder size.
The scroll container's height is set from the total count when the server tells us, or grows as pages arrive. Unloaded rows render as grey skeletons, so a fast scroll to the bottom shows placeholders, and the visible range decides which pages to fetch.
Grid view is the same idea in two dimensions, but simpler than it sounds: tiles have a fixed size, so the grid is really a list of rows where each row holds columns tiles. With a 1,200 px wide content area and 200 px tiles plus gaps, that is 5 columns, so 2,000 items are 400 rows. When the window resizes, columns change, so row count changes; we keep the first visible item in view by recomputing its row, not by keeping the pixel offset.
Thumbnails without melting the network
Grid view shows a preview image per file, and a folder of 2,000 photos must not start 2,000 image downloads. Three rules keep it under control. First, only rendered tiles have an img, so virtualization already limits it to the tiles near the viewport, about 30 at a time.
Second, the server makes thumbnails at fixed sizes, and the client asks for the size it draws, about 2x the tile width for sharp screens. With srcset, a 200 px tile gets a 400 px image of about 20 KB, not the 6 MB original (see the image optimization lesson). Third, the thumbnail URL is short-lived (the Drive API documents its thumbnailLink as lasting hours), so it is read from the item record, cached by the browser, and refreshed when it expires.
Tiles that scroll away before their image loads drop it. When an img is removed before it finishes, browsers may cancel its request, and in any case it no longer competes for decode and paint. Files without a thumbnail show a type icon, never a broken image.
Check your understanding · Exercise 3
You show a 30,000-item folder sorted by name with folders first. Which choices are sound? Select all that apply.
Selection: click, Ctrl, Shift, marquee and keyboard
Almost every action in Drive starts with a selection: move these, share these, download these. People bring years of habits from Finder and Windows Explorer, so the rules must match what their hands expect.
The naive approach: a checkbox on each row
A boolean selected on each item, toggled by a checkbox, is easy, and it fails quickly. Shift-click needs to know where the range started, which a boolean per item can't tell you. A boolean on the item record also leaks selection into the shared store, so selecting a file in the folder view selects it in the search dropdown too. And "select all" in a 30,000-item folder with 300 loaded would either select only the loaded ones (surprising) or force loading 30,000 items first (slow).
The selection model: a set, an anchor, and a focus
Selection is view state that belongs to one listing, not to the items. It has three parts: the selected set holds ids; the anchor is the item where a range starts (the last item you clicked without Shift); and the focus is the item the keyboard is on, shown with a focus ring. The data table guide uses the same model for rows; here we extend it to grids and rubber-band selection.
| Input | Selected set | Anchor | Focus |
|---|---|---|---|
| Click an item | Only that item | That item | That item |
| Ctrl-click (Cmd-click on Mac) | Toggle that item, keep the rest | That item | That item |
| Shift-click | Replace with the range from anchor to the item, in display order | Unchanged | That item |
| Ctrl+Shift-click | Add the range to what was selected | Unchanged | That item |
| Arrow key | Only the newly focused item | Newly focused item | Moves |
| Shift+Arrow | Range from anchor to the new focus | Unchanged | Moves |
| Ctrl+Arrow, then Space | Arrow moves focus only; Space toggles the focused item (Drive uses Shift+Space) | Space sets it | Moves |
| Ctrl+A (Cmd+A) | Everything in the listing | Unchanged | Unchanged |
| Escape or click empty space | Empty | Cleared | Unchanged |
"In display order" matters: a range is computed from the listing's ids array between the anchor's index and the target's index, so it follows the current sort and works the same in list and grid views. In the grid, Up and Down move by the number of columns.
Google's published shortcut list for Drive on the web (Ctrl+/ shows it in the app) follows the same model: arrows select, Shift+arrows extend, Ctrl+arrows move without changing the selection, Shift+Space toggles, and Esc clears. It also has F2 to rename, Shift+F10 for the actions menu, Ctrl+Z to undo, and / to search. Matching these keeps the habits people already have.
Marquee (rubber-band) selection
Pressing on empty space and dragging draws a rectangle (a marquee); every item it touches is selected, live. With Ctrl held, the rectangle adds to the selection instead of replacing it. Two details make or break it.
First, hit testing can't use getBoundingClientRect on each item, because virtualized items outside the viewport have no DOM. We compute hits from geometry instead: in a list, the rectangle's top and bottom map to row indexes by dividing by the row height; in a grid, to rows and columns by tile size. That is a few divisions, not thousands of DOM reads.
Second, when the pointer reaches the top or bottom edge, the list must auto-scroll and the rectangle grows with it, so its start is stored in content coordinates (including scroll offset), not screen coordinates. The pointer is captured with setPointerCapture, so the drag keeps working even if the pointer leaves the list.
Pressing on an item may instead start a drag of the selection. So one pointerdown can become a click, a marquee, or a drag, depending on where it started and how far the pointer moves. A small state machine keeps these straight, with a 4 px threshold so a shaky click is still a click.
setPointerCapture keeps either alive if the pointer leaves the list.Try the rules in the lab below, a grid of 12 files. Watch the anchor and focus markers and the count as you click with and without the modifier toggles, or run the marquee control.
0 selected
Select all in a folder you haven't fully loaded
Decision What does Ctrl+A mean in a 30,000-item folder with 300 loaded?
What we pick: The "all except" mode for large folders, with a clear label: "All 30,000 items in Invoices are selected". Gmail uses a similar two-step for selecting all conversations that match a search. Drive's shortcut list describes Ctrl+A as selecting all visible items, the first option, which is fair if the bar says "300 of 30,000 selected". For folders that are fully loaded, the explicit set is fine and simpler.
The actions bar
When the set is not empty, the toolbar swaps in a selection bar: "3 selected", plus Share, Download, Move, Delete, Copy link, and a menu for the rest. Which buttons are enabled is the intersection of the selected items' capabilities: if one of three files is view-only, Delete is disabled with a tooltip that says why, not hidden, so people learn the rule.
The count is announced to screen readers through a polite live region once the selection settles (about 300 ms with no change), so a marquee drag over 80 items doesn't read 80 numbers.
Selection must also survive the data changing under it. If a selected file is deleted on another device, the sync layer removes its id from the set and the count updates. If the anchor disappears, the next Shift-click falls back to the focus item. Opening another folder clears the selection, as desktop file managers do.
Check your understanding · Exercise 4
In a grid sorted by name, you click file C, then Shift-click file H, then Ctrl-click file E. What is true now? Select all that apply.
Drag and drop: moving files and dropping files in
Drag and drop does two jobs in Drive: inside the app it moves items between folders, and from the desktop it uploads files. The browser handles these very differently, so we design them separately.
Decision Which mechanism should move items inside the app?
What we pick: Both, each for its job: pointer events to move items inside the app, because we need the count badge, auto-scroll and touch; native drag and drop events only to receive files from the operating system, the only way the browser hands them over. The kanban board guide uses the same pointer approach for cards.
Drop targets: rows, tree nodes and breadcrumbs
Three places accept a drop: a folder row or tile, a folder node in the left tree, and a crumb in the breadcrumbs. Each registers with the drag layer when it mounts and unregisters when it unmounts. While dragging, the layer finds the target under the pointer and asks whether the drop is allowed. The checks are local and fast:
- The target is a folder (or a shortcut to one), and the person has
canAddChildrenon it. - Each dragged item has
canMoveItemWithinDrive(or the cross-drive capability when the target is in another drive). - The target is not one of the dragged items and not inside one of them. We check by walking
parentIdup from the target; if we meet a dragged id, the drop would create a loop. - The target is not already the parent of every dragged item (a no-op).
Allowed targets get a highlight; forbidden ones get a "not allowed" cursor and a short reason in the drag chip, such as "You can't move a folder into itself". If the pointer rests on a collapsed tree node for about 700 ms, the node expands so you can drop deeper; near the top or bottom edge of a scrolling area, the area scrolls. Escape cancels and puts everything back.
The move: optimistic, with undo
When you drop, the client does not wait for the server. It applies the move to its store at once (an optimistic update: see the optimistic updates lesson). It sets each item's parentId, removes the ids from the source listing, and adds them to the target listing if that listing is loaded. Then it sends the request and shows a toast, "Moved 3 items to Q3 Reports", with an Undo button.
The toast guide covers the toast component; here the key point is what Undo does. It is not a local rollback, because by the time you press it the server may have finished. Undo sends a second move back to the old parents, which we saved before the first move.
Failure is per item. Moving 3 files may succeed for 2 and fail for 1 because someone removed your edit access a minute ago. The server returns a result per item; the client keeps the successes, rolls back the failure to its saved parent, and changes the toast to "Moved 2 of 3 items. 1 couldn't be moved: you no longer have permission".
Requests for the same item are ordered: if you move a file twice quickly, the second request waits for the first, so the server never sees them out of order (the request races lesson covers this pattern).
Dropping files in from the desktop
When the person drags files from the desktop over the page, dragenter fires with "Files" in dataTransfer.types. The client shows an overlay on the current folder ("Drop files to upload to Q3 Reports") and also lets folder rows and tree nodes act as targets, so you can drop straight into a subfolder.
On drop, the client reads the files (and, for folders, walks the directory entries) and hands them to the uploader with the target folder id. Reading entries, chunking and resumable uploads are covered in the file uploader guide; the next chapter adds the app-wide upload panel.
Dragging files out to the desktop is the weak spot. A file in Drive lives on a server, and the web platform has no standard way to drag a remote file onto the desktop. Chromium has a non-standard DownloadURL drag type that some apps used, but other engines don't support it. The honest design is a Download action, not a fake drag.
The keyboard alternative: Move to
Drag is a pointer gesture, so every move must also be possible without it. A Move to dialog (from the actions bar, the context menu, or a shortcut) shows a small folder browser with the same lazy tree, a search box, and a Move button that is disabled for forbidden targets with the same reason text. It calls the same move function, so the optimistic update, the toast and Undo are identical.
This also helps on touch screens and for anyone who finds dragging hard. WCAG 2.2 criterion 2.5.7 (Dragging Movements) asks for exactly this kind of single-pointer alternative.
Check your understanding · Exercise 5
You drag folder Projects (which contains folder 2026) onto folder 2026. What should happen? Select all that apply.
Uploads and downloads at scale
The single-file upload is a solved problem: the file uploader guide covers picking, validation, chunked resumable uploads, retries and resuming after a reload. Drive adds three things on top: folder uploads that rebuild a tree, an upload panel that lives above every page, and downloads of many files at once.
Uploads belong to the app, not the page
The naive design puts the upload queue inside the folder view component. You drop 400 photos into Trips and click into another folder while they upload; the folder view unmounts, and the uploads either stop or keep running with no visible progress. Instead, the upload manager is an app-level service created once at startup, outside the router.
Views talk to it through the store: they enqueue files with a target folder id and read progress from it. The upload panel in the bottom corner is mounted in the app shell, so it stays while you browse, open previews and search.
- The panel shows a summary ("Uploading 182 items, about 3 minutes left") and a collapsible list with progress, cancel and retry per file.
- The panel's list is virtualized too: a folder upload of 5,000 files is 5,000 rows.
- The queue runs 3 files at a time, the file uploader's default. Small files go in one request each; large ones go in resumable chunks.
- If the target folder is open, each file in progress shows there as a placeholder row with a progress ring, swapped for the real item when the server confirms.
- Closing the tab with uploads running triggers a
beforeunloadprompt. Large uploads save their upload session in IndexedDB, so after a reload they continue from the last confirmed byte once the person picks the same file again (a page can't reopen aFileby itself).
Folder uploads rebuild a tree
Folders reach the client two ways. The picker is input type="file" with the webkitdirectory attribute, which returns a flat list where each file has a webkitRelativePath; a drop gives directory entries the client walks. Either way the result is a list of files with relative paths like Trips/2026/Rome/IMG_0001.jpg, and the server needs folders to exist before files can go into them.
So the client first collects every distinct folder path, creates the folders top-down (parents before children, siblings in parallel), and keeps a map from local path to new folder id. Then it uploads files into those ids: 5,000 files in 120 folders means 120 small create requests, then 5,000 uploads. If folder creation fails halfway, the files for the missing folders wait as failed with a clear reason, and Retry resumes from the map.
Names can collide. Drive allows two files with the same name in one folder, because identity is the id; Dropbox, which is closer to a disk, renames the new file with a suffix like "report (1).pdf". Our client follows the server's rule and shows the result. Before starting, the client also checks the storage quota: if the batch is 18 GB and 4 GB is free, it says so up front instead of failing at file 900.
Downloading many files
One file is easy: the server returns it with Content-Disposition: attachment and the browser's own download manager takes over, with its own progress and resume. Many files, or a folder, need to become one zip.
Decision Where should a multi-file download be zipped?
What we pick: Server-side zipping, which is what Drive does: a small "Preparing download" card appears and the zip arrives when it is ready. The client's job is the state of that request: preparing, ready, failed, and cancelled if you close the card.
preparing, ready, failed) and hands the final URL to the browser's download manager, which can pause and resume on its own.Check your understanding · Exercise 6
Someone drops a folder of 2,000 photos into Trips, then opens Recent while it uploads. Which design choices keep this working? Select all that apply.
Search and previews
Search must be server-side: the client holds a few hundred items while the person has hundreds of thousands, and the server can search inside documents and PDFs. The client's job is a fast box, clear filters, and results that live in the same store as everything else.
- Suggestions while typing: after a short debounce (about 150 ms), the box asks for 5 to 8 quick matches on names and shows them in a dropdown, with the newest request winning and older ones aborted with
AbortController. This is the autocomplete guide's design reused, including the combobox ARIA pattern. - Full results on Enter: a listing like any folder, with page tokens, the same views, selection and drag, plus each result's location.
- Filters as chips: type (folders, PDFs, images, spreadsheets), owner (me, not me, a person), and modified date (today, last 7 days, a range). They map to the server's query language, and they live in the URL (
?q=budget&type=pdf&modified=7d) so a search can be shared, bookmarked and restored with Back. - Results are items in the store: a search listing holds ids, so renaming a file from the results updates its folder view too.
The preview overlay
Double-clicking an image, PDF or video opens a full-screen overlay above the folder, not a new page, so closing it returns you to the same scroll position and selection. The URL changes (for example /file/ID/view) so the preview can be linked and Back closes it. The left and right arrow keys step to the previous and next previewable file in the current listing order, which is just moving an index through the listing's ids.
Decision How do we load the viewers for images, PDFs and video?
What we pick: Lazy-load per type, and start the import when a previewable file is selected or hovered for about 200 ms, so by the time the double-click lands the code is often ready. While a big PDF loads, the overlay shows the thumbnail we already have, then page 1, then the rest as you scroll. When you press the right arrow, we preload the next file's thumbnail and metadata, not the whole file.
Previews run untrusted content. A PDF or SVG uploaded by anyone must never run script on our origin, so the server renders previews from a separate, sandboxed domain (Google serves user content from domains like googleusercontent.com for this reason) and the overlay shows it in a sandboxed frame or as plain images. Files the browser can't show get a clear "No preview available" with Download and Open with buttons.
Check your understanding · Exercise 8
Which choices fit search and preview in this design? Select all that apply.
Staying in sync with changes made elsewhere
You have Q3 Reports open. A colleague drops two files into it, your phone renames a photo, and someone moves budget.xlsx elsewhere. Within a few seconds your screen should show all three, without a reload and without losing scroll position or selection.
Decision How does the client learn about changes?
What we pick: A push nudge plus the change feed, with slow polling (about every 60 s) as a safety net. The nudge carries no data, like the Drive API's changes.watch notifications, which only tell a webhook that changes exist so the app calls changes.list. The cursor makes everything exact; the nudge only makes it fast.
Patching the store
Each change says which item changed and gives its new state, or says it was removed (deleted, or you lost access). The sync layer applies them in order:
- Updated: replace the item record. If
parentIdchanged, remove the id from the old folder's listing and add it to the new one, if those listings are loaded. - Added to a loaded folder: insert the id at its sorted position if that position falls in the loaded range; if it sorts past the last loaded page, do nothing now, the next page will contain it.
- Removed: delete the record, drop the id from every listing and from the selection, and close it in the tree.
- Unloaded folders: nothing to patch, but mark their cached listings stale so they refetch when opened.
New items don't make the list jump under the pointer. If the change lands above the viewport, the virtualizer keeps the first visible item in place by adjusting the scroll offset. A short highlight shows what arrived.
When your action and theirs collide
- The file you are viewing is deleted. The preview shows "This file was moved to the trash by Maya" with a Close button, instead of a broken viewer.
- You move a file someone else just moved. The server applies requests in the order it receives them; the last one wins. Your client sees the final state in the feed and shows it.
- Your own changes come back as echoes. Optimistic moves are pending operations on top of server state. A feed change that matches one drops it; a change to an item with a pending operation is applied first and the operation replayed on top, so your UI doesn't flicker back.
- The cursor is too old. After a laptop sleeps for days, the server may refuse the cursor. The client then refetches the visible listings and gets a fresh start token.
With several Drive tabs open, one tab can be elected leader (with the Web Locks API) to hold the connection and share changes with the others over BroadcastChannel, so 5 tabs cost one connection, not 5. While a tab is hidden, it stops polling and catches up from its cursor on visibilitychange.
Check your understanding · Exercise 9
The push connection dropped for 2 minutes and reconnected. What does the client need to do? Select all that apply.
Trash, storage quota, and offline
Delete in Drive is a soft delete: the item moves to Trash with trashed: true and keeps its id, its sharing and its old parent. Since 13 October 2020, Drive deletes items for good 30 days after they enter the trash, and trashed files still count against storage until then. The lifecycle is small enough to draw as a state machine, and the client needs every edge of it.
- Trash is optimistic with an Undo toast, like a move: the item leaves the listing at once, and Undo sends a restore.
- Trashing a folder trashes everything inside. The client only flips the folder; its descendants vanish with it, and the change feed patches search results that show them.
- Restore puts the item back under its old parent. If that parent is itself trashed or gone, the server picks a place (such as the root) and the toast says where: "Restored to My Drive".
- Delete forever is the one action with no undo, so it asks for confirmation and names the item and count.
- The Trash view shows how many days each item has left, so the 30-day rule is visible, not a surprise.
The storage quota
A free Google account gets 15 GB shared by Drive, Gmail and Photos. The left rail shows a bar ("9.2 GB of 15 GB used") from a small about request that returns the limit and usage, refreshed after uploads and deletes. Near the limit (say 90 percent) the bar turns to a warning color with a link to manage storage.
At the limit, uploads are blocked before they start, as the uploads chapter described, with a message that says how much space the batch needs. A "Storage" view lists files by size so people can free space; it is just a listing sorted by quotaBytesUsed on the server.
Offline
Google's own offline mode for Drive on the web needs Chrome or Edge with the Google Docs Offline extension, and it covers Docs, Sheets and Slides; other files go offline through the Drive for desktop app. Details change over time, so in an interview, don't promise full offline. Our design takes a smaller, honest step.
A service worker caches the app shell, and IndexedDB (see the IndexedDB lesson) stores the last 50 or so folder listings with their item records. Opening a cached folder offline shows it at once with a banner: "You're offline. Showing files from 10:42". Actions that need the server (move, share, upload) are disabled with that reason, and uploads already in the queue pause and resume when the online event fires.
The Gmail guide goes further: it queues writes in an IndexedDB outbox and replays them later. For a file manager, read-only offline covers most needs.
Check your understanding · Exercise 10
Which statements about the trash and quota are true for this design? Select all that apply.
Architecture, data model and API
With the hard parts designed, we can draw the whole client. The rule behind every boundary: data that many views share lives in one store, behavior that outlives a page lives in an app-level service, and views only read the store and send intents.
| Part | Owns | Why it is separate |
|---|---|---|
| App shell | Top bar, left rail, upload panel, toasts, the router outlet | It never unmounts, so anything that must survive navigation is mounted here. |
| Views | Folder view, Recent, Shared with me, Search, Trash, Preview overlay | Each is a route that reads a listing and renders it; they hold no copies of items. |
| Item store | Item records by id, listings by key, pending operations | One record per item keeps five views consistent after one change. |
| View state | Selection, anchor, focus, sort, list or grid | Belongs to one listing on one screen, not to the items. |
| API client | Requests, page tokens, retries, aborts, per-item request ordering | One place for auth, errors and races. |
| Sync service | Change cursor, push connection, polling fallback, applying changes | Runs whatever page is open; patches the store, never a view. |
| Upload manager | Queue, chunks, resume data, folder creation map | Must outlive views; reuses the file uploader's engine. |
| Offline cache | Service worker shell, IndexedDB listings | Survives reloads; read on start before the network answers. |
The data flow for the main screen has one path in and one path out. Reads: the view asks the store for a listing; if it is missing or stale, the API client fetches a page, the response is normalized into the store, and the view re-renders. Static code and thumbnails come from the CDN, metadata from the API.
Writes: the view sends an intent such as "move these ids to that folder". The store applies it optimistically and records a pending operation, the API client sends it, and the reply either confirms the operation or rolls it back. Changes from elsewhere enter through the sync service and land in the same store.
Rendering: the shell and the first folder can be server-rendered for a fast first paint, but almost everything after that is client-side, because a file manager is a long-lived, highly interactive session where most time is spent after load. The Gmail guide makes the same call for the same reason.
Think first
Why does the sync service write to the store and never call views directly?
Because a change can affect views that aren't open, or several that are: the folder, the tree, Recent and search can all show the same item.
Through the store, each view picks up the change like its own edits, and closed views find fresh data when they open.
It also makes the sync service testable on its own: feed it changes, assert the store.
Data model: what lives where
We have met every entity along the way; here they are in one place. Where each one lives decides how long it lasts and who can see it.
| Entity | Lives in | Notes |
|---|---|---|
| Item (file, folder, shortcut) | Normalized store by id; IndexedDB copy for recent folders | Includes capabilities for the current person. |
| Listing (folder, Recent, search) | Store, keyed like folder:ID:name or search:QUERY | Ids, next page token, total if known, fetched time. |
| Pending operation | Store | Move, rename, trash with its undo data; replayed over server state. |
| Selection, anchor, focus | View state of the open listing | Cleared when the listing changes. |
| Current folder, view mode, search and filters, preview id | URL | Linkable, and Back works. |
| Upload jobs | Upload manager; resume data in IndexedDB | Survive navigation and, for big files, reloads. |
| Change cursor | Sync service; saved in IndexedDB | Lets a reopened tab catch up from where it stopped. |
| Permissions list for one item | Fetched when the share dialog opens | Not kept in listings; most items are never shared from this screen. |
type Selection =
| { mode: "some"; ids: Set<ItemId> }
| { mode: "all"; folderId: ItemId; except: Set<ItemId> };
type ViewState = {
selection: Selection;
anchorId: ItemId | null;
focusId: ItemId | null;
};
type PendingOp =
| { kind: "move"; opId: string; ids: ItemId[]; to: ItemId; from: Record<ItemId, ItemId> }
| { kind: "rename"; opId: string; id: ItemId; name: string; before: string }
| { kind: "trash"; opId: string; ids: ItemId[] };
type Permission = {
id: string;
type: "user" | "group" | "domain" | "anyone";
email?: string;
role: "owner" | "organizer" | "fileOrganizer" | "writer" | "commenter" | "reader";
inheritedFrom?: ItemId; // absent when direct
};
The API
A REST-style API is enough; the main requests are listing pages, batch mutations, the change feed, and sharing. Names below are ours; the Drive API has close equivalents (files.list, files.update with addParents and removeParents, changes.list, permissions.create).
GET /api/folders/FOLDER_ID/children?orderBy=folder,name&pageSize=100&pageToken=T1
200 OK
{
"items": [ { "id": "f_91", "name": "Q3", "kind": "folder",
"capabilities": { "canEdit": true, "canShare": true } } ],
"nextPageToken": "T2",
"total": 30412
}
POST /api/items:batchMove
{ "opId": "op_7", "ids": ["a","b","c"], "to": "f_91" }
200 OK
{ "results": [ { "id": "a", "ok": true }, { "id": "b", "ok": true },
{ "id": "c", "ok": false, "error": { "code": "PERMISSION_DENIED",
"message": "You no longer have edit access to this item." } } ] }
GET /api/changes?cursor=C_1043&pageSize=200
200 OK
{
"changes": [
{ "itemId": "a", "removed": false, "item": { "id": "a", "parentId": "f_91" } },
{ "itemId": "z", "removed": true }
],
"nextCursor": "C_1045"
}
POST /api/downloads { "ids": ["a","b","f_91"] }
202 Accepted { "jobId": "d_5" }
GET /api/downloads/d_5
200 OK { "state": "ready", "url": "https://dl.example.com/d_5.zip" }
GET /stream (WebSocket) server sends: { "type": "changed" }
Errors use one shape with a stable code the client can switch on (PERMISSION_DENIED, NOT_FOUND, QUOTA_EXCEEDED, CURSOR_EXPIRED, RATE_LIMITED) and a message safe to show. Every mutation carries an opId so a retry after a timeout is idempotent: the server sees the same opId and returns the first result instead of moving twice.
Check your understanding · Exercise 11
Why does the batch move return a result per item instead of one status for the whole request? Select all that apply.
Performance: open fast, hop folders instantly
People open Drive many times a day, so two moments matter most: the first load of the tab, and each folder open after that.
First load
- App shell first. The top bar, left rail and an empty list frame come from a cached shell, so something real paints before data arrives.
- Data in parallel with code. The first folder's page 1 request starts from the HTML (a
link rel=preloador an inline fetch), not after the JavaScript bundle runs, saving a full round trip. - Stale first, fresh next. If IndexedDB has this folder from last time, render it at once, then revalidate and patch. A listing from 10 minutes ago plus a correct patch beats a spinner.
- Split the rarely used parts. The share and Move to dialogs, the viewers and the upload engine load with
import()when needed or when the browser is idle. - Thumbnails stay off the critical path. The LCP element should be the list, not a photo tile; fixed-size tiles fade in without shifting layout.
Prefetching the folder you're about to open
When the pointer rests on a folder row for about 100 ms, or keyboard focus lands on it, the client fetches page 1 of that folder at low priority (fetch with priority: "low" where supported) and puts it in the store. By the time the double-click arrives, often 200 to 400 ms later, the data may already be there and the folder opens in one frame.
Limits keep this cheap: at most 2 prefetches in flight, none on Save-Data or slow connections, and prefetched listings expire after a minute or so.
Staying smooth while working
Virtualization keeps the DOM small, but updates must be small too. Each row subscribes to its own item and its own selected flag, so Ctrl-clicking one file re-renders one row, not 60. Marquee and drag hit testing run at most once per frame in requestAnimationFrame, however many pointer events arrive, and a burst of 500 changes from the feed is applied in one store transaction, so the views render once.
The client also reports its own timings to real-user monitoring: time to first folder, folder-open time split by cached, prefetched and cold, and INP on the list.
Check your understanding · Exercise 12
Which changes would cut folder-open time for a folder the person is about to click? Select all that apply.
Accessibility: a file manager for keyboards and screen readers
A file list with selection, drag and drop and context menus is hard to make accessible, but most of the work is a clear semantic model plus the keyboard rules from the selection chapter.
Decision What should the file list be in the accessibility tree?
What we pick: An ARIA grid for list view, and the same grid model for tiles (each row of tiles is a grid row). It matches the keyboard model we already built: one tab stop, arrows, Shift-arrows, Space, Ctrl+A. See the keyboard navigation and ARIA patterns lessons for roving tabindex and the grid pattern.
- Announce what changed. The polite live region from the selection chapter also says "Moved 3 items to Q3 Reports" after a move and "Upload complete, 182 files" at the end of a batch, not every percent.
- Keep focus somewhere sensible. After trashing the focused file, focus moves to the next row (or the previous one at the end), never to the page body. After a dialog closes, focus returns to the button that opened it.
- Offer a non-drag path for every drag. Move to, Upload buttons, and a context menu reachable with Shift+F10 or the Menu key.
- Virtualized rows keep their place. The focused row stays mounted even if it scrolls out of the render window, so the screen reader doesn't lose it.
- Motion and contrast. Drag animations and the tile fade-in respect
prefers-reduced-motion; selection is shown with color plus a check mark, not color alone.
Test it the way people use it: whole tasks with only a keyboard (open a folder, select three files, move them, undo), and the same tasks with VoiceOver and NVDA. Automated checkers catch missing labels, but only real runs catch a focus ring that vanishes after a move or a live region that reads 80 numbers.
Check your understanding · Exercise 13
A screen reader user is on row 4,012 of a virtualized 30,000-item folder. Which details matter? Select all that apply.
Running it in the interview
The risk in 60 minutes is touching everything and going deep on nothing. Show the data model early, then spend most of the time on two or three hard parts the interviewer cares about.
| Minutes | What to do | What to draw or say |
|---|---|---|
| 0 to 6 | Scope questions: folder size, single parent, shared drives, real-time, roles, offline | "A folder can hold 50,000 items, so I'll page and virtualize." |
| 6 to 10 | The five hard properties and the wireframe | Rail, breadcrumbs, list, actions bar, upload panel, toast. |
| 10 to 20 | Data model: items by id, listings of ids, one parent plus shortcuts | The item and listing types; why paths are not ids. |
| 20 to 28 | Architecture and data flow | Shell, views, store, API client, sync, upload manager. |
| 28 to 48 | Two or three deep dives the interviewer picks: selection, move with undo, sharing, sync | A sequence diagram for move with its failure path; capabilities, not roles. |
| 48 to 55 | API sketch | Listing page with token, batch move with per-item results, change feed. |
| 55 to 60 | Performance and accessibility, then trade-offs you'd revisit | Prefetch on hover, grid with roving focus, read-only offline. |
Follow-up questions to expect
Interviewers often pick one area and push until something breaks. Answer each follow-up with the failure it prevents, then the mechanism. Try these before reading the answers.
Think first
"Why not let a file live in two folders, like it used to?"
Many parents make the data a graph: several paths per file, so breadcrumbs and Move become ambiguous, and inherited access comes from several folders at once.
Shortcuts give the same convenience while the real item keeps one home. That is the trade Drive made in 2020.
Think first
"I rename a file in one tab. How does my other tab update?"
The rename goes to the server, which appends it to my change feed and nudges my connections.
The leader tab reads the feed and shares the changes over BroadcastChannel; each tab patches its own store, and every view of the item re-renders.
If the nudge is lost, the slow poll catches it, because the cursor records exactly what each tab has seen.
Think first
"What happens if I select all in a huge folder and press Delete?"
Selection is in "all except" mode, so the client sends the folder id, the exceptions, and a snapshot time, not 30,000 ids.
The server trashes in the background; the client shows a progress toast and removes rows as the feed reports them.
Undo sends a restore for the same job. Items added by others after the snapshot are left alone.
Summary
- Model Drive as a tree: one parent per item, shortcuts for extra places, stable ids, and paths computed for display only.
- Normalize: items live once by id; folder views, Recent, search results and the tree are listings of ids, so one change updates every view.
- Big folders need server-side sort with folders first, page tokens, and a virtualized list or grid that keeps about 60 rows in the DOM.
- Selection is view state: a set, an anchor and a focus, with an "all except" mode for folders the client hasn't fully loaded.
- Moves are optimistic with per-item results and an Undo that sends a reverse move, never a local-only rollback.
- Show what the person can do from server capabilities, not from role names, and label inherited access with its source.
- Stay in sync with a change feed and a cursor; a push nudge makes it fast, slow polling keeps it correct.
- Uploads, the upload panel and sync are app-level services, so they survive navigation; multi-file downloads are zipped on the server.
Check your answer
Tick what your own design covered. Pick the level you're interviewing for: each level includes the ones before it.
References
- Google Drive API: Retrieve changes Start page tokens,
changes.list, and push notifications that carry no details. - Google Drive API: files.list reference Page tokens,
orderBywithfolder, and query filters. - Simplifying Google Drive's folder structure and sharing models The 2020 move to one parent per item, with shortcuts.
- Google Drive trash items will be automatically deleted after 30 days The trash policy from 13 October 2020.
- Google Drive API: Manage folders with limited and expansive access Inherited access,
inheritedPermissionsDisabled, andcanListChildren. - Google Drive API: Manage pending access proposals How access requests are listed and resolved.
- Google Drive Help: Keyboard shortcuts for Google Drive on the web Drive's own selection, navigation and action shortcuts.
- MDN: HTMLInputElement.webkitdirectory Picking a whole folder, with
webkitRelativePathon each file. - WAI-ARIA Authoring Practices: Grid pattern Roving focus, selection and keyboard rules for interactive grids.
- MDN: HTML Drag and Drop API How files arrive from the operating system in
dropevents. - MDN: Element.setPointerCapture() Keeping a marquee or drag alive when the pointer leaves the element.
- WCAG 2.2: Dragging Movements (2.5.7) Why every drag needs a single-pointer alternative.
Concepts behind it
- Normalizing stateStoring each entity once, in a table keyed by its id, and having lists and relationships point at ids instead of holding copies, so one change updates every view that shows it.
- Pagination: offset, cursor, and keysetWays to load a long list one piece at a time, and why the choice decides whether users see duplicates, miss items, or wait on slow deep pages when the list changes.
- List virtualizationRendering only the rows the user can see in a long list, so a list of 100,000 items costs about as much as a list of 30.
- Optimistic updatesShowing the result of a user's action at once, before the server confirms it, and quietly undoing it if the server says no: the trick that makes likes, checkboxes, and chat messages feel instant on any network.
- Keyboard navigationMaking every action work without a mouse: a Tab order that follows the page, a focus ring you can always see, arrow keys inside composite widgets, skip links past repeated content, and no place where focus gets stuck.
- ARIA widget patternsThe tested recipes for custom widgets from the ARIA Authoring Practices Guide: combobox, listbox, menu, tabs, dialog, tree, grid, disclosure, and accordion, with the roles, states, and keys each one promises, and how to pick the right one.
- IndexedDBThe browser's built-in transactional database: object stores of structured values, indexes for queries, versioned migrations, and room for hundreds of megabytes, which makes it the place for offline data, drafts, and large blobs.
- Uploading filesHow a file gets from the user's device to your storage: through your server or straight to object storage with a pre-signed URL, in resumable chunks with honest progress, checked on both sides, and served back without opening a security hole.