stevenacoffman/software-design
Software design and refactoring: deep modules, information hiding, and incremental change.
Use when judging whether a proposed optimization, rewrite, refactor, migration, or reorg is premature, or why a finished one didn't improve outcomes. Applies the 3T test: Thing (real bottleneck?), Time (right moment?), Trade-offs (all costs counted?).
Apply when a team needs a defensible, shared decision on whether full DDD is worth adopting for a project: at inception, in architecture review, or when someone challenges whether DDD overhead is justified.
Use when designing an API or function that throws for inputs already in the desired state (delete missing item, duplicate insert), when callers wrap calls in defensive exists-checks, or exceptions cross layers that can't handle them. Redefine semantics so the error vanishes.
Invoke before committing to a significant interface: new module, service boundary, public API, or library contract. Also when someone says the design is obvious or a review only tweaks names. Sketch two or more radically different designs and compare before implementing.
Use when replacing a library, framework, or large component used across many call sites in a live codebase without a feature freeze. Introduce an abstraction layer and migrate incrementally. Not git branching, not database schema migration, not small one-commit refactors.
Invoke when altering a live production database schema (add, rename, or drop columns or tables) without downtime. Applies the expand-contract (parallel change) lifecycle: expand, dual-write, backfill, migrate readers, stabilize, contract, instead of in-place renames.
Use when a module is painful to change: one class changes for many unrelated reasons (Divergent Change) or one logical change needs edits across many files (Shotgun Surgery). Not for whole-codebase smell sweeps or Long Function.
Use when deciding when and why to refactor during normal work: preparatory (code isn't shaped for a new feature), comprehension (code is hard to follow), litter-pickup (problems found in passing), or long-term structural fixes over weeks.
Use when deciding how to handle performance: optimizing while writing code, a slow system, or write it fast vs write it right. Endorses profile-then-tune over constant attention, since most time is spent in a small fraction of code and hot-spot guesses are usually wrong.
Use when refactoring and feature or bug-fix work are mixed in one session and you can't tell whether a failing test comes from structure or behavior changes. Enforces the two-hats discipline of switching modes explicitly. Not for whether to refactor or long migrations.
Apply when a team wants speculative flexibility: future-proofing, plugin architectures, or abstract interfaces for extensibility with no current use case. Pairs YAGNI with refactoring: build only what's needed now and refactor safely when new needs arrive.
Use when classes or services are split by processing step rather than knowledge: Reader/Parser/Processor/Writer for one format, open-process-close triads, or microservices per saga step sharing a schema. Detects temporal decomposition that leaks information across modules.
Use when a method, service layer, adapter, or decorator just forwards calls with the same arguments and adds nothing, or a variable is threaded through many signatures only the last one uses. Signals the wrong number of layers; merge or redistribute responsibility.
Use when an API pushes decisions onto callers: options structs where most fields stay zero, timeout, buffer, batch, or retry parameters set arbitrarily, or every caller passing the same value. Move the decision into the module. Not when callers hold context the module lacks.
Use in API design or review when methods are named for UI actions (backspace, onClickSave), the interface grows one method per feature request, or a method has a single caller. Prefer somewhat general-purpose interfaces that capture the underlying capability.
Invoke when a team is adopting DDD, planning service decomposition, or doubting its DDD payoff, especially when tactical patterns (entities, aggregates, repositories) exist but service boundaries feel wrong. Do strategic design and bounded contexts first.
Apply when designing inter-service communication or choosing sync vs async coordination, especially when reaching for a distributed transaction or synchronous RPC to update another bounded context atomically. Prefer eventual consistency across contexts.
Use when starting a new class, interface, or public method: write the interface comment before the implementation as a design check. Also when a module can't be understood from its interface comments, after a refactor changes semantics, or when a long comment hints at complexity.