Skip to content

Archive

Software Design

88 articles
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 Type Conditionals with Polymorphism

A type-based switch can be the simplest way to express a small rule. The trouble starts when the same distinction appears in several places. Pricing checks whether an order is standard or express. Delivery estimates check the same thing. Cancellation rules do too. Adding a new order type then means finding every branch that knows the list of types. The problem is not the switch syntax itself. The problem is distributed knowledge: several callers know which variants exist and which behavior belongs to each one.

Software Engineering 05 Sep 2026 10 min read

Replacing Primitive Values with Domain Types

Many programs represent important concepts with ordinary strings, numbers, and booleans. That is convenient at first. A customer ID is a string, an email address is a string, and a quantity is an integer, so using those primitive types seems sufficient. The trouble starts when values that have different meanings share the same representation. A function can receive a product ID where it expected a customer ID. Validation rules spread across callers. A number that means cents can be confused with one that means whole currency units. The compiler or runtime may see perfectly valid primitives even though the program has made a domain mistake.

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

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 9 min read

Moving Behavior Closer to the Data It Uses

A function often starts in a reasonable place and becomes awkward as the system grows. It reads several fields from another object, interprets those fields, applies rules to them, and repeats the same pattern whenever a new requirement appears. The problem is not simply that the function is long. The deeper problem is responsibility placement: one part of the system owns the data, while another part knows too much about what that data means.

Software Engineering 05 Sep 2026 8 min read

Isolating Volatile Dependencies Behind Stable Boundaries

A dependency can be technically easy to call and still be expensive to change. A pricing library may rename operations between releases. A shipping provider may expose a model that changes as its API evolves. An internal rules engine may be rewritten while the business workflow around it stays largely the same. When application code uses those changing details everywhere, each dependency change becomes an application-wide edit. The problem is not simply that the dependency changes. The problem is that knowledge of how it works today has spread into code that has different reasons to change.

Software Engineering 05 Sep 2026 11 min read

Designing with Preconditions and Postconditions

A function can have a precise type signature and still leave its most important rules implicit. A transfer operation may accept two accounts and an amount, yet the signature does not tell you whether the amount may be zero, whether the source needs enough funds, or what must be true after a successful transfer. When these rules remain scattered through comments, conditionals, and tests, callers have to reconstruct the contract themselves. That makes misuse easier and changes harder to reason about.

Software Engineering 05 Sep 2026 8 min read

Designing Modules Around Information Hiding

A module can have a small public API and still be difficult to change. The problem appears when callers must know details that supposedly belong inside the module: a storage layout, a retry rule, a naming convention, a calculation step, or the order in which internal operations happen. When those details change, callers change too. The boundary exists in the code, but it does not contain the knowledge that creates maintenance work.

Software Engineering 05 Sep 2026 9 min read

Designing Interfaces Around Client Needs

A shared component often starts with a small interface. As more callers arrive, new methods are added until every caller sees operations it never uses. The interface still works, but it now connects unrelated needs through one contract. That matters because an interface is a dependency. When a client depends on a broad contract, changes to unrelated parts of that contract can affect its compilation, tests, mocks, generated code, or understanding of the component even when the client needs only one capability.

Software Engineering 04 Sep 2026 8 min read

Translating Errors at Abstraction Boundaries

A function can hide how data is stored and still leak the storage technology through every error it returns. When that happens, callers become coupled to details the abstraction was supposed to contain. Suppose an order service asks a repository to load an order. The repository promises to find orders; it does not promise that callers understand SQL drivers, HTTP clients, file formats, or whichever mechanism happens to sit underneath it. If a missing order appears as a driver-specific NoRows exception today and an HTTP 404 exception after a migration, callers must change even though the repository’s meaning did not.

Software Engineering 04 Sep 2026 9 min read

Separating Decisions from Side Effects

Business logic often becomes difficult to test for a simple reason: the code that decides what should happen is mixed with the code that makes it happen. A function reads the clock, queries storage, applies pricing rules, sends an email, and writes an audit record. To test one discount rule, the test must now arrange several unrelated dependencies. When a failure occurs, it is harder to tell whether the decision was wrong or an external operation failed.

Software Engineering 04 Sep 2026 7 min read

Separating Commands from Queries

A method named getBalance looks harmless. A developer may call it twice, use it while debugging, or add it to a log statement without expecting the program to change. If that method also clears pending adjustments, increments a counter, or refreshes state, those ordinary actions can alter behaviour. The underlying design problem is not simply a poor method name. Reading information and changing state have different consequences, yet one operation is doing both.

Software Engineering 04 Sep 2026 10 min read

Separate Decisions from Side Effects

A function reads an order, checks inventory, chooses a shipping method, updates a record, sends an email, and writes a log entry. A small rule change arrives: express shipping is now allowed only when every item is in stock. The rule itself is simple. Testing it is not. To exercise the decision, a test may need a database, an inventory service, an email substitute, and careful setup for several unrelated operations.

Software Engineering 04 Sep 2026 8 min read

Removing Temporal Coupling from APIs

Some APIs look simple because each method is simple. The difficulty appears only when you try to use them: configure() must run before connect(), connect() before start(), and stop() is valid only after startup succeeds. When correctness depends on operations happening in a particular order, the code has temporal coupling. The word temporal refers to time or sequence: one operation is valid only because another operation happened earlier. Temporal coupling is not automatically a design flaw. Many real processes have genuine ordering constraints. The engineering problem is hidden temporal coupling: callers must remember an important sequence that the API does not make clear or enforce.

Software Engineering 04 Sep 2026 8 min read

Removing Hidden Temporal Coupling

Some code works only when its methods are called in the right order. The individual methods may look valid, yet swapping two calls or forgetting an earlier call produces an error much later. This is temporal coupling: one operation depends on another operation having happened earlier. The coupling is not automatically a design flaw. Opening a transaction before committing it is a real lifecycle constraint. The problem is hidden temporal coupling, where the API allows an invalid sequence and leaves callers to remember an undocumented rule.

Software Engineering 04 Sep 2026 8 min read

Reducing Change Amplification in Code

A requirement can sound small and still produce a surprisingly large patch. Add one order state, rename one business concept, or change one validation rule, and suddenly five modules, several tests, and a serializer all need coordinated edits. The problem is not simply that the codebase is large. It is change amplification: one conceptual change causes many implementation changes because knowledge about that concept is spread across the system. This article shows how to recognize change amplification, trace it back to duplicated knowledge, and reduce it without forcing unrelated code into one giant abstraction.

Software Engineering 04 Sep 2026 9 min read

Parse Boundary Data into Trusted Types

Validation often starts as a small check near the edge of a program. As the system grows, the same fact gets checked again in handlers, services, helpers, and background jobs because none of those places can tell whether the value they received has already been validated. The result is defensive code everywhere and uncertainty about what a function may safely assume. A useful design technique is to parse boundary data into a trusted type. Instead of checking a raw value and then continuing to pass that raw value around, convert it into a representation that can exist only after the required checks succeed. Core code receives that representation and can rely on the facts it expresses.

Software Engineering 04 Sep 2026 7 min read

Making Time an Explicit Dependency

Code that asks the system clock for the current time looks harmless. The difficulty appears when behavior depends on the answer. A test for an expired reservation may pass now and fail later. A boundary such as midnight can become awkward to reproduce. Business logic becomes tied to an environmental input that callers cannot see or control. A useful design move is to treat the current time as a dependency. Instead of letting decision-making code reach for a clock whenever it needs one, obtain the time at a clear boundary and pass the relevant value or clock into the code that makes the decision.

Software Engineering 04 Sep 2026 9 min read

Limiting Object Navigation to Protect Encapsulation

A method can depend on far more of a system than its parameter list suggests. The warning sign is often a chain that reaches through one object to inspect several others: order.customer.address.country.code The expression is short, but the caller now knows that an order has a customer, the customer has an address, the address has a country, and the country exposes a code. A change anywhere along that path can force the caller to change even when the caller’s real question has not changed.

Software Engineering 04 Sep 2026 9 min read

Keeping Framework Code Thin with Humble Objects

Some code is difficult to test for reasons that have little to do with the business rule it implements. A user-interface handler may need a framework event. A scheduled job may be created by a runtime. A message consumer may receive objects owned by a library. The code then mixes two jobs: interacting with an awkward environment and deciding what the application should do. When those jobs stay together, a small rule can require a large test setup. Developers may respond by skipping tests, mocking many framework details, or testing through slow integration paths even when the important logic is simple.

Software Engineering 04 Sep 2026 9 min read

Keeping Behavior Close to the Data It Needs

A method can belong to one module while doing most of its thinking with data owned by another. When that happens repeatedly, a small change to the data often forces changes in distant code as well. This design smell is commonly called feature envy: behavior appears more interested in another object or module than in the one that contains it. The useful lesson is broader than the name. When a piece of behavior depends heavily on particular data and the rules around that data, keeping them close usually makes those rules easier to find, protect, and change together.

Software Engineering 04 Sep 2026 10 min read

Designing for Reversible Decisions

Software teams make decisions with incomplete information. A library looks suitable until production traffic exposes a limitation. A pricing rule changes after customers use it. A component boundary that seemed natural becomes awkward when a new workflow arrives. The problem is not that developers sometimes choose incorrectly. The harder problem is making an uncertain choice so expensive to change that the system must live with it long after the assumptions behind it have failed.