A Philosophy of Software Design
Information Hiding and Leakage
Localize design knowledge so one decision can change without touching unrelated modules.
Information hiding means placing a design decision inside one module and preventing other modules from depending on it. The hidden information might be a file format, a cache policy, a database schema, a wire protocol, or the rule for deriving a value. Callers should see a stable abstraction rather than the details that make it work.
This principle does more than restrict access to fields. A private field can still leak when callers must know how it is encoded, when it becomes valid, or which other method must run first. Information is hidden only when a caller can use the module without learning the decision.
One decision, one owner
Consider a service that imports customer records from comma-separated files. If the upload handler splits rows, the validator knows column positions, and the database writer translates special status codes, then the file format is spread across three modules. Adding a quoted field or renaming a column requires coordinated edits.
A dedicated importer can own the external format and return domain values:
type ImportedCustomer = {
email: string;
displayName: string;
isActive: boolean;
};
const customers = await customerImporter.read(upload);
await customerRepository.saveAll(customers);The repository no longer knows that the source was a CSV file. The upload handler no longer knows column positions. If the format changes, the importer is the only place that needs to understand both the old representation and the domain model.
Recognize leakage
Information leakage often appears in ordinary-looking code:
- The same constant or condition is repeated in unrelated modules.
- Several callers perform the same sequence before invoking an API.
- A database column name appears in presentation code.
- Callers branch on a value that should be interpreted by its owner.
- Two modules must always be changed together.
Passing details through parameters can also leak them. Replacing a global setting with five arguments does not hide the setting if every caller must still obtain and coordinate those values. A better boundary accepts the intent of the operation and resolves mechanics internally.
Avoid temporal decomposition
Temporal decomposition organizes modules around execution order: read, parse, validate, save. That sequence may be useful for describing a process, but it is not always a good ownership boundary. If parsing and validation both depend on the same format rules, separating them forces that knowledge to cross an interface.
Organize around knowledge instead. A customer importer can perform the format-specific reading and validation together, then return a clean domain object. Other validation that depends on account policy can remain with the account domain. The boundary follows what each part knows, not merely when each step runs.
Design test
Name the decisions that a proposed module hides. If the list is empty, the boundary may be adding navigation without reducing complexity.
Change becomes the proof
Information hiding is easiest to judge when requirements change. Replace the CSV file with a spreadsheet. Move stored reports from disk to object storage. Change a cache from time-based expiry to version-based invalidation. A strong design confines each change to the module that owns the relevant knowledge.
Perfect isolation is not always possible. Domain concepts legitimately cross boundaries, and some decisions affect the whole system. The aim is to make dependencies explicit and few. Each hidden decision reduces the amount of the system a developer must hold in mind, which is the central work of managing complexity.