Skip to content

Archive

Maintainability

114 articles
Software Engineering 08 Sep 2026 8 min read

Designing for Change Locality

A product rule changes from “free delivery above $50” to “free delivery above $60.” The code change sounds small, but the developer has to edit a checkout service, an order validator, a receipt formatter, and two unrelated utility modules. Missing one location leaves the system internally inconsistent. The problem is not simply that several files changed. Some changes legitimately cross many files. The warning sign is that one conceptual decision is represented in several places that must change together.

Software Engineering 08 Sep 2026 8 min read

Choosing Coverage Criteria for Conditional Logic

A test suite can report high coverage and still miss an important bug. The problem is often not the percentage itself, but what the percentage measures. Consider a decision such as isMember && hasCredit. A test may execute the if statement, both outcomes of the decision, or each individual condition in different ways. Those are different kinds of evidence. Treating them as interchangeable makes coverage numbers more reassuring than they should be.

Software Engineering 07 Sep 2026 10 min read

Reducing Temporal Coupling by Making Order Explicit

Some APIs look simple because each method is simple. The difficulty appears only when you try to use them correctly: one method must run before another, a third method is legal only after some state change, and cleanup must happen at the end. The code is then coupled not only to what operations exist, but also to when they happen. This is temporal coupling: correctness depends on operations occurring in a particular order. Some ordering is unavoidable. You cannot read a file before opening it, and a transaction cannot commit before it begins. The design problem is hidden or unnecessarily fragile ordering, where callers must remember rules that the API does not make clear.

Software Engineering 07 Sep 2026 9 min read

Reducing Coupling with Connascence

Two pieces of code are coupled when a change in one can require a change in the other. That definition is useful, but it leaves an important engineering question unanswered: which coupling should you fix first? A shared constant, a parameter order, and a distributed workflow can all create coupling. Treating them as equally harmful leads to unnecessary abstractions in some places and fragile dependencies in others. Connascence gives a more precise mental model. Two software elements are connascent when they must agree in some way for the system to work correctly. By asking what must agree, how difficult that agreement is to maintain, how far apart the elements are, and how many elements participate, you can make better refactoring decisions.

Software Engineering 07 Sep 2026 10 min read

Reducing Change Coupling with the Law of Demeter

A line of code can be short and still know too much. Consider a shipping service that needs the destination country for an order: country = order.customer().profile().shippingAddress().countryCode() The line works, but it depends on several structural decisions at once: an order has a customer, the customer has a profile, the profile owns the shipping address, and the address exposes a country code. If any link in that path changes, the shipping service may need to change even though its actual responsibility did not.

Software Engineering 07 Sep 2026 9 min read

Managing Feature Flags as Temporary Code

A feature flag can make a risky change easier to release. Instead of making deployment and exposure happen at the same moment, the team can deploy code while keeping new behavior disabled, enable it for a limited audience, observe the result, and turn it off without rebuilding the application. That flexibility has a cost. Every flag introduces another condition that can affect behavior. If old flags remain indefinitely, developers must reason about combinations that no longer serve a useful purpose.

Software Engineering 07 Sep 2026 10 min read

Keeping Behavior Close to Data with Tell, Don’t Ask

A class can hide its fields and still force every caller to understand its rules. The usual symptom is code that asks an object for several values, makes a decision with those values, and then tells the object what state to change. That design spreads knowledge. When the rule changes, every place that reconstructed the rule may need to change too. Tell, Don’t Ask is a design heuristic for reducing that problem. Instead of asking an object for internal information so another object can decide what should happen, tell the object the intent and let the component that owns the relevant rules make the decision.

Software Engineering 07 Sep 2026 10 min read

Designing APIs for Observable Behavior

An API can keep every documented promise and still break its users. Suppose a function returns search results with no documented ordering guarantee. Its current implementation happens to return items alphabetically. A client notices that behavior and removes its own sorting step. Months later, the implementation changes and returns the same items in a different order. The API still satisfies its written contract, but the client breaks. This is the practical problem behind Hyrum’s Law: when an API has enough consumers, some consumers are likely to depend on almost any observable behavior, whether or not that behavior was intended as part of the contract.

Software Engineering 06 Sep 2026 9 min read

Using Snapshot Tests Without Freezing Incidental Details

Some outputs are awkward to test one assertion at a time. A serializer may produce a structured document, a formatter may emit many related lines, or a component may build a nested representation. Writing an assertion for every field can obscure the behavior you are trying to protect. A snapshot test takes a representative output and compares it with a previously accepted copy, called the snapshot. When the output changes, the test shows the difference and asks the developer to decide whether the new output is correct.

Software Engineering 06 Sep 2026 7 min read

Using Guard Clauses to Keep Control Flow Flat

A function often starts simple and becomes difficult to read as conditions accumulate. One check wraps another, the main work moves several indentation levels to the right, and a developer must remember which conditions are still true while reading the code. A guard clause handles a case that should stop or divert the current operation near the point where that case becomes known. Instead of wrapping the normal path in another conditional, the function deals with the exceptional, invalid, or inapplicable case and exits that path early.

Software Engineering 06 Sep 2026 9 min read

Replacing Branching with Lookup Tables

A chain of conditionals is not automatically a design problem. Sometimes each branch expresses genuinely different behavior, and an if or switch is the clearest way to show it. But another kind of branching appears when the logic is identical in every branch and only a value changes. That distinction matters. If the program is repeatedly asking, “Which constant belongs to this case?”, the conditionals are acting as a hand-written lookup mechanism. A lookup table can make the relationship explicit: keys represent cases, values represent the data associated with them.

Software Engineering 06 Sep 2026 8 min read

Reducing Temporal Coupling in APIs

Some APIs look simple because each method is simple. The difficulty appears only when you try to use them: one method must run before another, initialization must happen at exactly the right time, and an innocent-looking call fails because an earlier step was missed. This kind of order dependency is called temporal coupling. Two operations are temporally coupled when their correctness depends on when or in what order they happen. Some ordering is inherent to the problem, but hidden ordering makes code harder to understand, test, and change.

Software Engineering 06 Sep 2026 9 min read

Designing Modules with Information Hiding

A module can have private fields and still expose too much of its design. Callers may know how its data is stored, which steps must happen in which order, or which third-party concepts sit underneath it. When one of those decisions changes, code outside the module must change too. Information hiding is the practice of placing a design decision behind a boundary so other code depends on what the module provides, not on how it provides it. The goal is not secrecy. The goal is to contain the cost of change.

Software Engineering 06 Sep 2026 9 min read

Building Focused Test Data with Test Data Builders

Tests become hard to understand when creating the object under test requires many values that are irrelevant to the behavior being checked. A test for an overdue invoice may need an identifier, customer, currency, issue date, due date, line items, tax settings, and status even though only the due date matters. Copying complete fixtures into every test makes that irrelevant detail visible everywhere. Sharing one mutable fixture hides the detail, but it can make tests depend on each other. A test data builder offers a middle path: it creates a valid object from sensible test defaults while letting each test override only the values important to its scenario.

Software Engineering 05 Sep 2026 10 min read

Splitting Mixed Work into Explicit Phases

A function often starts with one job and gradually becomes a pipeline hidden inside a block of code. It reads raw input, interprets it, applies business rules, prepares parameters, performs an external action, and formats the result. Each step may be reasonable, but mixing them makes the whole function harder to understand and change. A useful response is Split Phase: separate work that happens for different reasons into explicit stages, and pass a meaningful result from one stage to the next.

Software Engineering 05 Sep 2026 7 min read

Separating Decisions from Side Effects with a Functional Core

Business logic often becomes difficult to test for a reason that has little to do with the rules themselves. A function decides what should happen while also reading the clock, querying storage, calling a service, sending a message, and writing logs. To test one decision, you must arrange all of those surroundings. A functional core, imperative shell design separates those concerns. The functional core receives ordinary data and computes decisions without performing external side effects. The imperative shell gathers inputs, calls the core, and carries out the resulting actions.

Software Engineering 05 Sep 2026 9 min read

Replacing Type Conditionals with Polymorphism

A conditional is often the clearest way to express a small decision. Problems begin when the same type question spreads through a codebase. One function asks whether a notification is an email or SMS to format it. Another asks the same question to validate it. A third asks again to calculate delivery cost. Adding a new notification type then means finding and changing several unrelated switches. This is a useful signal for polymorphism: different implementations respond to the same operation according to their own behavior. The goal is not to remove every if or switch. The goal is to stop many callers from repeatedly deciding what an object is before they can decide what it does.

Software Engineering 05 Sep 2026 7 min read

Replacing Repeated Null Checks with Null Objects

Optional collaborators often begin harmlessly. A service may accept an optional audit recorder, notification sink, or metrics collector. Then every operation that uses the collaborator grows the same check: if audit != null: audit.record("order_created", order.id) One check is easy to understand. Dozens of checks create a different problem: knowledge that the collaborator may be absent is scattered through code that should be focused on other work. A missed check can fail at runtime, while slightly different checks can produce inconsistent behavior.

Software Engineering 05 Sep 2026 8 min read

Replacing Magic Values with Named Concepts

A literal value can be perfectly clear when it describes the mechanics of a calculation. index + 1 usually needs no explanation. But the same syntax becomes harder to understand when a value carries a business rule or engineering decision: if retries > 3, price * 0.85, or timeout = 30. The problem is not that numbers or strings appear in code. The problem is that some literals have meaning that the code does not name. These are often called magic values.

Software Engineering 05 Sep 2026 9 min read

Replace Query-Then-Act with Intention-Revealing Operations

An object can expose perfectly reasonable getters and still make a system difficult to change. The problem appears when callers repeatedly read those values, interpret them, and then decide which mutation is allowed. Suppose several parts of an application do this: if order.status == "pending" and order.paymentReceived: order.status = "confirmed" The caller is not merely using data. It knows the rule for confirming an order. If another caller needs the same behavior, that rule is likely to be copied. When the rule changes, every copy becomes a place that can disagree.

Software Engineering 05 Sep 2026 8 min read

Refactoring Tangled Methods with Method Objects

A long method is not automatically a design problem. Sometimes a calculation is easiest to understand when its steps stay together. Trouble starts when one method accumulates many temporary values, later steps depend on several earlier results, and extracting any part requires passing a long list of arguments. At that point, ordinary Extract Method refactoring can feel blocked by the method’s local state. A method object is one way through that problem: move the computation into a short-lived object, turn the important local variables into fields, and then extract parts of the computation into small methods on that object.

Software Engineering 05 Sep 2026 8 min read

Reducing Object Graph Coupling with the Law of Demeter

A small change to an object model can cause surprising edits far away from the changed class. A developer moves an address under a customer profile, for example, and code in pricing, notifications, and reporting all breaks because each caller navigates the same chain of objects. The immediate problem looks like missing properties. The deeper problem is that those callers know the shape of an object graph they do not own.

Software Engineering 05 Sep 2026 10 min read

Reducing Change Amplification by Keeping Decisions Together

A small requirement can produce a surprisingly large patch. Changing one pricing rule might require edits in an API handler, a validator, a report formatter, three tests, and a scheduled job. None of the edits is difficult by itself, yet missing one can leave the system inconsistent. This is change amplification: one conceptual change requires modifications in many places. The practical problem is not the number of files alone. It is that knowledge about one decision is scattered, so developers must rediscover every place that encodes it whenever the decision changes.

Software Engineering 05 Sep 2026 9 min read

Protecting Your Model with an Anti-Corruption Layer

Integrating another system often starts with a small amount of mapping code. Then its field names appear in business logic. Its status values enter conditionals. Its error codes shape application decisions. Months later, changing providers or even upgrading the integration requires edits across the codebase. The problem is not simply that the application has an external dependency. The deeper problem is that the external system’s model has become part of the application’s own model.