Actor Model
A Practical Guide to Concurrency, Locks, and Distributed Systems
How the Actor Model helps us build concurrent and distributed applications without fighting shared-state complexity.
Modern applications need to handle thousands or even millions of operations at the same time.
A web server processes multiple requests. A payment system handles transactions concurrently. A distributed service communicates with other services across different machines.
The challenge is not just executing tasks in parallel.
The real challenge is managing shared state, race conditions, synchronization, and failures.
This is where the Actor Model becomes useful.
In this article, we will explore:
- What is the Actor Model?
- How does it work?
- How is it different from traditional concurrency using locks?
- How can it help in distributed systems?
- When should you use it?
1. What Is the Actor Model?
The Actor Model is a mathematical model for designing concurrent and distributed systems.
Instead of thinking about threads directly, we think about independent entities called Actors.
An Actor has three important characteristics:
- It has its own private state.
- It communicates with other Actors through messages.
- It processes messages one at a time.
An Actor can receive a message and, as a result:
- Change its internal state.
- Send messages to other Actors.
- Create new Actors.
The key idea is simple:
Don’t share mutable state directly. Communicate through messages.
This makes it easier to reason about concurrent applications.
2. How Does an Actor Work?
Imagine a bank account.
The account has a balance:
Balance = 1000Two customers want to withdraw money at the same time.
Without proper synchronization, both requests might read the same balance.
Traditional shared-state approach
Thread A Thread B
| |
Read balance = 1000 Read balance = 1000
| |
Withdraw 700 Withdraw 500
| |
Write balance = 300 Write balance = 500The final balance could incorrectly become 500.
This is a classic race condition.
Actor-based approach
Instead of allowing multiple threads to directly modify the account balance, we create an Account Actor.
Customer A
|
| Withdraw(700)
v
+-------------------+
| Account Actor |
| |
| Balance = 1000 |
+-------------------+
^
|
| Withdraw(500)
|
Customer BThe Account Actor processes messages sequentially:
Message 1: Withdraw(700)
Balance: 1000 -> 300
Message 2: Withdraw(500)
Balance: 300 -> RejectedThere is no direct concurrent access to the balance.
The Actor owns the state.
3. Actor Model and Concurrency
Concurrency means multiple tasks are in progress during overlapping periods.
Parallelism means multiple tasks are executing at the same time, typically on multiple CPU cores.
The Actor Model supports concurrency by allowing many Actors to operate independently.
For example:
+------------------+
| User Actor |
+------------------+
|
v
+------------------+
| Order Actor |
+------------------+
|
v
+------------------+
| Payment Actor |
+------------------++------------------+
| Notification |
| Actor |
+------------------+Each Actor can process messages independently.
A runtime may execute different Actors on different threads or CPU cores.
Important distinction
Actors process their own messages sequentially, but many Actors can run concurrently.
This gives us a useful combination:
- Local state isolation.
- Concurrent execution.
- Message-based communication.
Example: Order Processing
Imagine an e-commerce application.
An order may go through these steps:
Order Created
|
v
Payment Processing
|
v
Inventory Reservation
|
v
Order ConfirmedEach part can be represented by an Actor.
The Order Actor receives:
CreateOrderThen it sends:
ReserveInventoryThe Inventory Actor handles inventory changes and sends back:
InventoryReservedThe Order Actor updates its state and continues.
This design separates responsibilities and avoids having every component directly modify shared data.
4. Actor Model vs Locks
Locks are one of the most common ways to protect shared mutable state.
For example, in Java:
class Counter {
private int value = 0;public synchronized void increment() {
value++;
} public synchronized int getValue() {
return value;
}
}
The synchronized keyword ensures that only one thread at a time can execute the synchronized method on the same object monitor.
This is a valid solution for many problems.
But as applications grow, lock-based systems can become complicated.
Problems with locks
1. Deadlocks
Two threads wait for each other forever.
Thread A holds Lock 1
Thread B holds Lock 2Thread A waits for Lock 2
Thread B waits for Lock 1Neither can continue.
2. Lock contention
Many threads compete for the same lock.
Thread A ----\
Thread B -----\
Thread C ------> Shared Lock
Thread D -----/Only one thread can enter the critical section at a time.
3. Complex synchronization
When multiple locks are involved, developers must carefully manage:
- Lock ordering.
- Lock acquisition.
- Lock release.
- Exception handling.
- Timeouts.
4. Shared mutable state
Many components may directly access the same object.
This increases the number of possible interactions that developers need to reason about.
5. How Actors Reduce the Need for Locks
The Actor Model uses a different approach.
Instead of sharing state between threads, an Actor owns its state.
Other components send messages to it.
+--------------------+
| Inventory Actor |
| |
| Stock = 100 |
+--------------------+
^
|
| Reserve(10)
|
+--------------------+
| Order Actor |
+--------------------+The Inventory Actor decides how to update its stock.
Other Actors do not directly modify the inventory state.
Lock-based approach
synchronized (inventory) {
if (inventory.stock >= quantity) {
inventory.stock -= quantity;
}
}Actor-based approach
InventoryActor.receive(Reserve(10))Conceptually:
if stock >= quantity:
stock -= quantity
reply Reserved
else:
reply RejectedThe Actor’s mailbox and processing model provide serialized access to its state.
Does this mean Actors never need locks?
No.
Actor implementations may use locks internally, depending on their runtime and execution model.
Also, an Actor system does not automatically eliminate every synchronization problem.
The important difference is that application-level state ownership and coordination happen through messages, rather than arbitrary direct access to shared mutable state.
6. Actor Model and Distributed Systems
This is where the Actor Model becomes especially interesting.
A distributed system consists of multiple computers or processes communicating over a network.
Examples include:
- Microservices.
- Payment systems.
- Multiplayer games.
- Messaging platforms.
- IoT platforms.
- Real-time trading systems.
- Large-scale cloud applications.
In a distributed system, shared memory is not generally available across machines.
Communication happens through messages.
That makes the Actor Model a natural fit.
Local Actor
Application Process
----------------------------+------------------+
| Actor A |
| Private State |
+------------------+
|
| Message
v
+------------------+
| Actor B |
| Private State |
+------------------+Distributed Actor
Machine A Machine B+------------------+ +------------------+
| Order Actor | | Payment Actor |
| | | |
| Order State | | Payment State |
+------------------+ +------------------+
| ^
| |
+------ Network ------------+
MessageThe message may travel over the network instead of staying inside one process.
This allows an Actor-based system to distribute work across machines.
7. A Practical Use Case: Distributed Bank Account
Let’s imagine a banking system with many requests.
Client 1 ----\
Client 2 -----\
Client 3 ------> Account Actor
Client 4 -----/The Account Actor owns the account balance.
Messages could be:
Deposit(100)
Withdraw(50)
GetBalanceThe Actor processes these messages sequentially.
Example
Initial balance:
1000Messages:
Deposit(500)
Withdraw(200)
Withdraw(300)Processing:
1000 + 500 = 1500
1500 - 200 = 1300
1300 - 300 = 1000The account state is updated by one owner.
Why is this useful?
It simplifies the logic around concurrent updates.
However, a production banking system still needs additional guarantees, such as:
- Durable storage.
- Transactional persistence.
- Idempotency.
- Recovery after failure.
- Correct handling of duplicate messages.
- Strong consistency where required.
An Actor alone does not provide all banking guarantees.
8. Actor Model and Mailboxes
Most Actor systems use a mailbox.
A mailbox is where incoming messages wait until the Actor processes them.
Message A
Message B
Message C
|
v
+------------------+
| Mailbox |
+------------------+
|
v
+------------------+
| Actor |
+------------------+Conceptually:
while running:
message = mailbox.receive()
handle(message)The Actor processes one message at a time.
Important mailbox details
Depending on the framework:
- Messages may be queued.
- Mailboxes may have bounded capacity.
- Messages may be processed in different orders.
- Mailbox overflow may need handling.
- Message delivery may not be guaranteed.
Do not assume every Actor framework provides the same delivery guarantees.
9. Actor Model and Message Ordering
Suppose we send two messages to an Actor:
Message A: Deposit(100)
Message B: Withdraw(50)If the messages are delivered in that order, the Actor can process them in that order.
But distributed systems introduce additional complexity.
Messages may be:
- Delayed.
- Duplicated.
- Lost.
- Delivered out of order.
A robust distributed Actor system must define its delivery and ordering guarantees.
For example, an application might use:
- Message IDs.
- Sequence numbers.
- Idempotent handlers.
- Persistent event logs.
- Retries with deduplication.
This is critical in real-world distributed systems.
Its main advantage is often simplifying state ownership and coordination.
11. Actor Model in Real-World Frameworks
Several technologies use Actor-based programming models.
Akka
Akka is a toolkit for building concurrent, distributed, and resilient applications on the JVM.
It provides Actor-based programming and other tools for building distributed systems.
Typical use cases include:
- Distributed services.
- Stateful processing.
- Event-driven applications.
- Clustered applications.
Erlang / Elixir
Erlang’s lightweight processes and message-passing model are closely associated with Actor-style concurrency.
Elixir builds on the Erlang VM and uses processes extensively.
Typical use cases include:
- Telecom systems.
- Messaging.
- Real-time applications.
- Fault-tolerant services.
Microsoft Orleans
Orleans provides a virtual Actor model using concepts called Grains.
A Grain represents a unit of state and behavior that can be activated where needed.
Typical use cases include:
- Distributed stateful applications.
- Gaming backends.
- Cloud services.
- Stateful workflows.
Important note
These frameworks differ in their APIs, scheduling, persistence, delivery guarantees, and failure-handling mechanisms.
The underlying Actor principles are similar, but the production behavior depends on the specific framework.
12. When Should You Use the Actor Model?
The Actor Model is useful when your application has many independent stateful entities.
Examples:
1. Multiplayer Games
Each player can be represented by an Actor.
Player Actor 1
Player Actor 2
Player Actor 3Each Actor owns player-specific state.
Messages represent actions:
Move
Attack
CollectItem
JoinGameThis can make game state management easier.
2. IoT Systems
Each device can be represented by an Actor.
Device Actor
|
+-- Temperature
+-- Battery
+-- ConnectionThe system can process device events independently.
3. E-commerce
Actors can represent:
- Orders.
- Shopping carts.
- Inventory items.
- Customer accounts.
This is useful when each entity has its own state and behavior.
4. Real-Time Messaging
Actors can represent:
- Users.
- Chat rooms.
- Connections.
- Sessions.
Messages can be used to coordinate communication.
5. Distributed Workflows
Actors can represent workflow instances.
For example:
Order Created
|
v
Payment Pending
|
v
Payment Completed
|
v
Order ShippedEach workflow instance can maintain its own state.
13. When Should You Not Use Actors?
The Actor Model is not the best solution for every problem.
You may not need Actors when:
Simple synchronous code is enough
If your application performs straightforward operations, adding an Actor runtime may introduce unnecessary complexity.
You need simple shared-memory operations
For example, a small counter may be perfectly handled with an atomic integer.
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();You need database transactions
Actors do not replace database transactions.
If multiple records must be updated atomically, you may still need a database transaction.
You need high-throughput data processing
Depending on the workload, a lock-free data structure, batching, or a specialized stream-processing system may be a better fit.
Always evaluate the actual workload.
14. Actor Model Is Not a Magic Solution
The Actor Model solves one important class of problems:
How can we organize concurrent state and communication?
But it does not automatically solve:
- Network failures.
- Duplicate messages.
- Data persistence.
- Distributed transactions.
- Exactly-once processing.
- Global consistency.
- Resource exhaustion.
For example, if an Actor sends a payment request and the network fails, what should happen?
Get Kiamars Mirzaee’s stories in your inbox
Join Medium for free to get updates from this writer.
The system may need:
PaymentRequested
|
v
Retry
|
v
PaymentConfirmedBut retrying can create duplicate payments if the receiving service does not support idempotency.
So production Actor systems require careful design.
15. Final Thoughts
The Actor Model is a powerful way to think about concurrency and distributed systems.
Its central principle is:
Encapsulate state inside Actors and communicate through messages.
Compared with traditional shared-state concurrency, it can make complex systems easier to design by giving each Actor clear ownership of its state.
It is especially useful for:
- Stateful concurrent applications.
- Distributed services.
- Event-driven systems.
- Real-time applications.
- Systems with many independent entities.
However, Actors are not a replacement for every synchronization mechanism.
Locks, atomics, database transactions, and message passing all have their place.
The right choice depends on your application’s requirements.
The best concurrency model is the one that makes your system correct, understandable, and maintainable.
Actor Model vs Traditional Concurrency: Advantages, Disadvantages, and Use Cases
When developers work with concurrency, one of the most common problems is a race condition.
Two threads access the same data at the same time, and the final result depends on the order of execution.
Traditional concurrency tools such as locks, mutexes, semaphores, and atomic operations help solve this problem.
But there is another approach: the Actor Model.
Instead of protecting shared state with locks, the Actor Model organizes state ownership and communication around independent Actors.
Let’s compare both approaches and understand when each one is useful.
1. The Traditional Way: Shared State + Locks
Imagine a counter shared by multiple threads.
class Counter {
private int count = 0
public synchronized void increment() {
count++;
public synchronized int getCount() {
return count;
}
}Multiple threads can call increment().
The synchronized keyword ensures that only one thread at a time executes the synchronized method on the same object monitor.
How it works
Thread A ----\
Thread B -----\
Thread C ------> Lock ---> Counter
Thread D -----/Only one thread can enter the protected section at a time.
This prevents the classic race condition:
count = 0
Thread A reads count = 0
Thread B reads count = 0
Thread A writes count = 1
Thread B writes count = 1
Final count = 1
Expected count = 2The lock prevents these conflicting updates from happening simultaneously.
2. The Actor Model Approach
The Actor Model uses a different design.
Instead of allowing many threads to modify the same state, an Actor owns its state and receives messages.
+-----------------------+
| Counter Actor |
| |
| count = 0 |
+-----------------------+
^
|
Increment Message
^
|
Other ActorsThe Actor processes messages sequentially.
Conceptually:
CounterActor.receive(Increment)
count = count + 1If ten messages arrive:
Increment
Increment
Increment
Increment
Increment
Increment
Increment
Increment
Increment
IncrementThe Actor processes each message according to its mailbox and scheduling rules.
The state is not directly modified by external threads.
Main principle
Traditional concurrency protects shared state. Actor-based concurrency organizes state so that it is owned by one Actor.
Advantages of the Actor Model
Advantage 1: Reduces Shared-State Race Conditions
This is one of the biggest benefits.
In traditional concurrency:
Thread A ---> Shared Counter <--- Thread BDevelopers must protect the counter with locks or atomic operations.
In the Actor Model:
Thread A ---> Counter Actor <--- Thread BThe Actor owns the counter.
External components send messages rather than modifying the counter directly.
This reduces the risk of accidental unsynchronized access to that state.
Important: This protection applies to state managed by the Actor. Shared state outside the Actor can still have race conditions.
Advantage 2: Less Lock Management
Traditional code often requires:
lock.lock();
try {
// Critical section
} finally {
lock.unlock();
}Developers must reason about:
- When to acquire a lock.
- When to release it.
- Whether the lock order is correct.
- What happens if an exception occurs.
- Whether a thread can wait forever.
With Actors, the application typically sends a message:
AccountActor.receive(Withdraw(100))The Actor handles the operation while owning its state.
This can reduce explicit lock-management complexity.
However, the Actor runtime may use internal synchronization, and Actor applications can still have coordination problems.
Advantage 3: Avoids Many Common Deadlock Patterns
Traditional deadlock:
Thread A:
Lock A
Wait for Lock B
Thread B:
Lock B
Wait for Lock ABoth threads wait forever.
In an Actor-based application, state is not normally protected by developers acquiring multiple locks in arbitrary order.
This removes many classic lock-order deadlocks.
But Actors do not eliminate all deadlocks.
For example:
Actor A waits for response from Actor B
Actor B waits for response from Actor AIf neither can proceed, the application can still become stuck.
So the more accurate statement is:
The Actor Model reduces lock-based deadlock risks, but does not guarantee deadlock-free systems.
Advantage 4: Natural Fit for Distributed Systems
Traditional shared-memory concurrency assumes that threads can access the same process memory.
Distributed systems work across multiple machines.
Machine A Machine B+-------------+ +-------------+
| Order | | Payment |
| Service | ---------> | Service |
+-------------+ Message +-------------+Actors communicate through messages, making the model conceptually similar to distributed communication.
This is useful for:
- Microservices.
- Distributed workflows.
- Real-time systems.
- Stateful cloud applications.
- Multiplayer games.
- Messaging systems.
However, a local Actor message and a network message are not equivalent.
Network communication introduces failures, delays, duplicates, and ordering concerns.
Advantage 5: Easier State Ownership
Consider an application with multiple entities:
User 1
User 2
User 3
Order 1
Order 2
Order 3Each entity can be represented by an Actor.
UserActor(1)
UserActor(2)
UserActor(3)OrderActor(1)
OrderActor(2)
OrderActor(3)Each Actor owns its own state.
This makes it easier to answer:
Who is allowed to modify this state?
In large applications, clear state ownership can be more valuable than simply adding more locks.
Advantage 6: Supports Event-Driven Architecture
Actors work naturally with event-driven systems.
For example:
OrderCreated
|
v
Order Actor
|
v
PaymentRequested
|
v
Payment Actor
|
v
PaymentCompletedThe application reacts to events and messages rather than requiring every component to call every other component directly.
This can improve separation of responsibilities.
4. Disadvantages of the Actor Model
The Actor Model is not always better.
It introduces its own costs and challenges.
Disadvantage 1: Message-Passing Overhead
Traditional method call:
counter.increment();Actor-style message:
counterActor.send(Increment)The message may involve:
- Allocating a message object.
- Enqueuing it in a mailbox.
- Scheduling the Actor.
- Processing the message.
Depending on the framework, this can be more expensive than a simple direct method call or atomic increment.
For a very small operation, an AtomicInteger may be a simpler and faster solution.
AtomicInteger counter = new AtomicInteger();
counter.incrementAndGet();Use Actors when their coordination and state-ownership benefits justify the overhead.
Disadvantage 2: More Complex Debugging
Traditional code often has a direct call stack:
Controller
|
v
Service
|
v
RepositoryActor systems are asynchronous.
Actor A
|
| Message
v
Mailbox
|
v
Actor BThe sender may finish before the receiver processes the message.
Debugging can become harder because:
- The error may occur later.
- Messages may be processed in unexpected orders.
- Multiple Actors may be active at the same time.
- The original call stack may not explain the full workflow.
Good logging, tracing, and message correlation IDs become important.
Disadvantage 3: Message Ordering Problems
In a simple local system, messages may be processed in a predictable order.
In distributed systems, messages can be delayed or duplicated.
Example:
Message 1: Deposit(100)
Message 2: Withdraw(50)If the messages are delivered in the wrong order, the result may differ.
This is especially important for:
- Banking.
- Inventory.
- Payments.
- Distributed workflows.
Solutions may include:
- Sequence numbers.
- Idempotency keys.
- Durable event logs.
- Retry handling.
- Explicit consistency protocols.
The Actor Model alone does not solve distributed ordering.
Disadvantage 4: Distributed State Is Still Difficult
Suppose we have two Actors:
Account A: 100
Account B: 100We want to transfer 50 from A to B.
A distributed transfer involves multiple state changes:
Account A: -50
Account B: +50What happens if the first operation succeeds but the second fails?
Account A: 50
Account B: 100The money has effectively disappeared from the expected combined balance.
This is a distributed transaction problem.
The Actor Model does not automatically provide atomic updates across multiple Actors.
Possible solutions include:
- Saga patterns.
- Transactional databases.
- Durable workflows.
- Two-phase commit where appropriate
- Compensation logic.
Disadvantage 5: Mailbox and Backpressure Problems
An Actor may receive messages faster than it can process them.
Incoming messages
|
v
+------------------+
| Mailbox |
| |
| 1000 messages |
| 2000 messages |
| 3000 messages |
+------------------+
|
v
Actor processes slowlyIf messages keep arriving, the mailbox can grow.
This can cause:
- Increased memory usage.
- Higher latency.
- Timeouts.
- System overload.
Production systems need to consider:
- Bounded mailboxes.
- Backpressure.
- Rate limiting.
- Message prioritization.
- Load shedding.
Disadvantage 6: Not Every Problem Fits the Actor Model
Some problems are naturally solved using shared-memory tools.
For example:
AtomicInteger counter = new AtomicInteger(0);
counter.incrementAndGet();Creating an entire Actor system just to increment a number may be unnecessary.
Likewise, a database transaction is often the right tool for updating several database records atomically.
The Actor Model should be selected based on the problem, not simply because it is a modern concurrency technique.
5. Actor Model vs Locks: Detailed Comparison
6. Which Method Should You Use?
There is no single best concurrency model for every application.
The right choice depends on the type of problem.
Use Locks or Mutexes When
You have:
- Shared mutable memory.
- A small critical section.
- A simple synchronization requirement.
- A need for direct synchronous access.
Example:
synchronized (sharedResource) {
sharedResource.update();
}This is often straightforward for small, well-defined critical sections.
Use Atomic Operations When
You need simple thread-safe operations on individual values.
Example:
AtomicInteger count = new AtomicInteger();
count.incrementAndGet();Atomic operations are useful for counters, flags, and other operations supported by the atomic type.
Use the Actor Model When
Your system has:
- Many independent stateful entities.
- Event-driven communication.
- Concurrent workflows.
- Distributed services.
- Stateful sessions or devices.
- A need for clear ownership of mutable state.
Example:
Order Actor
Payment Actor
Inventory Actor
Notification ActorEach Actor can own a piece of application state and communicate with others using messages.
Use Database Transactions When
You need multiple database changes to succeed or fail together.
For example:
Transfer Money
|
+-- Debit Account A
|
+-- Credit Account BIf both changes must be atomic, use a database transaction or an appropriate distributed transaction design.An Actor does not replace ACID transactions.
7. Can Actors and Locks Be Used Together?
Yes.
In real systems, these techniques are not mutually exclusive.
A system might use:
Actor Model
|
+-- Message-based state ownership
|
+-- Database transactions
|
+-- Atomic counters
|
+-- Internal locks
|
+-- Distributed coordinationFor example, an Actor may own an order’s state while using a database transaction to persist an update.
This is often a practical approach.
The important principle is to keep responsibilities clear.
8. A Practical Example: Inventory Reservation
Imagine an online store with 100 units of a product.
Two customers simultaneously try to purchase 80 units each.
Traditional approach
Thread A Thread B
| |
Read stock = 100 Read stock = 100
| |
Reserve 80 Reserve 80
| |
Write stock = 20 Write stock = 20Without correct synchronization, both orders might be accepted.
This is an overselling race condition.
A lock or database transaction can protect the inventory update.
Actor approach
Order A ----\
Order B -----\
Order C ------> Inventory ActorThe Inventory Actor processes reservations sequentially:
Initial stock = 100Reserve 80
Stock = 20Reserve 80
Rejected
This can simplify in-memory inventory coordination.
Production consideration
If inventory is shared across multiple application instances, the Actor system must ensure that the inventory state has a single authoritative owner or use a suitable distributed consistency mechanism.
A local Actor on each server does not automatically protect shared inventory across all servers.
9. The Most Important Difference
The biggest difference is not simply:
Actors are faster than locks.
Or:
Actors are always safer than threads.
The real difference is how state is organized.
Traditional concurrency
Shared State
|
v
Locks / Mutexes / Atomics
|
v
ThreadsActor-based concurrency
Actor
|
+-- Owns State
|
+-- Receives Messages
|
+-- Processes Messages
|
+-- Sends MessagesTraditional concurrency focuses on synchronizing access to shared memory.
The Actor Model focuses on independent state ownership and communication.
Both can be useful.
Conclusion
The Actor Model is a valuable approach for building concurrent and distributed systems.
It can reduce shared-state complexity, make state ownership explicit, and provide a natural design for message-driven applications.
However, it also introduces message-passing overhead, asynchronous debugging challenges, mailbox management, and distributed consistency problems.
Traditional methods such as locks, mutexes, semaphores, and atomic operations remain essential tools.
The practical rule
Use locks for simple shared-memory synchronization.
Use atomics for simple thread-safe operations.
Use Actors for stateful, message-driven concurrency and distributed applications.
Use database transactions when you need atomic database updates.
The goal is not to replace traditional concurrency with Actors.
The goal is to choose the right tool for the right problem.






