mainzelmen renames, converts, and extracts data from files. It started on local directories, with each domain receiving a FileSystem protocol through a typed context. Adding Google Drive was the expected payoff: new class, same eight methods, no domain changes. The test I hadn't planned for came weeks later, when I removed the original adapter entirely.
The command that doesn't know
FileSystem is a Python Protocol - eight methods, no imports from infrastructure:
class FileSystem(Protocol):
def list_files(self, directory: Path, pattern: str = '*') -> list[Path]: ...
def read_file(self, path: Path) -> bytes: ...
def write_file(self, path: Path, data: bytes) -> None: ...
def rename_file(self, old: Path, new: Path) -> None: ...
def delete_file(self, path: Path) -> None: ...
def get_file_metadata(self, path: Path) -> FileMetadata: ...
def copy_file_times(self, source: Path, target: Path) -> None: ...
def file_exists(self, path: Path) -> bool: ...
Python
Each domain's Context holds a file_system: FileSystem field. The converter command reads a text file, generates a PDF, writes it, copies timestamps, deletes the original - five filesystem operations, all through ctx.file_system:
@dataclass(frozen=True)
class ExecuteConversionCommand:
operations: list[ConversionOperation]
def execute(self, ctx: Context) -> list[ConversionOperation]:
for op in self.operations:
text_data = ctx.file_system.read_file(op.source_path)
pdf_data = ctx.pdf_generator.convert_text_to_pdf(
text_data, author='mainzelmen', title=op.source_path.name
)
ctx.file_system.write_file(op.target_path, pdf_data)
ctx.file_system.copy_file_times(op.source_path, op.target_path)
ctx.file_system.delete_file(op.source_path)
Python
This code ran against LocalFileSystem for months. When GoogleDriveFileSystem took over - 157 lines of Drive API calls, per-folder caching, batch list operations - not a line changed.
What removal proves
The commit message read "Purge local filesystem integration." 46 files changed, over a thousand lines removed. Frontend components for directory browsing, upload routes, session handling for local paths, test fixtures wiring temporary directories - all gone.
Zero domain commands touched. Every execute(self, ctx: Context) method stayed identical.
Adding a backend tests whether the protocol is complete - whether eight methods cover every operation the domain uses. Removing one tests something harder: whether the protocol is a real boundary or a thin wrapper around the first implementation. A wrapper leaks assumptions into calling code - path separators, OS-specific errors, stat structures that only make sense on local disk. A domain-owned protocol defines what file operations mean in domain terms. The backends translate.
The acid test of an abstraction isn't whether you can plug in a second implementation. It's whether you can unplug the first one.
After the purge, InMemoryFileSystem - a dict-based fake satisfying the same protocol - still runs every domain test. No filesystem setup, no temp directories, no cleanup. The domain's ignorance is structural, not a matter of discipline.