cf.completefrontend
My progress
SYSTEM DESIGN · #106

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.

QuestionAnswer we assumeWhat 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

QualityTargetWhy this number
First loadApp 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 folderUnder 200 ms if cached or prefetched, under 1 s coldFolder hopping is the most common action. Cached folders should feel instant.
InteractionSelection, hover and drag feedback in one frame; INP under 200 msA file manager is judged by how it feels under the mouse.
Large foldersScroll a 50,000-item folder at 60 fps with under 150 rows in the DOM50,000 rows of DOM would take seconds to build and hundreds of MB of memory.
FreshnessChanges from elsewhere show within about 5 s while the tab is visibleFast enough that two people in a meeting see the same folder.
CorrectnessNever show a button the person can't use; never lose a move or upload silentlyPermission errors after a click feel broken. Silent loss of files is the worst failure.
AccessibilityEvery action works with the keyboard and a screen reader, including drag and dropDrag 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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.

Wireframe of the Drive main screen. A search box sits in the top bar. On the left is the rail with a New button, My Drive with the Q3 Reports folder in a tree, Shared drives, Trash, and a storage bar showing 9.2 of 15 GB. On the right are breadcrumbs My Drive / Team / Q3, an actions bar reading 3 selected: Share, Move, Download, and four file rows, two of them selected. At the bottom are a toast reading Moved 2 items, Undo, and the upload panel reading Uploading 3 of 12.drive.google.com/drive/folders/q3Search in Drive+ NewMy DriveQ3 ReportsShared drivesTrash9.2 of 15 GBMy Drive / Team / Q33 selected: Share, Move, DownloadInvoices (folder)budget.xlsxplan.pdfphoto.jpgMoved 2. UndoUploading 3 of 12
The rail, breadcrumbs and actions bar frame one big list. The toast and the upload panel live in the app shell, so they stay when you open another folder.

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?

Many parents (a graph)

Each item has a list of parent ids.

The same file appears in every folder that lists it.

Pros

  • Flexible: one file can live in Finance and in Q3 Report.
  • No extra shortcut items.

Cons

  • "Where is this file?" has several answers, so breadcrumbs are ambiguous.
  • Permissions inherit from several folders at once, which surprises people.
  • Deleting from one folder vs deleting everywhere is confusing.
  • Cycles become possible and must be prevented.

One parent plus shortcuts (a tree)

Each item has exactly one parentId.

A shortcut is its own item with its own parent and a targetId.

Pros

  • One path per item, so breadcrumbs and Move are simple.
  • Inherited access has one source.
  • A move is one field change.

Cons

  • Shortcuts can point to items you can't open or that were deleted.
  • Two items (shortcut and target) to render and keep in sync.

Path strings as identity

An item is known by its path, like /Team/Q3/report.pdf.

Like a disk or an object store key.

Pros

  • Easy to read and to show in breadcrumbs.
  • Natural for sync clients that mirror a disk.

Cons

  • Renaming a folder changes the identity of every item under it.
  • Links and caches keyed by path break on every move.

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.

ts
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;
};
Items live once in a map; listings only hold ids.
The file model. On the left, a tree: My Drive has two folders, Team and Photos. Team holds plan.pdf and the folder Q3. Photos holds a shortcut to Q3, drawn dashed, with an arrow labelled targetId pointing to Q3. Every arrow from a folder to a child is a parentId. On the right, the normalized store: items by id, such as Team with parentId root, Q3 with parentId t1, and the shortcut s7 with parentId photos and targetId q3, and a listing folder:t1 by name holding the ids p1 and q3.My DriveTeamPhotosplan.pdfQ3Q3 shortcutparentIdtargetIdone parent each, shortcuts pointnormalized storeitems[t1]: TeamparentId: rootitems[q3]: Q3parentId: t1items[s7]: shortcutparentId: photostargetId: q3folder:t1 by nameids: [p1, q3]
Every item has exactly one 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?

Offset pages (page=3&size=100)

Ask for items 200 to 299 in the chosen sort.

Pros

  • Easy to jump to any page.
  • Easy to show "page 3 of 500".

Cons

  • If a file is added or deleted while you scroll, items shift: you see one twice or miss one.
  • Deep offsets are slow on most databases.

Page tokens (cursors)

The server returns a nextPageToken with each page.

The client sends it back to get the next page. No token means the end.

Pros

  • Stable while items are added or removed.
  • Fast at any depth.
  • This is what the Drive API uses.

Cons

  • You can't jump to page 300 directly.
  • Tokens don't last forever (Drive says a page token is typically valid for several hours), so the client must be ready to restart from page 1.

Load all ids, then fetch details in visible ranges

One cheap request returns every id in sort order.

Rows ask for full details as they scroll into view.

Pros

  • The scrollbar is exact from the start.
  • Jumping to the middle is easy.

Cons

  • 50,000 ids is still a large first response.
  • Two round trips before a row has its name.

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.

Paging and virtualization for a folder of 30,000 items, 100 per page. On the left, page 1 and page 2 are loaded, page 3 is being fetched with the next page token, and pages 4 to 300 are not loaded. An arrow labelled rendered window goes from the end of page 2 to the right side, which shows what is in the DOM: 20 buffer rows, 19 visible rows, and 20 more buffer rows, about 60 rows in total.folder: 30,000 itemspage 1 loadedpage 2 loadedpage 3 fetchingpages 4 to 300pageToken: T3in the DOM20 buffer rows19 visible rows20 buffer rowsabout 60 rowsrendered windownear the end
The DOM holds about 60 rows whether the folder has 40 items or 30,000. Scrolling near the end of the loaded pages sends the next page token; rows of pages that haven't arrived show as skeletons.

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.

InputSelected setAnchorFocus
Click an itemOnly that itemThat itemThat item
Ctrl-click (Cmd-click on Mac)Toggle that item, keep the restThat itemThat item
Shift-clickReplace with the range from anchor to the item, in display orderUnchangedThat item
Ctrl+Shift-clickAdd the range to what was selectedUnchangedThat item
Arrow keyOnly the newly focused itemNewly focused itemMoves
Shift+ArrowRange from anchor to the new focusUnchangedMoves
Ctrl+Arrow, then SpaceArrow moves focus only; Space toggles the focused item (Drive uses Shift+Space)Space sets itMoves
Ctrl+A (Cmd+A)Everything in the listingUnchangedUnchanged
Escape or click empty spaceEmptyClearedUnchanged

"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.

State machine for a pointer press in the file list. Idle goes to Pressed on pointerdown. Pressed goes back to Idle on pointerup after moving less than 4 px, which counts as a click and updates the selection. Pressed goes to Marquee if the pointer moved 4 px after pressing on empty space; the marquee selects items by geometry. Pressed goes to Dragging if it moved 4 px after pressing on an item; drop targets light up. From Marquee or Dragging, pointerup applies the result and Escape cancels, both back to Idle.idlepressedmarqueehit test by mathdraggingtargets light uppointerdownup under 4 px: clickmoved, empty spacemoved, on an itempointerup: apply, to idleEscape: cancel, to idle
The 4 px threshold keeps a shaky click a click. Where the press started decides marquee or drag, and 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.

Hold

0 selected

A grid of 12 files in 3 rows of 4. 0 selected: none. Anchor: none. Focus: Budget.BudgetfocusContractDeckInvoiceLogoMemoNotesPhotoPlanReportSpecsVideo0 selectedanchor: none, focus: Budget
Click a file. Then try Ctrl-click, Shift-click, the marquee, or the arrow keys.

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?

Select only the loaded items

Add the 300 loaded ids to the set.

Pros

  • Simple; the set is always explicit.

Cons

  • Unless the bar says so plainly, moving "all" moves 300 of 30,000, and people find out later.

Load everything, then select

Page through the whole folder, then add all ids.

Pros

  • Exact and explicit.

Cons

  • 100 requests and seconds of waiting before the action bar even appears.

An "all except" mode

Selection becomes { mode: "all", folderId, except: Set }.

Ctrl-clicking an item adds it to except.

Bulk actions send the folder id and the exceptions to the server.

Pros

  • Instant, exact, and works for any size.
  • The count comes from the folder's total.

Cons

  • Every action needs a server endpoint that accepts "all in folder except".
  • Items added by others after you pressed Ctrl+A are a judgment call: the server should use a snapshot time.

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?

Native HTML drag and drop (draggable, dragstart, drop)

Mark rows draggable, put ids in dataTransfer, read them in drop.

Pros

  • Built in; the same events also receive files from the desktop.
  • The browser draws the drag image.

Cons

  • Little control over the drag image and the cursor.
  • Touch support for in-page drags is uneven across mobile browsers.
  • dragover fires over and over on the element under the pointer, and the dragged data can't be read until drop, only its types.

Pointer events with our own drag layer

On pointerdown past the 4 px threshold, show a floating "3 items" chip that follows the pointer.

Hit-test drop targets from registered rectangles and highlight the one under the pointer.

Pros

  • Full control: badge with count, auto-scroll, auto-expand, touch.
  • Works with virtualized rows because targets register as they mount.

Cons

  • More code, and we must handle Escape to cancel and pointer capture ourselves.

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 canAddChildren on 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 parentId up 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.

Sequence diagram of moving three files by drag and drop, between the User, the Drag layer, the Store, and the API. The user drops on Q3. The drag layer calls move with the ids and Q3. The store updates at once: the rows leave and a toast shows. The store sends POST batchMove to the API. The API replies that a and b are ok, and that c failed with 403. The store rolls back c and the toast changes to Moved 2 of 3. Later the user presses Undo, which sends a reverse move for a and b.UserDrag layerStoreAPIdrop on Q3move(ids, Q3)rows leave, toastPOST batchMovea, b: okc: 403roll back c, 2 of 3Undo: a, b go back
The screen changes before the network answers. The reply is per item, so only the failed file goes back, and Undo is a new request to the server, not a local rewind.

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 beforeunload prompt. 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 a File by 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?

On the client

Fetch each file, stream them through a zip library in a Web Worker, save the result.

Pros

  • No server work.
  • Starts right away.

Cons

  • Every byte crosses the network into the tab's memory or a stream; a 10 GB folder can crash the tab.
  • Saving a stream to disk without holding it in memory needs the File System Access API, which not every browser has.
  • Closing the tab loses the work.

On the server, as a job

The client posts the ids, the server zips in the background, the client polls (or listens) for status, then downloads one link.

Pros

  • Works in every browser, the browser's download manager handles the big transfer.
  • The server can split huge downloads into several zips.

Cons

  • A wait while zipping, so the UI needs a "Preparing download" state.
  • Server cost for CPU and temporary storage.

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.

Sequence diagram of downloading 40 files, between the User, the Client, the API, and a Zip worker. The user asks to download 40 files. The client sends POST /downloads and gets 202 with job id d_5. The API asks the zip worker to zip the 40 files. The client shows a Preparing download card. The client polls GET /downloads/d_5 and gets ready with a URL. The browser then saves the zip with its own download manager.UserClientAPIZip workerDownload 40 filesPOST /downloads202, jobId d_5zip 40 filesPreparing downloadGET /downloads/d_5ready, urlbrowser saves zip
The tab never holds the bytes. It tracks a small job (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.

Sharing and permissions: the UI as a function of access

Sharing is where a frontend bug becomes a security story: a Share button that shows for a viewer, or a link setting that looks restricted but isn't. The client never enforces access (the server does), but it must show access truthfully and never offer what will fail.

The share dialog

  • An input to add people and groups by name or email, with suggestions from contacts as you type (the same debounced, race-safe pattern as the autocomplete guide).
  • A role for the new people: Viewer, Commenter or Editor, a "Notify people" checkbox, and an optional message.
  • The list of people with access, each with a role menu and Remove. The owner is listed first and can't be removed.
  • General access: Restricted (only people added), your organization, or Anyone with the link, each with its own role. This is the setting people most often get wrong, so it shows a plain sentence: "Anyone on the internet with the link can view".
  • Copy link, which copies the item's URL. The link alone grants nothing unless general access allows it.

The dialog is a modal dialog with focus trapped inside and returned to the Share button when it closes (see the focus management lesson). Changes are not saved one by one: the person edits, then presses Save or Send, and the client sends one batch. That gives a clear moment to warn: "You are sharing with 2 people outside your organization".

Inherited access

Access flows down the tree. Share the folder Team with Ana as Editor, and Ana can edit every file and subfolder inside, including files added later. The file's own sharing list then has two kinds of entries: direct ones added on the file, and inherited ones that come from an ancestor folder.

The dialog must show both, and label inherited entries with their source, such as "Editor, from Team". You can't remove an inherited entry from the file; the dialog offers to open the folder where it was granted. In the Drive API, each permission carries permissionDetails with inherited and inheritedFrom fields for exactly this display.

Inherited access. The folder Team is shared with Ben as Editor and with anyone with the link as Viewer. Its subfolder Q3 inherits both. The file plan.pdf inside Q3 inherits them too, and also has Ana as a direct Viewer. The share dialog for plan.pdf lists Ana: Viewer, direct; Ben: Editor, from Team; and Link: Viewer, from Team. A note says the effective role is the strongest of the direct role and every ancestor's role.TeamBen: Editor, link: ViewerQ3inherits from Teamplan.pdfAna: Viewer, directinheritsinheritseffective role = strongest ofdirect and every ancestorplan.pdf, people with accessAna: Viewer, directBen: Editor, from TeamLink: Viewer, from Team
One parent per item means one chain of ancestors, so every inherited entry has exactly one source the dialog can name and link to.

With one chain of ancestors, a person's effective role on a file is the strongest of their direct role and every inherited role on the way up. Moving a file changes its ancestors and so can change who has access: people who had access only through the old folder lose it, while direct access stays. That is why the UI warns before such a move.

There is one exception, added to Drive in 2025: limited access folders. The owner (or a Manager in a shared drive) can turn off inheritance on a folder; people with only inherited access still see the folder but can't open it. In the API this is inheritedPermissionsDisabled on the folder, and capabilities.canListChildren tells the client to show a "You need access" state instead of an empty folder.

Limited access applies to folders only. Since September 2025, Drive no longer lets people restrict a single file inside a shared folder, so a file that needs tighter access goes into a limited access folder.

What the UI may show: roles or capabilities?

Decision How does the client decide which actions to show for an item?

Compute from the role on the client

The item says role: "reader"; a table in the client maps roles to allowed actions.

Pros

  • Small payload.
  • Easy to reason about.

Cons

  • Misses everything that isn't the role: an owner who stopped editors from re-sharing, a viewer download block, admin policies, items in a shared drive with different rules.
  • Two copies of the rules (client and server) drift apart.

Capabilities from the server

Each item comes with booleans for this person: canEdit, canShare, canRename, canTrash, canDownload, canAddChildren, canMoveItemWithinDrive, and more.

The UI reads them directly.

Pros

  • One source of truth: the server that will enforce the rule decides it.
  • Policies and special cases need no client change.
  • This is how the Drive API exposes it, through a capabilities object on each file.

Cons

  • Bigger item payloads (a few hundred bytes each).
  • After a sharing change, capabilities of many items may change at once, so the client must refetch them.

What we pick: Capabilities from the server. The client keeps a role-to-action table only to explain things ("Editors can rename"), never to decide. For a multi-selection, an action is enabled only if every selected item allows it.

Role on an itemTypical capabilities trueUI showsUI hides or disables
Vieweropen, download (unless blocked), copy linkPreview, Download, Request edit accessRename, Move, Delete, Share (hidden); Download disabled with a reason if blocked
Commenterthe above, plus commentComments in previewSame as Viewer for organizing
Editorrename, move within the drive, add children, trash, share (if allowed)Rename, Move, Upload here, ShareTransfer ownership; Share disabled if the owner turned off editor sharing
Ownereverything, including transfer ownershipAll actionsNothing
Content manager or Manager (shared drive)add, edit, move and trash inside the drive; Managers also manage members and move items outOrganizing actions for the whole driveTransfer ownership (the organization owns the files); member settings for Content managers

Hide or disable? A rule of thumb: hide actions that never apply to this role (a viewer never renames), and disable with a tooltip when the action normally exists but is blocked here ("The owner has turned off downloading"). Disabled buttons teach the rule; hidden ones keep the UI calm.

Try it in the lab below. Change the folder's sharing and see what Ana (direct on the file), Ben (through the folder) and Cy (outside, with the link) can see and do.

Show the UI for
Effective roles on plan.pdf inside folder Team. Ana: Viewer (direct). Ben: Editor (from Team). Cy: no access (no share). Ben's actions: Open on, Download on, Comment on, Rename on, Move on, Share on.Ana: ViewerdirectBen: Editorfrom TeamCy: no accessrequest accesswhat Ben sees on plan.pdfOpenonDownloadonCommentonRenameonMoveonShareonplan.pdf, people with access:Ana: Viewer, directBen: Editor, from TeamGeneral access: RestrictedBen: Editorfrom Team
Ben is an Editor on the folder Team, so he edits plan.pdf through inheritance.

The access request flow

Someone opens a link to a file they can't see. The server answers the metadata request with 403 or 404, and the client shows a "You need access" page with a Request access button, an optional message, and the account they are signed in as. The most common cause is the wrong account, so a Switch account link sits right there.

The owner gets an email and can approve the request with a role; the Drive API models pending requests as access proposals that an approver lists and resolves. On the requester's side, the item appears when the change feed reports it, and the page can offer "Open" without a reload.

Check your understanding · Exercise 7

Ben is an Editor on folder Team through a share, and Viewer directly on file plan.pdf inside it. Which statements are true? 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?

Bundle every viewer in the main app

The PDF renderer, video player and image viewer ship with the first load.

Pros

  • Preview opens with no extra download.

Cons

  • A PDF renderer alone can be hundreds of KB of JavaScript. Every visit pays for it, even if nobody opens a PDF.

Lazy-load the viewer for the file type

import() the PDF viewer when a PDF preview opens; start it on hover or on selection to hide the delay.

Pros

  • The main bundle stays small.
  • Each viewer is cached after first use.

Cons

  • A short wait the first time, so a spinner over the thumbnail is needed.

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?

Refetch the open folder now and then

Every 30 s, and on window focus, reload page 1 of the open folder.

Pros

  • Simple, no new server feature.

Cons

  • Misses changes on page 7, in the tree, in Recent and in search results.
  • Reloads everything to find one change; heavy for big folders.
  • Up to 30 s stale.

A change feed with a cursor, polled

The server keeps an ordered log of changes per person.

The client stores a cursor and asks "what changed since cursor X?" every few seconds.

Pros

  • One small request covers every view, every folder.
  • Nothing is missed: the cursor makes it exact.
  • This is the Drive API's model: changes.getStartPageToken, then changes.list with the token, which returns a newStartPageToken for next time.

Cons

  • Polling every few seconds costs requests even when nothing changed.

A push nudge plus the change feed

A WebSocket or server-sent events stream carries a tiny "something changed" signal.

On a nudge, the client reads the change feed from its cursor.

Pros

  • Changes show within about a second.
  • No traffic when nothing changes.
  • If a nudge is lost, the next poll or nudge still catches up, because the feed is the source of truth.

Cons

  • A long-lived connection to keep up, with reconnects and fallbacks.

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.

Sequence diagram of sync, between Other device, Server, Sync service, and Store. The other device renames photo.jpg. The server sends a nudge, changed, to the sync service. The sync service asks for changes since cursor C_1043. The server replies with 2 changes and the next cursor C_1045. The sync service applies them to the store in one batch. Later, after a long sleep, the sync service asks with cursor C_1045 and the server replies CURSOR_EXPIRED, so the sync service tells the store to refetch the visible listings.Other deviceServerSync serviceStorerename photo.jpgnudge: changedlist since C_10432 changes, C_1045apply in one batchdays later: C_1045CURSOR_EXPIREDrefetch visible
The nudge says only that something changed; the cursor says exactly what this client has seen. When the cursor is too old, the client falls back to refetching what is on screen.

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 parentId changed, 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.

State machine of an item's lifecycle. Active goes to Trashed on Trash. Trashed goes back to Active on Restore. Trashed goes to Gone after 30 days, or on Delete forever. Trashed items still use storage quota. Notes: Trash and Restore get an Undo toast; Delete forever gets a confirmation dialog; trashing a folder hides everything inside it; if the old parent is gone, the item is restored to My Drive and the toast says so.activetrashedstill uses quotagoneno undoTrashRestore30 daysDelete foreverTrash, Restore: Undo toastDelete forever: confirmtrashing a folder hideseverything inside itold parent gone? restored to My Drive, and the toast says so
Only one arrow can't be undone, and it is the only one behind a confirmation. Everything else is reversible, so it can be instant with an Undo toast.
  • 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.

Architecture of the Drive web client in three layers. UI views: App shell, Folder view, Preview, and Share dialog, which is lazy-loaded. State: the Item store, View state, and the URL. Services: API client, Sync service, Upload manager, and Offline cache. The Folder view sends intents to the Item store, and the store sends updates up to the views. The store reads through the API client, and the Sync service patches the store. The API client, Sync service and Upload manager talk to the server: API, change feed, and CDN.UI viewsApp shellFolder viewPreviewShare dialogStateItem storeView stateURLServicesAPI clientSync serviceUpload managerOffline cacheServer: API, change feed, CDNintentsupdatesreadspatches
Views never fetch or patch data themselves. They read the store and send intents; services that must outlive a page (sync, uploads) sit below the views, so navigation can't stop them.
PartOwnsWhy it is separate
App shellTop bar, left rail, upload panel, toasts, the router outletIt never unmounts, so anything that must survive navigation is mounted here.
ViewsFolder view, Recent, Shared with me, Search, Trash, Preview overlayEach is a route that reads a listing and renders it; they hold no copies of items.
Item storeItem records by id, listings by key, pending operationsOne record per item keeps five views consistent after one change.
View stateSelection, anchor, focus, sort, list or gridBelongs to one listing on one screen, not to the items.
API clientRequests, page tokens, retries, aborts, per-item request orderingOne place for auth, errors and races.
Sync serviceChange cursor, push connection, polling fallback, applying changesRuns whatever page is open; patches the store, never a view.
Upload managerQueue, chunks, resume data, folder creation mapMust outlive views; reuses the file uploader's engine.
Offline cacheService worker shell, IndexedDB listingsSurvives 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.

Data flow for the folder screen. Reads go down the middle: the Server API sends page JSON to the API client, which normalizes it into the Item store, which gives ids and records to the Folder view. Writes go up: the Folder view sends a move intent to the Item store, the store records a pending op and hands it to the API client, and the API client sends a batchMove request to the server. On the left, the server sends a nudge and the change feed to the Sync service, which sends changes into the Item store. On the right, the CDN sends JavaScript and thumbnails, and thumbnails go to the Folder view.Server APIAPI clientItem storeFolder viewpage JSONnormalizeids, recordsmove intentpending opbatchMoveSync servicenudge, feedchangesCDNJS, thumbnailsthumbnailsuser: open, drop, rename, scroll
Every path ends in the same store. A view can't tell a rename it made from one made on another device, and it doesn't need to.

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?

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.

EntityLives inNotes
Item (file, folder, shortcut)Normalized store by id; IndexedDB copy for recent foldersIncludes capabilities for the current person.
Listing (folder, Recent, search)Store, keyed like folder:ID:name or search:QUERYIds, next page token, total if known, fetched time.
Pending operationStoreMove, rename, trash with its undo data; replayed over server state.
Selection, anchor, focusView state of the open listingCleared when the listing changes.
Current folder, view mode, search and filters, preview idURLLinkable, and Back works.
Upload jobsUpload manager; resume data in IndexedDBSurvive navigation and, for big files, reloads.
Change cursorSync service; saved in IndexedDBLets a reopened tab catch up from where it stopped.
Permissions list for one itemFetched when the share dialog opensNot kept in listings; most items are never shared from this screen.
ts
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
};
View state, pending operations with their undo data, and a permission entry.

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).

http
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." } } ] }
A listing page with a token, and a batch move that answers per item.
http
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" }
The change feed, the zip job, and the push nudge that carries no data.

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=preload or 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?

A plain table with checkboxes

table markup, a checkbox per row, links for names.

Pros

  • Easy and familiar for screen reader users.

Cons

  • Tab visits every checkbox and link: two or more tab stops per row, thousands in a big folder.
  • No natural place for Shift-range or arrow-key selection.

A listbox with multiple selection

role=listbox with aria-multiselectable=true, each item role=option with aria-selected.

Pros

  • Simple model for a grid of tiles.

Cons

  • Options can't have interactive cells, so owner, date and size columns become one long label.

An ARIA grid with roving focus

role=grid with aria-multiselectable=true; rows with aria-selected; one tab stop, arrow keys move.

aria-rowcount and aria-rowindex describe the full folder even though only 60 rows exist in the DOM.

Pros

  • One tab stop for the whole list; arrows inside, as the APG grid pattern describes.
  • Columns stay columns, so a screen reader can read "Modified, 3 March".
  • Works for virtualized rows.

Cons

  • More work: roving tabindex, focus restore after re-render, and careful testing with real screen readers.

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.

MinutesWhat to doWhat to draw or say
0 to 6Scope 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 10The five hard properties and the wireframeRail, breadcrumbs, list, actions bar, upload panel, toast.
10 to 20Data model: items by id, listings of ids, one parent plus shortcutsThe item and listing types; why paths are not ids.
20 to 28Architecture and data flowShell, views, store, API client, sync, upload manager.
28 to 48Two or three deep dives the interviewer picks: selection, move with undo, sharing, syncA sequence diagram for move with its failure path; capabilities, not roles.
48 to 55API sketchListing page with token, batch move with per-item results, change feed.
55 to 60Performance and accessibility, then trade-offs you'd revisitPrefetch 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?"

Think first

"I rename a file in one tab. How does my other tab update?"

Think first

"What happens if I select all in a huge folder and press Delete?"

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.

Junior 0 of 4
Mid-level 0 of 5

References

Concepts behind it

Try next