Use coding principles to make a required change understandable and testable. Prefer clear names, explicit contracts and the simplest design that meets the requirement. Share code when it represents the same business rule; keep different policies separate even when their implementations look alike.
Confusing names, hidden dependencies and weak tests can make a change harder to assess. A cleanup also takes effort and can introduce a regression. For founders and developers, the useful question is which design makes the next real change easier to review without altering behavior that users still need.
The Agile Manifesto principles connect technical excellence, good design, simplicity and sustainable pace to agility. They do not provide a measured delivery-speed guarantee for a particular refactor.
Hapy’s engineering view
We judge coding principles by whether they help the product survive change:
- Can a new developer understand the module without guessing?
- Can the team add a feature without editing five unrelated areas?
- Can tests catch the risky behavior?
- Can the code reflect the business language, not only technical shortcuts?
- Can the team refactor without turning every release into a rewrite?
The answer depends on habits, not slogans. Pair coding principles with a practical software development environment, SDLC process, and QA methodology.
Apply principles to a concrete change
Simplicity and naming
KISS asks for the simplest design that meets the requirement, not the fewest characters. Prefer names that explain domain meaning and units. Avoid abbreviations or clever expressions that make a maintainer reconstruct the business rule.
For an illustrative JavaScript fee calculation with a nonnegative integer subtotal in cents:
// Before: compact, but the name and units are unclear.
const f = (m, s) => m || s >= 5000 ? 0 : 500;
// After: same rule for the stated input domain.
function shippingFeeCents(isMember, subtotalCents) {
if (isMember || subtotalCents >= 5000) return 0;
return 500;
}
The longer version is easier to explain. The threshold and fee are illustrative, not commerce advice. Preserve tests for members, nonmembers, and the 4999/5000 boundary when changing it. See the complete refactoring example for before-and-after checks.

DRY and YAGNI
DRY means avoiding multiple conflicting representations of the same knowledge. Share a shipping rule used by checkout and draft order review when both must change together. Do not combine unrelated customer and employee validation merely because the fields currently look alike.
YAGNI means avoiding speculative functionality until there is a requirement. It does not justify omitting known security, reliability, or accessibility needs. Prefer a small implementation with a clear extension boundary over a generic framework for imagined future products.
Check whether two policies should share code
Combine duplicate code only after confirming that its callers represent the same rule. Sandi Metz describes how an unsuitable abstraction can accumulate parameters and conditional paths as different requirements arrive. Similar syntax is not enough reason to share a policy.
Extend the hypothetical shipping example above. Checkout and an unconfirmed draft order preview both use the same membership status, currency and subtotal. Assume both must use the current policy. Before extracting one shared function:
- Confirm those assumptions with the person who owns shipping policy. Identify every caller, including any caller that must preserve an older quoted fee.
- Add behavior checks at both callers. For the stated input domain, use the expected fees below. Record expectations directly; do not calculate the expected value by calling the same helper being checked.
- Move only the common rule into
shippingFeeCents. Keep input validation and unrelated order work at their existing boundaries. - Run the same checks against both callers after extraction. Review errors, side effects and saved values as well as the returned fee. Passing these cases protects the listed behavior; it does not prove equivalence for every possible input.
| Membership | Subtotal in cents | Expected checkout fee in cents | Expected draft-preview fee in cents |
|---|---|---|---|
| Member | 0 | 0 | 0 |
| Nonmember | 4999 | 500 | 500 |
| Nonmember | 5000 | 0 | 0 |
| Nonmember | 5001 | 0 | 0 |
These proposed checks assume boolean membership and a nonnegative integer subtotal. They do not cover currency conversion, invalid input or external shipping services. An already confirmed order needs its agreed fee preserved; it must not silently acquire today’s policy through this refactor.
Now suppose staff shipments become free at every subtotal, while ordinary checkout keeps the rule above. A nonmember subtotal of 4999 would produce a fee of 0 for a staff shipment and 500 for ordinary checkout. Those are different policies. Do not change a shared function in a way that silently changes both. Separate the policies, or extract only a smaller concept that both still need. Accepting some duplication can make the two rules clearer than adding unrelated mode flags.
The decision is whether the policies must change together. The number of functions removed does not answer it. This is a labelled decision example and proposed test plan, not a report of a production implementation.
Refactoring and documentation
Refactoring improves structure while preserving observable behavior. It is distinct from a feature, bug fix, or wholesale rewrite. Establish regression coverage, make small changes, and review the result against the contract callers rely on.
Document intent, unusual constraints, interfaces, and operational decisions that the code cannot explain clearly. Comments that restate every line add maintenance work. Update documentation when behavior changes, and use names and tests to make ordinary behavior discoverable.
Keep responsibilities and contracts clear
| Principle | Useful meaning | Practical boundary |
|---|---|---|
| Single responsibility | Group code around one cohesive business responsibility or reason to change | Formatting an invoice and sending it over a network may need separate owners and tests; a class is not required for every tiny action |
| Separation of concerns | Give UI, domain rules, and persistence explicit interfaces | They collaborate through contracts; they are not completely independent systems |
| Encapsulation | Hide internal representation and protect valid state | Expose operations the caller needs, not unrestricted mutation of every field |
| Delegation | Let a collaborator perform the responsibility it owns | Delegate storage to a repository rather than repeating connection logic in every screen |
| Open/closed | Provide extension points for known kinds of variation | Stable behavior can accept a new policy without changing every caller; existing code still needs fixes and maintenance |
| Interface segregation | Clients should not depend on operations they do not need | Separate read-only access from mutation rather than forcing a report reader to implement delete methods |
| Liskov substitution | A replacement subtype must honor the contract expected of the original type | Do not require stricter inputs, weaken promised outputs, or introduce incompatible errors |
| Program to an interface | Depend on the behavior you need rather than a particular provider | Use the narrow contract that permits testing and replacement, without inventing layers for every dependency |
The Single Responsibility Principle explanation connects responsibility to reasons for change. The point is to limit the impact of unrelated changes, not to optimize line counts.
Interface segregation and substitution example
Suppose a report only needs to list invoices. Requiring a read-only source to implement deletion and throw an error creates a misleading contract.
// Before: too broad for a report reader.
interface InvoiceStore {
listIds(): string[];
deleteById(id: string): void;
}
// After: callers request only the capability they need.
interface InvoiceReader {
listIds(): string[];
}
interface InvoiceWriter {
deleteById(id: string): void;
}
function invoiceCount(reader: InvoiceReader): number {
return reader.listIds().length;
}
This illustrative TypeScript example splits read and write capabilities. A mutable store can implement both; a read-only source only implements InvoiceReader. Define whether listIds returns unique IDs, which invoices are visible, ordering, and failure behavior in the real contract. Type compatibility alone does not prove behavioral substitution. Returning unrelated data or requiring callers to catch a new unsupported-operation error can still break expectations.
Composition versus inheritance
Composition assembles behavior from collaborators. Inheritance establishes a subtype relationship. Prefer composition when behavior varies independently; use inheritance when a subtype genuinely satisfies the parent’s contract and the hierarchy remains understandable.
For example, a report exporter can accept a formatter and a destination. That avoids separate subclasses for every format/destination combination. The cost is more explicit wiring and interfaces. For one fixed output, a simple function may be enough. This is a design tradeoff, not a rule to replace all inheritance.
Use tests and evidence to judge the design
A code smell is a prompt to investigate a possible deeper problem, not proof that a rewrite is needed. A long function may express a coherent process; a short one can hide dangerous coupling.
For each change, ask what must remain true and test the risky boundaries: empty data, invalid input, authorization, external failures, and repeated requests as applicable. Favor assertions about observable behavior so implementation details can change without rewriting every test.

Before accepting a cleanup, check whether it makes the next real change easier to understand, review, and release. Balance its benefit against regression risk and opportunity cost. “Clean code at all costs” is not a useful project goal.
Keep a shared style guide, small reviews, and documentation that reflects the current system. Contact Hapy when you need a focused maintainability review tied to a concrete product change.

