Side effect — any change a function makes beyond its return value
Beginner5 min read·eng-12-023
interviewcompare
Concept
Side effect — any observable change a function makes to the world OUTSIDE of computing and returning its result.
What counts as a side effect:
- Writing to a database.
- Writing to a file or stdout.
- Sending an email or HTTP request.
- Modifying a global variable or a mutable object passed by reference.
- Reading from or writing to a cache.
- Logging.
- Throwing an exception.
- Generating a random number (technically, since it changes the RNG state).
What is NOT a side effect: Computing a value and returning it.
Why side effects matter:
- Testing: A function with side effects is harder to test — you need to verify the DB write, mock the email, etc.
- Reasoning: Pure functions (no side effects) are easy to understand — their only effect is the return value.
- Caching: A function with side effects can't be safely memoized — calling it twice would skip the side effect.
- Parallelism: Functions with side effects on shared state can race.
Side effects are necessary: You can't write a useful program with zero side effects — eventually you have to persist data, display output, or communicate. The goal is to ISOLATE and CONTROL side effects, not eliminate them.
Functional core, imperative shell: Push side effects to the "shell" (controllers, commands). Keep the "core" (domain logic) pure. Test the core cheaply; test the shell with integration tests.
PHP practices:
- Keep service classes that do pure computation separate from those that do I/O.
- Queue side effects (email, HTTP calls) so they don't slow down the primary flow.
- Use Eloquent events carefully — they're "hidden" side effects.
Code Example
php
<?php
// ❌ Function with side effects — hard to test, hard to reuse
function processOrder(array $data): array
{
$order = Order::create($data); // side effect: DB write
Mail::to($data['email'])->send(new OrderConfirmation($order)); // side effect: email
\Log::info('Order created', ['id' => $order->id]); // side effect: log write
Cache::forget('dashboard:order_count'); // side effect: cache write
return $order->toArray();
}
// Testing this requires: DB, mailer, logger, cache — all mocked or real
// ✅ Separated — pure computation + isolated side effects
class OrderProcessor // pure computation — no side effects
{
public function calculateTotals(array $items, float $taxRate): array
{
$subtotal = array_sum(array_column($items, 'price'));
$tax = $subtotal * $taxRate;
return ['subtotal' => $subtotal, 'tax' => $tax, 'total' => $subtotal + $tax];
}
public function validateOrder(array $data): array // returns errors, doesn't throw
{
$errors = [];
if (empty($data['items'])) $errors[] = 'Order must have at least one item';
if (empty($data['email'])) $errors[] = 'Email is required';
return $errors; // pure: same input → same output, no side effects
}
}
class OrderController extends Controller // "shell" — orchestrates side effects
{
public function store(CreateOrderRequest $request): JsonResponse
{
$processor = new OrderProcessor();
$errors = $processor->validateOrder($request->all()); // pure call
if ($errors) return response()->json(['errors' => $errors], 422);
$totals = $processor->calculateTotals($request->items, 0.10); // pure call
// Side effects happen HERE, in the controller (imperative shell):
$order = Order::create(array_merge($request->validated(), $totals)); // DB
SendOrderConfirmationEmail::dispatch($order); // queue
\Log::info('Order created', ['id' => $order->id]); // log
return response()->json($order, 201);
}
}
// Testing OrderProcessor — pure, no side effects, no mocking needed
class OrderProcessorTest extends \PHPUnit\Framework\TestCase
{
public function test_calculates_tax(): void
{
$processor = new OrderProcessor();
$result = $processor->calculateTotals([['price' => 100], ['price' => 50]], 0.10);
$this->assertEquals(165.0, $result['total']);
// No DB, no email, no mocking — it's a pure function test
}
}