Design Google Drive: File Storage, Sharing, and Sync
When I reason through designing a file storage system, the first thing that trips me up is the file-and-folder hierarchy. At first it feels like a tree problem: folders contain files, folders can contain other folders, and you need to navigate the tree. That is true. But the question that reshapes the design is: should a file and a folder be different types, or the same type with different behavior?
If you make them different, you end up writing code that asks “is this a file or a folder?” everywhere. Every function that traverses the tree has to handle two cases. Every permission check has to branch. The Composite pattern solves this cleanly: a folder is a node that can contain children, a file is a leaf node that cannot, and both implement the same FileSystemNode interface. Code that navigates, shares, or versions the hierarchy never needs to know which kind of node it is holding. That is the insight that makes everything else in this design tractable.
The second interesting problem is permissions. Google Drive’s sharing model looks simple from the outside: you can be an Owner, Editor, or Viewer. But the inheritance question is subtle. If Alice shares a folder with Bob as Editor, does that permission automatically propagate to every file inside it? What if Charlie then changes the permission on one specific file? How do you resolve the conflict between the inherited folder permission and the explicit file permission? Reasoning through this cleanly is more interesting than the CRUD operations people usually focus on.
Requirements
Functional
- Users can create, read, update, and delete files and folders
- Files and folders can be shared with other users with Owner, Editor, or Viewer permission levels
- Permissions on a folder propagate to all children unless explicitly overridden
- Every file update creates a new version, and users can retrieve or restore previous versions
- A sync client detects local changes and propagates them to the server
Non-functional
- The design must handle deep folder hierarchies without special-casing at each level
- Changing the storage backend (local disk, S3, GCS) should not require changing domain logic
- Conflict detection when two users edit simultaneously should be addressable as an extension
Core Entities
FileSystemNode is the abstract base for everything in the hierarchy. It holds the common fields: ID, name, owner, creation timestamp, and a permission map. Both files and folders extend it. Code that traverses the tree operates on FileSystemNode and never needs to know whether it has a file or a folder.
File is a leaf node. It holds the current version reference and delegates to the storage backend for actual byte retrieval. It cannot have children.
Folder is a composite node. It holds a list of child FileSystemNode objects. Adding or removing a child changes the folder’s contents without knowing anything about what kind of node is being added.
FileVersion is an immutable snapshot of a file at a point in time. When a user uploads a new version, a new FileVersion is created and prepended to the file’s version history. The previous version is not deleted. It is just no longer the current one.
Permission represents a single user’s access level on a node. It holds the user reference, the role (OWNER, EDITOR, VIEWER), and whether it was explicitly set or inherited. That inheritance flag matters for the resolution logic.
ShareLink is an optional, token-based access mechanism. Rather than granting a specific user access, you generate a shareable URL. Anyone with the link gets the associated permission. This is a separate entity so it can be revoked independently of user-level permissions.
StorageBackend is an interface abstracting where file bytes actually live. Local disk and cloud object storage are two implementations. The rest of the system treats them identically.
SyncClient detects local filesystem changes, debounces them, and pushes diffs to the server. It also listens for server-side change notifications and applies them locally.
Class Design
+------------------------+
| FileSystemNode | <-- abstract
|------------------------|
| node_id: str |
| name: str |
| owner: User |
| created_at: datetime |
| permissions: dict |
|------------------------|
| get_permission(user) |
| share(user, role) |
| revoke(user) |
+------------------------+
^ ^
| |
+--------+---+ +---+--------+
| File | | Folder |
|------------| |------------|
| versions | | children |
| current_v | | |
| storage | | add_child()|
|------------| | remove() |
| read() | | list() |
| write(data)| +------------+
+------------+
+------------------+ +------------------+
| FileVersion | | Permission |
|------------------| |------------------|
| version_id: str | | user: User |
| file_id: str | | role: Role |
| storage_key: str | | inherited: bool |
| size_bytes: int | +------------------+
| created_at |
| author: User |
+------------------+
+------------------+ +-------------------+
| ShareLink | | StorageBackend |
|------------------| |-------------------|
| token: str | | + put(key, data) |
| node: FsNode | | + get(key) -> ... |
| role: Role | | + delete(key) |
| expires_at | +-------------------+
| is_revoked: bool |
+------------------+
+----------------------+
| SyncClient |
|----------------------|
| local_path: str |
| remote_root: Folder |
| _watcher |
|----------------------|
| start() |
| _on_local_change() |
| _apply_remote() |
+----------------------+
Key Implementation
from __future__ import annotations
import uuid
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum, auto
from typing import Iterator, Optional
class Role(Enum):
OWNER = "owner"
EDITOR = "editor"
VIEWER = "viewer"
@dataclass
class User:
user_id: str
email: str
@dataclass
class Permission:
user: User
role: Role
inherited: bool = False
@dataclass
class FileVersion:
version_id: str
file_id: str
storage_key: str
size_bytes: int
created_at: datetime
author: User
@staticmethod
def create(file_id: str, storage_key: str, size: int, author: User) -> "FileVersion":
return FileVersion(
version_id=str(uuid.uuid4()),
file_id=file_id,
storage_key=storage_key,
size_bytes=size,
created_at=datetime.utcnow(),
author=author,
)
class StorageBackend(ABC):
@abstractmethod
def put(self, key: str, data: bytes) -> None: ...
@abstractmethod
def get(self, key: str) -> bytes: ...
@abstractmethod
def delete(self, key: str) -> None: ...
class LocalStorageBackend(StorageBackend):
def __init__(self, base_path: str) -> None:
self._base = base_path
def put(self, key: str, data: bytes) -> None:
import os
path = os.path.join(self._base, key)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as f:
f.write(data)
def get(self, key: str) -> bytes:
import os
with open(os.path.join(self._base, key), "rb") as f:
return f.read()
def delete(self, key: str) -> None:
import os
os.remove(os.path.join(self._base, key))
class FileSystemNode(ABC):
def __init__(self, name: str, owner: User) -> None:
self.node_id = str(uuid.uuid4())
self.name = name
self.owner = owner
self.created_at = datetime.utcnow()
# explicit permissions set directly on this node
self._permissions: dict[str, Permission] = {
owner.user_id: Permission(user=owner, role=Role.OWNER)
}
def share(self, user: User, role: Role, inherited: bool = False) -> None:
self._permissions[user.user_id] = Permission(
user=user, role=role, inherited=inherited
)
def revoke(self, user: User) -> None:
self._permissions.pop(user.user_id, None)
def get_permission(self, user: User) -> Optional[Permission]:
return self._permissions.get(user.user_id)
def can_read(self, user: User) -> bool:
perm = self.get_permission(user)
return perm is not None
def can_write(self, user: User) -> bool:
perm = self.get_permission(user)
return perm is not None and perm.role in (Role.OWNER, Role.EDITOR)
@abstractmethod
def size_bytes(self) -> int: ...
class File(FileSystemNode):
def __init__(self, name: str, owner: User, storage: StorageBackend) -> None:
super().__init__(name, owner)
self._storage = storage
self._versions: list[FileVersion] = []
@property
def current_version(self) -> Optional[FileVersion]:
return self._versions[0] if self._versions else None
def write(self, data: bytes, author: User) -> FileVersion:
key = f"{self.node_id}/{uuid.uuid4()}"
self._storage.put(key, data)
version = FileVersion.create(
file_id=self.node_id,
storage_key=key,
size=len(data),
author=author,
)
# Prepend so index 0 is always current
self._versions.insert(0, version)
return version
def read(self) -> bytes:
if not self._versions:
raise ValueError(f"File {self.name!r} has no content yet")
return self._storage.get(self._versions[0].storage_key)
def restore_version(self, version_id: str) -> None:
"""Move the specified version to the front without deleting history."""
for i, v in enumerate(self._versions):
if v.version_id == version_id:
self._versions.insert(0, self._versions.pop(i))
return
raise ValueError(f"Version {version_id} not found")
def list_versions(self) -> list[FileVersion]:
return list(self._versions)
def size_bytes(self) -> int:
return self._versions[0].size_bytes if self._versions else 0
class Folder(FileSystemNode):
def __init__(self, name: str, owner: User) -> None:
super().__init__(name, owner)
self._children: dict[str, FileSystemNode] = {}
def add_child(self, node: FileSystemNode) -> None:
if node.node_id in self._children:
raise ValueError(f"Node {node.name!r} already exists in this folder")
self._children[node.node_id] = node
def remove_child(self, node_id: str) -> None:
self._children.pop(node_id, None)
def list_children(self) -> list[FileSystemNode]:
return list(self._children.values())
def propagate_permission(self, user: User, role: Role) -> None:
"""
Share this folder with user and cascade the permission
to all descendants that do not have an explicit override.
"""
self.share(user, role)
for child in self._walk():
existing = child.get_permission(user)
# Only set inherited permission if no explicit permission exists
if existing is None or existing.inherited:
child.share(user, role, inherited=True)
def _walk(self) -> Iterator[FileSystemNode]:
for child in self._children.values():
yield child
if isinstance(child, Folder):
yield from child._walk()
def size_bytes(self) -> int:
return sum(child.size_bytes() for child in self._children.values())
@dataclass
class ShareLink:
token: str
node: FileSystemNode
role: Role
created_by: User
expires_at: Optional[datetime] = None
is_revoked: bool = False
@staticmethod
def create(
node: FileSystemNode,
role: Role,
created_by: User,
expires_at: Optional[datetime] = None,
) -> "ShareLink":
return ShareLink(
token=str(uuid.uuid4()),
node=node,
role=role,
created_by=created_by,
expires_at=expires_at,
)
def is_valid(self) -> bool:
if self.is_revoked:
return False
if self.expires_at and datetime.utcnow() > self.expires_at:
return False
return True
Design Decisions and Trade-offs
Why Composite is the right call here. The alternative is a class hierarchy where Folder has a files: list[File] and a subfolders: list[Folder]. That forces every caller to handle two separate collections and check types constantly. With Composite, Folder._children is a single dict[str, FileSystemNode]. Move a file into a folder, move a folder into a folder, calculate total size recursively: all of these work identically regardless of node type. The only place the type distinction matters is when you need folder-specific behavior (listing children, propagating permissions), and that code lives inside Folder where it belongs.
Permission inheritance and explicit overrides. The propagate_permission method cascades a permission down to all descendants, but only if the descendant has no explicit permission already set (or only has a previously inherited one). This means explicit permissions win over inherited ones. If Alice shares a folder with Bob as Viewer, and then shares one specific file inside it with Bob as Editor, Bob keeps Editor access to that file even when Alice updates the folder-level permission later. The inherited flag on Permission is what makes this distinction trackable.
Versioning: full snapshots vs. deltas. This design stores full snapshots of each version. Every write creates a new storage key with the complete file contents. The simpler mental model is the primary advantage. The cost is storage: a 10MB document edited 100 times takes 1GB of version storage. Delta-based versioning stores only the diff between versions, which is far more space-efficient for large text documents but requires you to reconstruct the full file by replaying a chain of deltas. Google Docs actually uses operational transforms for collaborative editing (more on that in a separate post) which produces a natural delta log. For a basic Drive implementation, full snapshots are the right starting point. Deltas are an optimization you add when storage cost becomes a measured problem.
Storage backend as a Strategy. LocalStorageBackend and a hypothetical S3StorageBackend implement the same three-method interface. The File class never calls cloud-provider APIs directly. This means you can test the entire file hierarchy logic with LocalStorageBackend and swap to object storage in production without touching domain code. It also means a file’s storage location is configurable per file, which lets you implement tiered storage (hot files on SSD, cold archived versions in cheap object storage) by choosing the backend at write time.
Share links as a separate entity. The temptation is to model sharing as simply adding a permission to the node. But share links have their own lifecycle: they can expire, they can be revoked independently, and they grant access to anyone with the token rather than a specific user. Keeping ShareLink as a separate entity means you can invalidate all outstanding links for a file without touching user-level permissions, and you can audit link usage separately from direct share usage.
Conflict resolution: the gap this design leaves open. Two users downloading the same file, editing locally, and uploading back is the classic conflict scenario. The SyncClient detects that the server version changed since the client’s last sync and raises a conflict. The simplest resolution is “last write wins”: whoever uploads last has their version stored as current, and the other version is preserved in history. That is what this design does implicitly. The more correct approach for text documents is operational transforms or CRDTs, which allow merging concurrent edits. For binary files like images and spreadsheets, last-write-wins is often the only practical choice. Making the conflict resolution strategy explicit and swappable is the right design direction if you need to support both.
If you want to dig into how permission inheritance should behave at the edges, or why delta versioning is harder than it sounds, I’d enjoy the conversation. Reach out on Twitter or LinkedIn.
Tags:
Related Posts
Design a Load Balancer: Algorithms, Health Checks, and Session Persistence
A low-level design walkthrough of a load balancer covering round-robin, weighted, least-connections, and IP-hash algorithms, health checking, and the sticky sessions trade-off.
Design a Vending Machine: State Machines in Practice
A first-principles LLD walkthrough of a vending machine using the State pattern. Covers state transitions, inventory management, payment handling, and why State beats if/else chains when behavior must vary by state.
Design an ATM Machine: Transactions, State, and Security in LLD
A first-principles LLD walkthrough of an ATM covering state management, transaction types, card and account entities, cash dispensing logic, and why atomicity matters even in a machine-coding exercise.