Skip to content

Archive

Maintainability

114 articles
Software Engineering 03 Sep 2026 11 min read

Reducing Temporal Coupling in Code

Some APIs look simple because each method is simple. The difficulty appears only when the methods must be called in exactly the right order. A report builder might require loadData() before render(). A client might require connect() before send(). A job might require prepare() before execute() and execute() before publish(). When those rules exist but are not visible in the interface, callers must remember history: What has already happened to this object? That dependency on operation order is called temporal coupling.

Software Engineering 03 Sep 2026 12 min read

Put Decisions Next to the Data They Need

A method can look perfectly reasonable while depending on far more knowledge than it should. Imagine checkout code that asks a customer object for membershipLevel, joinedAt, and totalOrders, then combines those values to decide whether the customer receives priority support. The calculation works, but checkout now knows both the customer’s data and the rule that gives that data meaning. When the rule changes, every caller that reconstructed it becomes a possible edit site.

Software Engineering 03 Sep 2026 6 min read

Managing Technical Debt with Explicit Payoff Triggers

Technical debt is not simply bad code. It is the future cost created when an engineering decision makes today’s change easier at the expense of later work. Some debt is accidental: a rushed abstraction becomes difficult to extend, or duplicated logic grows in several places. Other debt is deliberate. A team may accept a narrow implementation to meet a deadline because building the general solution immediately would cost more than the expected benefit.

Software Engineering 03 Sep 2026 9 min read

Hide Design Decisions Behind Stable Interfaces

A module can have a small public API and still be difficult to change. The problem appears when callers know details they do not actually need: a file layout, a cache key format, a third-party response shape, a particular algorithm, or the order of internal steps. Once those details escape, changing an implementation becomes a multi-module change. Code that should have been independent must now move together. Information hiding is a design principle for preventing that spread. The idea is simple: identify decisions that are likely to change, keep those decisions inside one boundary, and expose an interface based on what callers need rather than how the work is currently done.

Software Engineering 03 Sep 2026 10 min read

Directing Dependencies Toward Stable Code

A dependency can look harmless when it is introduced. A business rule calls a payment SDK directly, a reporting module knows the exact storage format, or an order workflow imports a concrete notification client. Each choice may save a small amount of code today. The cost appears later. When an external library, storage mechanism, or delivery channel changes, code that represents important business behaviour must change with it. Dependency direction is a way to reduce that coupling. The central idea is simple: code that expresses important, relatively stable policy should not have to know the details that are likely to change around it.

Software Engineering 03 Sep 2026 9 min read

Designing Modules Around Cohesion and Coupling

A codebase can be split into many files and still be difficult to change. A small feature may require edits in several modules, tests may need large fixtures, and one internal change may unexpectedly break distant code. The problem is often not the number of modules. It is where their boundaries are drawn. Two ideas are especially useful when evaluating those boundaries: cohesion and coupling. Cohesion asks whether the responsibilities inside a module belong together. Coupling asks how much one module depends on the details of another. Used together, they provide a practical way to decide where code should live and which dependencies deserve attention.

Software Engineering 03 Sep 2026 8 min read

Designing Invariants That Make Invalid States Hard to Represent

Many software defects are not caused by complicated algorithms. They happen because the program reaches a state that should never have been possible: an order has a negative quantity, a completed job has no completion time, or a configuration contains two options that cannot be enabled together. An invariant is a condition that must remain true for a particular object, module, or operation to be valid. Designing around invariants turns assumptions into enforceable rules. The result is usually less defensive code, clearer interfaces, and failures that occur closer to their cause.

Software Engineering 03 Sep 2026 12 min read

Designing for Localized Change

A small requirement can produce a surprisingly large code change. Suppose a product changes the rule for displaying customer names. The new rule sounds simple: show the preferred name when one exists, otherwise show the legal name. Yet implementing it requires edits in an API response mapper, an email formatter, an audit message, a report generator, and three tests that each reconstruct the same choice. The problem is not that five files changed. Some changes legitimately cross many files. The warning sign is that one decision had to be rediscovered and edited in several places.

Python 03 Sep 2026 9 min read

Design Managed Attributes in Python with property

Python code often starts with plain public attributes. That is usually a good default: order.total is simpler than a pair of trivial getter and setter methods when reading and writing the value needs no extra behavior. Requirements can change. A value may need validation, an attribute may become computed, or an existing public field may need to keep its interface while its internal representation changes. Python’s built-in property type lets a class place method logic behind normal attribute access.

Software Engineering 03 Sep 2026 10 min read

Dependency Injection Without a Container

Dependency injection is often introduced together with a framework or container. That can make a simple design idea look like infrastructure: register services, configure scopes, add annotations, and ask a runtime to assemble an object graph. The underlying technique is smaller. A component receives the collaborators it needs instead of constructing or locating them itself. Another part of the program decides which implementations to supply. For many applications, ordinary constructors and functions are enough. Manual dependency injection keeps object creation visible, makes required dependencies explicit, and avoids coupling application logic to a container API. A container can still be useful when the object graph becomes large or a framework owns object creation, but it is not a prerequisite for dependency injection.

Software Engineering 03 Sep 2026 8 min read

Creating Seams for Safer Code Changes

Some code is difficult to change for reasons that have little to do with the change itself. A function may read the clock directly, create its own network client, access a global configuration object, or write to a file in the middle of business logic. The requested change may be small, but verifying it safely becomes difficult because the code is tightly connected to things that are slow, unpredictable, or hard to reproduce.

Software Engineering 03 Sep 2026 8 min read

Controlling Mutation with Defensive Copies

A class can expose a small, carefully designed interface and still lose control of its own state. The problem appears when the class stores a mutable value that other code can also modify. Imagine an order object that accepts a list of line items. The constructor validates the list, calculates a total, and assumes the items will now change only through the order’s methods. If the caller still holds the same list, that assumption is false. The caller can modify the list directly, bypassing validation and leaving the order’s cached total inconsistent with its items.

Software Engineering 03 Sep 2026 6 min read

Choosing Test Doubles Without Hiding Design Problems

Test doubles are useful when a test needs control over a dependency that would otherwise be slow, unpredictable, expensive, or difficult to observe. They can also make a test suite fragile when every internal interaction is replaced and asserted. The goal is not to avoid test doubles. It is to use the least powerful double that gives the test the control or evidence it needs. Start with the reason for replacing a dependency Before introducing a double, identify what makes the real dependency unsuitable for this test.

Software Engineering 03 Sep 2026 9 min read

Choosing Composition Over Inheritance for Behavior Reuse

Reusing code can create a dependency that lasts much longer than the code being reused. A common example is inheritance: a new class extends an existing class because it needs some of its behavior. The first change is convenient, but later the subclass may inherit assumptions, state, and lifecycle rules that it never wanted. Composition offers a different relationship. Instead of saying that one type is a specialized form of another, an object receives or owns another object that provides a capability. The behavior can then be replaced without changing the object’s identity.

Software Engineering 03 Sep 2026 8 min read

Changing Interfaces Safely with Parallel Change

Changing a shared interface often looks simple in the code that owns it. Rename a method, replace a parameter, or return a richer result, then update the callers. The difficulty appears when many callers cannot change at the same moment. They may live in different modules, be maintained by different teams, or be deployed independently. A change that is correct in isolation can then create a period where old callers and new code cannot work together.

Software Engineering 03 Sep 2026 6 min read

Branch by Abstraction for Incremental Change

Large replacements are tempting because they promise a clean boundary between the old design and the new one. In practice, a long-running replacement branch accumulates integration risk while the main codebase continues to change. Branch by abstraction offers a different approach. Instead of separating the work primarily with a source-control branch, engineers introduce an abstraction around the behaviour being replaced. The old implementation remains usable while a new implementation is developed and adopted behind the same boundary.

Software Engineering 02 Sep 2026 5 min read

Parallel Change for Safe Interface Refactoring

Changing a shared interface is risky when many callers depend on it. A large “update everything at once” patch can work in a small codebase, but it becomes harder to review, deploy, and roll back as dependencies spread across modules or services. Parallel change is a refactoring technique that keeps old and new interfaces working side by side for a limited period. The migration happens in three stages: expand, migrate, and contract.

Software Engineering 02 Sep 2026 7 min read

Managing Dependencies as Explicit Boundaries

Third-party libraries save engineering time, but every dependency also adds a contract that your software must live with. That contract includes more than function signatures. It can include configuration formats, exceptions, lifecycle rules, performance characteristics, release policies, and assumptions that spread through application code. Dependency management is therefore partly a software design problem. The goal is not to avoid dependencies. It is to make important dependencies explicit, contained, and inexpensive to change.