Loading content...
In 1973, Carl Hewitt proposed a radically different approach to concurrent computation. Rather than threads sharing memory and coordinating through locks, he envisioned a world of independent actors—self-contained units of computation that interact only by sending messages to each other. No shared state. No locks. No race conditions.
This is the actor model, and while it remained largely academic for decades, it has found powerful expression in modern systems: Erlang's fault-tolerant telecommunications systems, Akka's distributed JVM applications, and Orleans' virtual actors in cloud gaming. WhatsApp's 900+ million users are served by Erlang actors. These aren't toy systems—they're production infrastructure handling millions of concurrent connections.
By the end of this page, you will understand the actor model deeply: its core principles and how they eliminate traditional concurrency hazards, the message-passing semantics that govern actor communication, how actors enable both local and distributed concurrency with the same programming model, supervision hierarchies for fault tolerance, and practical implementation patterns across actor frameworks.
The actor model is built on a small set of powerful principles that fundamentally change how we reason about concurrent systems.
What is an Actor?
An actor is the fundamental unit of computation. Each actor encapsulates:
Actors are reactive—they only execute in response to messages. Between messages, an actor is dormant, consuming no CPU cycles.
| Axiom | Description | Implication |
|---|---|---|
| Create | An actor can create new actors | Enables dynamic system structure |
| Send | An actor can send messages to other actors | All communication is asynchronous |
| Become | An actor can designate behavior for next message | State transitions are explicit |
Key Properties:
1. Isolation (Share Nothing)
Actors share no mutable state. Each actor's memory is completely private. The only way to affect another actor's state is to send it a message—which the receiver processes when ready. This eliminates:
2. Asynchronous Message Passing
Message sends are fire-and-forget—the sender doesn't wait for a response. Messages are queued in the receiver's mailbox and processed one at a time. This enables:
3. Sequential Message Processing
Each actor processes one message at a time, in order of arrival. This provides:
Threads share memory and require explicit synchronization. Actors share nothing and communicate through messages. A single thread can multiplex many actors (millions in Erlang). Actor systems are typically built on a small thread pool, with runtime scheduling actors onto threads as messages arrive.
Understanding the semantics of message passing is crucial for building correct actor systems.
Message Delivery Guarantees:
Different actor systems provide different guarantees about message delivery:
| Guarantee | Meaning | Examples |
|---|---|---|
| At-most-once | Message delivered 0 or 1 time | Default in Akka, Erlang (local) |
| At-least-once | Message delivered 1+ times | With acknowledgments and retry |
| Exactly-once | Message delivered exactly 1 time | Requires distributed transactions |
Message Ordering:
A critical question: if actor A sends messages M1, M2, M3 to actor B, in what order does B receive them?
Guarantee: Messages from one actor to another are delivered in send order (FIFO per sender-receiver pair).
Non-guarantee: Messages from different senders may interleave arbitrarily.
12345678910111213141516171819
// Message ordering example (Akka/Scala) // Actor A sends to C: M1, M2, M3// Actor B sends to C: X1, X2 // GUARANTEED: C receives M1 before M2 before M3// GUARANTEED: C receives X1 before X2// NOT GUARANTEED: relative order of M* and X* // Possible orderings C might see:// M1, M2, M3, X1, X2 (all A first)// M1, X1, M2, X2, M3 (interleaved)// X1, X2, M1, M2, M3 (all B first)// M1, X1, X2, M2, M3 (interleaved differently) // For global ordering, use techniques like:// 1. Single coordinator actor// 2. Sequence numbers// 3. Vector clocksRequest-Response Pattern:
While sends are asynchronous, we often need responses. The actor model handles this with explicit reply messages:
1234567891011121314151617181920212223242526272829303132333435
// Akka request-response pattern import akka.actor.{Actor, ActorRef, Props} // Request message includes reply-to addresscase class Request(query: String, replyTo: ActorRef)case class Response(result: String) class ServerActor extends Actor { def receive = { case Request(query, replyTo) => val result = processQuery(query) replyTo ! Response(result) // Send reply } def processQuery(q: String): String = s"Result for: $q"} class ClientActor(server: ActorRef) extends Actor { def receive = { case "start" => // Include self as reply address server ! Request("hello", self) case Response(result) => println(s"Got response: $result") }} // Modern Akka also supports ask pattern (Futures)import akka.pattern.askimport scala.concurrent.duration._ implicit val timeout = Timeout(5.seconds)val future = server ? Request("hello", _) // Returns Future[Response]The actor model philosophy prefers 'tell' (fire-and-forget) over 'ask' (request-response). Ask creates a Future that ties up resources waiting. Tell allows the sender to continue immediately. Design actors to tell each other what to do, not ask and wait for answers.
Erlang was designed from the ground up around the actor model (though Erlang calls actors "processes"). Developed at Ericsson for telecommunications, Erlang pioneered many patterns now common in actor systems.
Basic Erlang Actors (Processes):
123456789101112131415161718192021222324252627282930313233343536373839
-module(counter).-export([start/0, increment/1, get/1, loop/1]). %% Start a new counter actorstart() -> spawn(fun() -> loop(0) end). %% Create actor with initial state 0 %% Send increment messageincrement(Pid) -> Pid ! increment. %% Fire-and-forget send %% Send get request (include reply address)get(Pid) -> Pid ! {get, self()}, receive {count, Value} -> Value after 5000 -> timeout end. %% Actor's message processing looploop(Count) -> receive increment -> loop(Count + 1); %% Tail recursion with new state {get, From} -> From ! {count, Count}, %% Send reply loop(Count); %% Continue with same state stop -> ok %% Exit the loop (actor terminates) end. %% Usage:%% Counter = counter:start().%% counter:increment(Counter).%% counter:increment(Counter).%% counter:get(Counter). %% Returns 2Key Erlang/Actor Features:
1. Lightweight Processes
Erlang processes are extremely lightweight—a process uses only ~300 bytes initially. A single Erlang VM can run millions of processes. This enables "one process per connection" patterns that would be infeasible with OS threads.
2. Immutable Data
Erlang variables are immutable—once bound, they cannot change. This eliminates aliasing problems and makes message passing safe (no need to copy data).
3. Pattern Matching
The receive block uses pattern matching to select which messages to handle. Unmatched messages remain in the mailbox (selective receive).
4. Let It Crash
Rather than defensive programming with extensive error handling, Erlang embraces failure. Let processes crash; supervisors restart them. This leads to simpler code and more resilient systems.
WhatsApp serves 900+ million users with fewer than 50 engineers, running on Erlang. A single WhatsApp server handles 2+ million concurrent connections—each connection is an Erlang process. This demonstrates the actor model's power for massive concurrency.
Akka brings the actor model to the JVM ecosystem (Scala and Java), adding features for distributed systems, persistence, and cluster management.
Akka Actor Definition:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
import akka.actor.{Actor, ActorRef, ActorSystem, Props} // Message definitions (case classes for pattern matching)sealed trait CounterMessagecase object Increment extends CounterMessagecase object Decrement extends CounterMessagecase class GetCount(replyTo: ActorRef) extends CounterMessagecase class CountResult(value: Int) // Actor implementationclass CounterActor extends Actor { // Private mutable state private var count: Int = 0 // Message handler def receive: Receive = { case Increment => count += 1 println(s"Incremented to $count") case Decrement => count -= 1 println(s"Decremented to $count") case GetCount(replyTo) => replyTo ! CountResult(count) }} // Usageobject CounterApp extends App { // Create actor system (container for actors) val system = ActorSystem("counter-system") // Create counter actor val counter = system.actorOf(Props[CounterActor], "counter") // Send messages (fire-and-forget) counter ! Increment counter ! Increment counter ! Decrement // For request-response, create a temporary actor or use ask pattern import akka.pattern.ask import akka.util.Timeout import scala.concurrent.duration._ import scala.concurrent.ExecutionContext.Implicits.global implicit val timeout = Timeout(5.seconds) (counter ? GetCount(null)).mapTo[CountResult].foreach { result => println(s"Final count: ${result.value}") system.terminate() }}Akka's Typed Actors:
Akka Typed (now the recommended API) provides compile-time type safety for actor messages:
1234567891011121314151617181920212223242526272829303132333435363738394041424344
import akka.actor.typed.{ActorRef, ActorSystem, Behavior}import akka.actor.typed.scaladsl.Behaviors // Typed message protocolsealed trait Commandcase object Increment extends Commandcase class GetCount(replyTo: ActorRef[Int]) extends Command // Typed actor behaviorobject Counter { def apply(): Behavior[Command] = counter(0) private def counter(count: Int): Behavior[Command] = Behaviors.receive { (context, message) => message match { case Increment => context.log.info(s"Incremented to ${count + 1}") counter(count + 1) // Return new behavior with updated state case GetCount(replyTo) => replyTo ! count Behaviors.same // Keep same behavior } }} // Usage@main def run(): Unit = { val system: ActorSystem[Command] = ActorSystem(Counter(), "typed-counter") system ! Increment system ! Increment // Ask pattern with typed ActorRef import akka.actor.typed.scaladsl.AskPattern._ import scala.concurrent.duration._ given timeout: akka.util.Timeout = 3.seconds given scheduler: akka.actor.typed.Scheduler = system.scheduler given ec: scala.concurrent.ExecutionContext = system.executionContext val result = system.ask[Int](ref => GetCount(ref)) result.foreach(count => println(s"Count: $count"))}In Akka Typed, state changes are expressed by returning a new Behavior. The counter(count + 1) call returns a behavior that will handle the next message with the incremented count. This functional approach makes state management explicit and safer.
One of the actor model's most powerful features is its approach to fault tolerance through supervision hierarchies. Rather than handling every possible error locally, actors delegate failure handling to supervisors.
The Supervision Hierarchy:
Actors form a tree structure:
When a child actor fails (throws an exception), the supervisor decides how to respond:
| Strategy | Action | Use Case |
|---|---|---|
| Resume | Continue processing with current state | Transient error, state is valid |
| Restart | Stop actor, create fresh instance | Corrupted state, need clean slate |
| Stop | Terminate the actor permanently | Unrecoverable error, give up |
| Escalate | Pass failure to own supervisor | Don't know how to handle |
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
import akka.actor.typed.{ActorRef, Behavior, SupervisorStrategy}import akka.actor.typed.scaladsl.Behaviorsimport scala.concurrent.duration._ // Worker that might failobject Worker { sealed trait Command case class Process(data: String) extends Command def apply(): Behavior[Command] = Behaviors.receive { (context, message) => message match { case Process(data) => if (data == "fail") throw new RuntimeException("Simulated failure!") context.log.info(s"Processed: $data") Behaviors.same } }} // Supervisor with restart strategyobject Supervisor { sealed trait Command case class ProcessWork(data: String) extends Command def apply(): Behavior[Command] = Behaviors.setup { context => // Create supervised worker with restart strategy val worker: ActorRef[Worker.Command] = context.spawn( Behaviors.supervise(Worker()) .onFailure[RuntimeException]( SupervisorStrategy.restart .withLimit(maxNrOfRetries = 3, withinTimeRange = 1.minute) ), "worker" ) Behaviors.receiveMessage { case ProcessWork(data) => worker ! Worker.Process(data) Behaviors.same } }} // Usage:// supervisor ! ProcessWork("data1") // Works// supervisor ! ProcessWork("fail") // Worker crashes, restarts automatically// supervisor ! ProcessWork("data2") // Works (on restarted worker)Let It Crash Philosophy:
Erlang/OTP pioneered the "let it crash" philosophy:
This approach leads to simpler code and more resilient systems. The key insight is that most errors are transient—a fresh restart often resolves the problem.
Ericsson's AXD301 switch, built on Erlang/OTP supervision, achieved 99.9999999% uptime (31ms downtime per year). The supervision hierarchy allows the system to heal itself continuously—failed components restart while the rest continues operating.
The actor model naturally extends to distributed systems. Since actors communicate only through messages and share no state, an actor on another machine looks the same as a local actor.
Location Transparency:
Actor references (addresses) abstract away physical location. Sending a message to a remote actor uses the same syntax as sending to a local actor—the runtime handles serialization and network transport.
Erlang Distribution:
Erlang nodes can connect to form clusters. Any process can send messages to any process on any connected node:
1234567891011121314151617181920212223242526272829303132333435
%% Start Erlang nodes:%% Node 1: erl -name node1@192.168.1.10 -setcookie secret%% Node 2: erl -name node2@192.168.1.11 -setcookie secret %% On Node 1: Register a service-module(service).-export([start/0, handle/0]). start() -> Pid = spawn(fun handle/0), register(my_service, Pid). %% Register with name handle() -> receive {From, Message} -> io:format("Received: ~p from ~p~n", [Message, From]), From ! {reply, "Got it!"}, handle() end. %% On Node 2: Connect and send messageconnect_and_send() -> %% Connect to Node 1 net_adm:ping('node1@192.168.1.10'), %% Send message to registered process on remote node {my_service, 'node1@192.168.1.10'} ! {self(), "Hello from Node 2!"}, %% Wait for reply receive {reply, Response} -> io:format("Got response: ~p~n", [Response]) end. %% The message send syntax is IDENTICAL for local and remote!Akka Cluster:
Akka provides sophisticated clustering with automatic node discovery, sharding, and distributed data:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546
// Akka Cluster configuration (application.conf)akka { actor { provider = cluster } remote.artery { canonical.hostname = "127.0.0.1" canonical.port = 2551 } cluster { seed-nodes = [ "akka://ClusterSystem@127.0.0.1:2551", "akka://ClusterSystem@127.0.0.1:2552" ] }} // Scala code for cluster-aware actorimport akka.cluster.typed.{Cluster, Subscribe}import akka.cluster.ClusterEvent._ object ClusterListener { sealed trait Event case class MemberEventWrapper(event: MemberEvent) extends Event def apply(): Behavior[Event] = Behaviors.setup { context => // Subscribe to cluster membership events val cluster = Cluster(context.system) cluster.subscriptions ! Subscribe( context.messageAdapter(MemberEventWrapper), classOf[MemberEvent] ) Behaviors.receiveMessage { case MemberEventWrapper(MemberUp(member)) => context.log.info(s"Member up: ${member.address}") Behaviors.same case MemberEventWrapper(MemberRemoved(member, _)) => context.log.info(s"Member removed: ${member.address}") Behaviors.same case _ => Behaviors.same } }}While the actor model simplifies distribution, it doesn't eliminate distributed systems challenges. Network partitions, message loss, node failures, and ordering issues remain. The Eight Fallacies of Distributed Computing still apply. Actor systems provide tools to handle these, but you must design for failure.
Effective actor systems follow common patterns and practices.
Router/Load Balancer:
Distribute work across multiple worker actors for parallelism:
1234567891011121314151617181920212223242526272829
import akka.actor.typed.{ActorRef, Behavior}import akka.actor.typed.scaladsl.{Behaviors, Routers} object Worker { case class Task(data: String) def apply(): Behavior[Task] = Behaviors.receive { (ctx, task) => ctx.log.info(s"Processing: ${task.data}") Behaviors.same }} object WorkerPool { case class ProcessTask(data: String) def apply(): Behavior[ProcessTask] = Behaviors.setup { context => // Create a pool of 5 workers with round-robin routing val pool: ActorRef[Worker.Task] = context.spawn( Routers.pool(5)(Worker()), "worker-pool" ) Behaviors.receiveMessage { case ProcessTask(data) => pool ! Worker.Task(data) // Routed to next available worker Behaviors.same } }}The actor model provides a fundamentally different approach to concurrency—one that eliminates shared mutable state in favor of isolated actors communicating through messages. Let's consolidate the key insights:
Module Complete:
You have now explored the five major concurrency patterns that form the foundation of concurrent systems design:
These patterns appear throughout computing, from operating system kernels to distributed databases to web services. Recognizing which pattern fits your problem—and how to implement it correctly—is essential expertise for systems programming.
Congratulations! You have completed Module 6: Common Concurrency Patterns. You now possess deep understanding of fundamental patterns that power concurrent systems worldwide. These patterns will serve you throughout your career, whether you're building local multi-threaded applications or planet-scale distributed systems.