Intro Functional Programming- 4: Real world Example
Shreyan Ghosh

Shreyan Ghosh @zenoguy

About: Technologia

Location:
India
Joined:
Jun 4, 2025

Intro Functional Programming- 4: Real world Example

Publish Date: Jun 6
5 0

From WhatsApp to Your Next Project: Making Functional Programming Work in the Real World

Part 4 of 4: From Code Chaos to Mathematical Zen

We've taken quite a journey together. We started with the frustrations of Object-Oriented Programming's complexity, discovered the mathematical elegance of pure functions, explored the power of higher-order functions, and marveled at the recursive beauty of pattern matching and lazy evaluation.

But there's an elephant in the room: Real-world software isn't a math puzzle.

Users click buttons. Data gets stored in databases. APIs fail and need retry logic. Logs must be written. Even the simplest CRUD application is built on side effects—Create, Read, Update, Destroy. Things change, state mutates, and the world is inherently messy.

So how do we reconcile functional programming's mathematical purity with the need to build actual applications that work in the real world?

The answer isn't to throw out everything functional programming taught us. It's to introduce mutability and side effects in a controlled, principled way that preserves the benefits of safety, predictability, and composability.

Today, we'll see how this works in practice, using real systems that serve billions of users.

The Billion-User Proof: WhatsApp and Discord

Let's start with some mind-blowing numbers:

  • WhatsApp: Handles over 100 billion messages per day, built on Erlang
  • Discord: Serves millions of concurrent users in real-time, powered by Elixir
  • Financial systems: Process trillions of dollars using functional languages
  • Telecom infrastructure: Achieves 99.9999% uptime (about 30 seconds of downtime per year) with Erlang

These aren't toy examples or academic exercises. These are production systems handling scale that would break most traditional architectures.

So what makes functional programming work so well for these demanding applications?

The Actor Model: Functional Programming Meets the Real World

Erlang and Elixir use something called the Actor Model of concurrency. Here's how it works:

  • Every "actor" (or process) is lightweight and isolated
  • Each process has its own private state
  • Processes communicate only through message passing
  • If one process crashes, it doesn't affect others
  • The system can run millions of processes simultaneously

But here's the key insight: Because functional programming ensures immutability, there's no risk of data corruption from concurrent processes modifying shared data.

Example: Real-Time Chat System

# Each chat room is its own process
defmodule ChatRoom do
  use GenServer

  # Initial state - immutable data structure
  def start_link(room_name) do
    GenServer.start_link(__MODULE__, %{
      name: room_name,
      users: [],
      messages: []
    })
  end

  # Handle new user joining
  def handle_call({:join, user}, _from, state) do
    new_state = %{state | users: [user | state.users]}
    {:reply, :ok, new_state}
  end

  # Handle new message
  def handle_cast({:message, user, text}, state) do
    message = %{user: user, text: text, timestamp: DateTime.utc_now()}
    new_state = %{state | messages: [message | state.messages]}

    # Notify all users (side effect, but controlled)
    Enum.each(state.users, fn user ->
      send(user, {:new_message, message})
    end)

    {:noreply, new_state}
  end
end
Enter fullscreen mode Exit fullscreen mode

Notice what's happening:

  • State is immutable—we create new state rather than mutating existing state
  • Side effects (sending messages to users) are explicit and controlled
  • Each chat room is isolated—if one crashes, others continue working
  • The system can handle thousands of chat rooms simultaneously

Controlled Mutation: The Best of Both Worlds

Functional programming doesn't reject mutable state outright—it refuses to let it run wild.

Instead of thinking "how can I mutate this value?", functional languages encourage you to think "how can I manage and isolate change so it's controlled and safe?"

Example: Website Visit Counter

Let's say we want to count page views—a classic case of shared mutable state.

In a traditional imperative language, you might use a global variable:

// Dangerous: global mutable state
let visitCount = 0;

function recordVisit() {
  visitCount++; // What if two requests hit this simultaneously?
}
Enter fullscreen mode Exit fullscreen mode

In Elixir (a functional language), we use an Agent—an abstraction for managed, stateful processes:

# Safe: controlled state management
{:ok, pid} = Agent.start_link(fn -> 0 end, name: :visit_counter)

# Increment the counter
Agent.update(:visit_counter, fn count -> count + 1 end)

# Read the counter
Agent.get(:visit_counter, fn count -> count end)
Enter fullscreen mode Exit fullscreen mode

Here's what makes this safer:

  • The counter value lives inside the Agent process
  • You can't touch it directly—you send it a function
  • That function gets applied to the state in isolation
  • Every update is serialized—no race conditions
  • Multiple processes can safely interact with the counter

This gives us mutable state under functional discipline:

  • Controlled access: No direct mutation
  • No side effects leaking out: Changes are contained
  • Safe concurrency: Updates are automatically serialized

The "Let It Crash" Philosophy

One of the most counterintuitive aspects of Erlang/Elixir systems is the "let it crash" philosophy.

Instead of trying to handle every possible error condition, you design your system to fail fast and recover gracefully.

Here's how it works:

When a chat room process encounters an error:

  1. It crashes immediately (no trying to "handle" the error)
  2. The supervisor detects the crash
# Supervisor that manages chat rooms
defmodule ChatSupervisor do
  use Supervisor

  def start_link do
    Supervisor.start_link(__MODULE__, :ok, name: __MODULE__)
  end

  def init(:ok) do
    children = [
      # If a chat room crashes, restart it
      {ChatRoom, "general"},
      {ChatRoom, "random"},
      {ChatRoom, "tech-talk"}
    ]

    # Strategy: if one child crashes, restart just that child
    Supervisor.init(children, strategy: :one_for_one)
  end
end
Enter fullscreen mode Exit fullscreen mode
  1. The supervisor restarts just that chat room
  2. Other chat rooms continue working normally
  3. Users might see a brief disconnect, then everything works again

This approach makes systems incredibly resilient. Instead of one bug bringing down the entire application, errors are isolated and automatically recovered.

Bridging Pure and Practical: Multi-Paradigm Reality

Here's the truth that academic functional programming courses often skip: Real systems aren't purely functional.

Even the most functional languages provide escape hatches for the messy real world:

  • Haskell has the IO monad and STM (Software Transactional Memory)
  • Clojure runs on the JVM and interoperates seamlessly with Java
  • Elixir provides GenServers, Agents, and ETS for stateful operations
  • F# integrates naturally with the .NET ecosystem

The key insight is that functional programming gives you a default of safety with controlled escape valves when you need them.

Example: Database Operations in Elixir

defmodule UserService do
  # Pure function - easy to test and reason about
  def validate_user(user_data) do
    case user_data do
      %{email: email, age: age} when is_binary(email) and age >= 18 ->
        {:ok, user_data}
      _ ->
        {:error, "Invalid user data"}
    end
  end

  # Controlled side effect - database interaction
  def create_user(user_data) do
    with {:ok, validated_user} <- validate_user(user_data),
         {:ok, user} <- Repo.insert(%User{}, validated_user) do
      {:ok, user}
    else
      error -> error
    end
  end

  # Pure function - business logic
  def calculate_subscription_price(user, plan) do
    base_price = plan.price
    discount = if user.is_student, do: 0.5, else: 1.0
    base_price * discount
  end
end
Enter fullscreen mode Exit fullscreen mode

Notice the pattern:

  • Pure functions handle business logic and validation
  • Controlled side effects manage database operations
  • Clear boundaries between pure and impure code
  • Composable design allows easy testing and modification

The Functional Programming Spectrum

Rather than thinking "functional vs. imperative," think of a spectrum:

  1. Pure Functional Core: Business logic, calculations, transformations
  2. Controlled State Management: Agents, GenServers, STM
  3. System Boundaries: Database I/O, HTTP requests, file operations
  4. Integration Layer: Interfacing with imperative systems

The magic happens when you push as much logic as possible toward the pure end of the spectrum, using controlled mechanisms for the parts that genuinely need state or side effects.

Why This Approach Wins

This hybrid approach gives you the best of both worlds:

From Functional Programming:

  • Predictable code: Pure functions always return the same output for the same input
  • Easy testing: No mocking required for pure functions
  • Safe concurrency: Immutability eliminates race conditions
  • Composability: Small functions combine into larger systems naturally

From Controlled Mutation:

  • Real-world compatibility: Handle databases, APIs, user interfaces
  • Performance optimization: Avoid unnecessary copying when appropriate
  • Ecosystem integration: Work with existing libraries and systems
  • Pragmatic solutions: Use the right tool for each specific problem

Your Next Steps

Ready to bring functional programming to your next project? Here's how to start:

Start Small

Don't rewrite your entire codebase. Begin with:

  • Utility functions that transform data
  • Business logic that doesn't require state
  • Data processing pipelines
  • Validation and formatting functions

Choose Your Language

  • Elixir: Excellent for web applications, real-time systems, IoT
  • Clojure: Perfect for data processing, web services on the JVM
  • F#: Great for .NET environments, financial systems
  • JavaScript/TypeScript: Start using functional patterns with existing tools

Apply the Principles

Even in non-functional languages, you can:

  • Prefer immutable data structures
  • Write pure functions when possible
  • Use higher-order functions (map, filter, reduce)
  • Separate pure logic from side effects

Learn the Patterns

Master these functional patterns:

  • Pipeline operations: Transform data through a series of functions
  • Error handling: Use Result/Maybe types instead of exceptions
  • State management: Isolate mutable state behind clean boundaries
  • Composition: Build complex behaviors from simple functions

The Future is Functional

We're living through a shift in how software is built. The challenges of modern development—massive scale, real-time requirements, distributed systems, concurrent users—all favor functional approaches.

Companies like WhatsApp didn't choose Erlang because it was trendy. They chose it because it could handle 50 engineers supporting 900 million users. Discord didn't pick Elixir for academic reasons—they needed a system that could handle millions of concurrent connections without breaking a sweat.

The mathematics that seemed so abstract in computer science class? It turns out to be the most practical approach for building robust, scalable systems.

From Chaos to Zen

We started this series talking about the chaos of complex object-oriented systems—the tangled webs of dependencies, the debugging nightmares, the fear of changing one thing and breaking another.

Functional programming offers a path to something different: code that's predictable, systems that scale gracefully, and the zen-like peace of knowing that your functions do exactly what they say they do, every time.

It's not about rejecting the real world. It's about bringing mathematical precision to the messy business of building software that matters.

The journey from code chaos to mathematical zen isn't always easy, but it's always worth it. And with the right tools, patterns, and mindset, you can start that journey today.

Your users—and your future self—will thank you.

Comments 0 total

    Add comment