Skip to content

Topic archive

Software Engineering

Software Engineering covers developer tooling, architecture, maintainability, engineering workflows, protocols, and practices that improve how software is designed and built.

460 articles
Software Engineering 07 Sep 2026 9 min read

Designing Strict Input Contracts

Being tolerant of imperfect input can look helpful. A parser silently fixes an invalid value, an API treats an unknown option as a default, or a service accepts several spellings for the same field. The immediate caller succeeds instead of receiving an error. The cost often appears later. Once clients discover that invalid input is accepted, they may depend on that behavior. Tightening validation then becomes a compatibility change, and different implementations may interpret the same malformed input differently.

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 07 Sep 2026 11 min read

Circuit Breakers for Failing Dependencies

A dependency can fail in a way that is worse than an immediate error. It may accept connections but respond slowly, time out repeatedly, or reject nearly every request while callers continue sending more work. If your application keeps calling that dependency for every incoming request, the original failure can consume connection pools, worker capacity, and latency budgets in your own service. Retrying every failure can increase the pressure further.

Software Engineering 07 Sep 2026 8 min read

Adding Behavior Safely with Sprout Methods

A small feature can become a risky change when it must be added inside a large method that has weak tests. The obvious edit may be only ten lines, but those lines become mixed with old branches, mutable state, database calls, or other behavior you do not fully understand. Testing the new rule then requires exercising the entire method. One useful response is a sprout method: put the new behavior in a new, focused method and make the existing code call it. The old method changes only enough to connect the new behavior. The new method can often be tested directly with much less setup.

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

Replacing Behavior Switches with Polymorphism

A switch statement is not a design problem by itself. When a program has a small, stable set of cases, one explicit conditional can be easier to read than a hierarchy of types. Trouble starts when the same type distinction controls behavior in several places. Adding one new case then means finding every switch that knows about that type. Missing one produces a system where the new case works in some operations but not others.

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

Grouping Related Parameters into a Parameter Object

A function can have individually reasonable parameters and still be difficult to use correctly. The problem often appears when several values travel together through many calls and only make sense as a group. Consider code that repeatedly passes startTime, endTime, and timezone. Each value has a clear type, but callers must remember their relationship: the end must not precede the start, and both timestamps are interpreted using the same timezone rule.

Software Engineering 06 Sep 2026 9 min read

Designing Tolerant Readers for Evolving Contracts

A service reads a response from another system. It needs two fields, but its deserializer models twenty. A harmless producer change adds a field, changes an unused field, or expands an enum that the consumer never acts on. The consumer still breaks because it accidentally depended on more of the contract than its job required. A tolerant reader avoids that unnecessary coupling. It reads the smallest part of an external representation that the consumer needs and rejects changes only when they threaten assumptions the consumer actually relies on.

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

Designing APIs with Preconditions and Postconditions

An API can have clear parameter names and still leave its most important rules unstated. Can a withdrawal amount be zero? Must an account already be open? If a call succeeds, is the balance guaranteed to have changed, or has the request merely been accepted for later processing? When those questions are unclear, callers make assumptions. Different callers may make different assumptions, and failures appear far from the decision that caused them.

Software Engineering 06 Sep 2026 11 min read

Design Retry Budgets to Prevent Retry Storms

Retries are one of the simplest reliability tools in distributed systems. A transient network failure, a short leader election, or a momentary overload can make a request fail even though the dependency becomes healthy again a few milliseconds later. Retrying can hide that temporary failure from the user. The same mechanism can also make an outage worse. If a struggling dependency starts failing requests and every caller retries immediately, the dependency receives extra work precisely when it has the least capacity to handle it. A small failure rate can turn into a retry storm: retries create more load, more load creates more failures, and those failures create still more retries.

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

Replacing Systems in Slices with the Strangler Pattern

A system can be difficult to change without being possible to replace in one safe step. The old application may serve real traffic, contain years of business rules, and depend on behavior that nobody has fully documented. A complete rewrite asks the team to reproduce all of that correctly before users receive any value from the new system. The strangler pattern takes a different approach: replace the system one well-defined slice at a time while the old and new implementations coexist.

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

Replacing Primitive Data with Domain Types

Many bugs begin with a value that is technically valid for its programming-language type but invalid for the job it represents. A quantity is negative. A percentage is passed as 20 where another function expects 0.20. Two string arguments are swapped because both look identical to the type system. The problem is not that strings and numbers are poor types. They are useful building blocks. The problem appears when a primitive value has accumulated rules or meaning that the primitive cannot express.

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.