8 Keep the necessary complexity separate
Simple and easy describe different things. A familiar framework may be easy to use while entangling several decisions. An unfamiliar design may require learning but allow each concern to be understood independently. Neither familiarity nor brevity is a sufficient measure of simplicity.
Puma's standards use this distinction to resist arbitrary proxies such as file count, type count, or call count. The relevant question is whether independent concerns can be understood and changed independently. A coherent module can be substantial because the problem itself is substantial. Splitting it into many fragments may move complexity into coordination without reducing it. Code Design at the inspected revision.
Locate the coupling that hurts
Imagine one function that downloads bytes, chooses a runtime, updates a catalog, renders progress text, and deletes old files. It may be easy to call. Its internal decisions are entangled if changing a presentation label requires understanding publication order, or if supporting a new provider requires changing cleanup semantics.
Do not respond by extracting every step into a service. First identify the change that should be independent. Presentation might depend on a stable status model. Provider transfer details might sit behind an acquisition contract. Publication and catalog update may need to remain together because they preserve one invariant.
The design should follow the actual dependencies. Two operations occurring at different times do not necessarily belong in different owners. Two functions with similar code do not necessarily express the same rule.
Parnas's classic modularity comparison asks which design decisions should be hidden, rather than simply dividing a program by processing steps. It remains useful as a question about change: what should be able to vary without forcing unrelated consumers to vary? It is a design argument and worked comparison, not a theorem that more modules improve performance. On the Criteria To Be Used in Decomposing Systems into Modules.
Count the complexity you add
A refactor can remove one coupling and add several new concepts: an interface, registry, lifecycle, configuration format, and error translation layer. Those may be justified, but they are costs.
For the running example, extracting transfer adapters may make provider variation easier. If it also introduces a global plugin service, dynamic registration, remote discovery, and version negotiation for a single fixed provider, the abstraction may exceed the demonstrated need.
A useful explanation says both what becomes easier and what becomes harder. “The readiness rule has one owner, so adding a provider no longer changes publication. The cost is that each provider must translate its progress and errors into this contract.” That is more informative than calling the result clean.
The opposite problem is under-modeling. A dozen conditionals may seem smaller than an explicit state representation, but if they collectively encode a lifecycle that nobody can explain, the design is not simple in the relevant sense. Fewer declarations can conceal more mental work.
Separate policy from mechanism when they vary for different reasons
A transfer mechanism moves bytes. A retention policy decides how long to keep them. A scheduler decides when work may run. A readiness policy decides what a consumer can rely on. These may be implemented near one another, but their reasons for changing differ.
Not every separation needs a public interface. A private function or record can make a distinction clear without creating a new compatibility obligation. Public boundaries should be introduced where actual callers need them.
The level of abstraction also matters. A generic event system can represent every lifecycle transition, but may obscure the specific rules that make acquisition correct. A domain-specific operation may be easier to reason about because it narrows the permitted combinations.
Review a design through a change scenario
Choose a plausible change: add a provider, preserve a previous version, support cancellation during verification, or change the progress display. List the decisions that must be understood to make it safely.
Then ask which dependencies are necessary. If presentation depends on the meaning of ready, that is legitimate. If presentation depends on the provider's temporary filename scheme, the boundary may be leaking incidental detail.
A useful refactor reduces the number of unrelated decisions that must be held in mind for that change. It need not reduce lines or files. Test it by explaining the before and after paths to another developer. If the explanation simply substitutes five new abstractions for one old confusion, reconsider the design.