0

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
    }
}