Side Effect
Any change a function makes beyond returning its result.
Understand This First
- Algorithm – the pure algorithmic core is where side effects should be absent.
Context
At the architectural level, functions in software do two kinds of things: they compute a return value, and they change the world around them. A side effect is any change beyond the return value: writing to a database, sending an email, modifying a global variable, printing to the screen, altering a file on disk.
Side effects aren’t bad. Without them, software could never save data, talk to users, or reach other systems. But unmanaged side effects breed bugs, surprises, and debugging marathons. Knowing where side effects live in your system is how you build reliable software and direct AI agents that produce reliable code.
Problem
A function calculates a shipping cost. It returns the right number, but it also quietly updates a database record, logs a message that triggers a downstream process, and bumps a shared counter. When something breaks downstream, the cause is invisible from the function’s signature. How do you build systems where you can understand what a piece of code does without reading every line of its implementation?
Forces
- Side effects are how software interacts with the world, but each one makes behavior harder to predict and test.
- Adding “one more” effect to a function is easy in the moment. Accumulation makes the system opaque.
- Avoiding side effects sometimes means copying data or threading extra parameters through the call chain, which feels like overhead until you need to debug.
- Pure functions are trivial to test with input-output assertions. Testing side-effectful code requires mocks, stubs, or real infrastructure.
Solution
Make side effects visible, intentional, and concentrated.
Separate pure logic from effectful operations. Functions that compute results should not also send emails or write to databases. Keep the calculation in one function and the action in another. Gary Bernhardt called this the “functional core, imperative shell” pattern: the core computes, the shell acts. The core can’t call the shell; the shell feeds data in, gets results back, and performs whatever effects the situation requires. This is the same separation described in Determinism.
Make side effects explicit in names or types. If a function writes to a database, its name or documentation should say so. Some languages enforce this at the type level (Haskell’s IO monad, Rust’s ownership and borrowing model). In languages without that enforcement, naming conventions and code review carry the weight.
Localize effects. Side effects that happen in an unpredictable order or that touch shared global state are the hardest to reason about. Writing to a scoped output or returning a description of the effect (a command object, an event) rather than mutating a global keeps effects contained and testable.
How It Plays Out
An AI agent generates a function to process a customer order. The function validates the order, calculates the total, charges the payment, sends a confirmation email, and updates inventory, all in one block. It works, but it’s untestable as a unit: you can’t check the pricing logic without also triggering a real payment. A developer who understands side effects asks the agent to separate the pure calculation from the effectful actions, producing a testable core and a thin orchestration layer.
When reviewing agent-generated code, watch for hidden side effects: logging calls that trigger alerts, database writes buried inside utility functions, or HTTP calls inside what looks like a pure calculation. Agents optimize for “it works,” not for “the effects are visible.”
A team tracks down a mysterious bug: a report shows incorrect totals, but the calculation function looks correct. After hours of investigation, they find that a “helper” function called during the calculation modifies a shared list in place. The mutation is invisible from the call site. The fix: make the helper return a new list instead of modifying the input.
“Separate the pure order calculation logic from the side effects. The function should return the computed total and a list of actions to perform (charge payment, send email, update inventory) rather than performing them inline.”
Consequences
Pure functions can be tested with simple input-output assertions, no infrastructure required. Side-effectful code can be tested separately with focused integration tests. When bugs appear, you narrow the search to the effectful boundaries rather than suspecting every function in the call chain.
The cost is more functions and more explicit plumbing: passing dependencies in rather than reaching out for them. Strict separation also requires discipline that AI agents don’t exhibit on their own. You’ll need to review and restructure agent-generated code to keep the boundary clean.
Sources
The separation of pure computation from side effects is a central idea in functional programming, formalized in languages like Haskell through monadic I/O (Peyton Jones & Wadler, “Imperative Functional Programming,” 1993). Gary Bernhardt popularized the practical application for object-oriented and multi-paradigm codebases as the “functional core, imperative shell” pattern in his Destroy All Software screencast series (2012). The architectural parallel appears in Alistair Cockburn’s Hexagonal Architecture (Ports and Adapters, 2005), where the domain core has no knowledge of or dependency on the infrastructure that surrounds it.
Related Patterns
- Refines: Determinism – controlling side effects is the primary technique for achieving deterministic behavior.
- Depends on: Algorithm – the pure algorithmic core is where side effects should be absent.
- Enables: Event – event-driven designs often separate the recording of “what happened” (an event) from the side effects triggered by it.
- Contrasts with: API – API calls are intentional, visible side effects; the problem is unintentional or hidden ones.
- Contrasts with: Algorithmic Complexity – complexity analysis measures computation cost, while side effects concern observable changes beyond the return value.
- Enables: Concurrency – minimizing shared side effects reduces concurrency bugs.