Definition
- Actor – an object with private mutable state, a mailbox, and a message handler.
- Private state – no other thread or actor can read or write it; the only interface is messages.
- Immutable messages – data sent between actors must be immutable so sending is safe across threads.
- Mailbox – per-actor queue of pending messages; the actor processes one at a time, in order.
- Akka – JVM implementation scheduling millions of lightweight actors over a small thread pool.
- Trade-offs – message latency, eventual ordering, unbounded mailboxes, harder debugging.
Imagine each account at National Bank of India has its own dedicated clerk who alone can touch that account's ledger. If you want something done, you drop a written request into the clerk's in-tray (mailbox). The clerk handles one request at a time. Since only one clerk touches each ledger, two people can never corrupt the balance.
Understanding the Concepts
What are the three pillars of the Actor model?
The three pillars are private state, immutable messages, and the mailbox. Together they guarantee that shared mutable state never exists, so locks, data races, and deadlocks are impossible by design.
How does private state eliminate data races?
The actor's data is a serial island. Only the actor's own execution thread ever reads or writes its fields. There is never concurrent access. No thread outside the actor can call a setter, read a field, or reference the internal state in any way.
Why must messages be immutable?
When actor A sends a message to actor B, the message crosses a thread boundary. If the message were mutable, actor A could mutate something actor B is about to read, reintroducing a data race. Java records are ideal for actor messages because they are immutable by default.
package com.bank.actor;
import java.math.BigDecimal;
// Immutable messages using Java records
public record Deposit(String accountId, BigDecimal amount) {}
public record Withdraw(String accountId, BigDecimal amount) {}
public record BalanceQuery(String accountId) {}
public record TransferRequest(String fromAccount, String toAccount, BigDecimal amount) {}
How does the mailbox work?
Sending is asynchronous: the sender enqueues the message and continues without blocking. The actor drains the mailbox one message at a time in order. Because only one message is processed at a time, internal mutations are automatically safe.
How does Akka schedule actors?
Akka assigns a thread to an actor only when it has messages to process. After handling a configurable batch of messages, the thread is freed and can service another actor. This is why millions of actors can run over a handful of threads, similar to how virtual threads multiplex over carrier threads.
How would an account actor look in a banking system?
package com.bank.actor;
import java.math.BigDecimal;
import java.util.ArrayDeque;
import java.util.Queue;
public class AccountActor {
private final String accountId;
private BigDecimal balance;
private final Queue<Object> mailbox = new ArrayDeque<>();
public AccountActor(String accountId, BigDecimal initialBalance) {
this.accountId = accountId;
this.balance = initialBalance;
}
// Called by the scheduler, one message at a time
public void onMessage(Object message) {
if (message instanceof Deposit d) {
balance = balance.add(d.amount());
System.out.println(accountId + " deposited " + d.amount() + ", balance: " + balance);
} else if (message instanceof Withdraw w) {
if (balance.compareTo(w.amount()) >= 0) {
balance = balance.subtract(w.amount());
System.out.println(accountId + " withdrew " + w.amount() + ", balance: " + balance);
} else {
System.out.println(accountId + " insufficient funds for " + w.amount());
}
}
}
}
No synchronized blocks, no locks, no AtomicReference. The mailbox serializes all access to balance.
How do actors compare to locks?
| Aspect | Locks | Actors |
|--------|-------|--------|
| Shared state | Yes, guarded by locks | No, state is private |
| Data races | Possible if lock is wrong | Impossible by design |
| Deadlocks | Possible with inconsistent ordering | Impossible (no locks) |
| Scalability | Limited by contention | Millions of actors |
| Debugging | Stack traces, lock analysis | Async message flows, harder |
| Latency | Direct method call | Message-passing overhead |
| Simple critical section | Clean and obvious | Over-engineering |
What is supervision in Akka?
A parent actor supervises its children. If a child fails (throws an exception during message processing), the parent decides: restart it (reset state, keep mailbox), stop it (remove permanently), or escalate to its own parent. This builds resilient systems without try-catch everywhere.
What are the trade-offs of the Actor model?
Message-passing adds latency compared to a direct method call. Eventual ordering means you cannot assume messages from different senders arrive in a global order. Unbounded mailboxes can grow until OutOfMemoryError, so you need backpressure (Akka Streams, reactive pull). Debugging is harder because there are no stack traces across actors, only async message flows. Distributed tracing (Zipkin, OpenTelemetry) is essential.
What are common mistakes?
Using mutable messages breaks the safety guarantee entirely.
// WRONG: mutable message shared between actors
List<String> trades = new ArrayList<>();
trades.add("TRADE-1");
accountActor.tell(new ProcessTrades(trades)); // sent
trades.add("TRADE-2"); // sender mutates after sending! Data race!
// CORRECT: immutable message with defensive copy
accountActor.tell(new ProcessTrades(List.copyOf(trades)));
Another mistake is blocking inside an actor's message handler (JDBC call, Thread.sleep). This ties up the dispatcher thread, starving other actors. Move blocking work to a separate dispatcher configured for IO.
What are common interview follow-up questions?
- How do you handle request-response patterns in the Actor model? Use the ask pattern with a timeout.
- How does backpressure work with actor mailboxes? Use bounded mailboxes or Akka Streams.
- How do actors compare to virtual threads? Virtual threads are cheaper for IO concurrency but do not enforce private state or message-passing discipline.
Visual Overview
Actor Model:
┌──────────────┐ message ┌──────────────┐
│ Sender │──────────▶ │ Mailbox │
│ (async, fire │ │ [msg1] │
│ and forget) │ │ [msg2] │
└──────────────┘ │ [msg3] │
└──────┬───────┘
│
▼
┌──────────────┐
│ Actor │
│ Private │
│ State: │
│ balance=50K │
│ │
│ Processes │
│ one msg at │
│ a time │
└──────────────┘
No locks needed: only one thread touches the state.
No data races: state is private.
No deadlocks: no locks to order incorrectly.
Key Takeaway
The Actor model gives each actor private state, asynchronous immutable messages, and a mailbox processed one at a time. This eliminates shared mutable state, so there are no locks, no data races, and no deadlocks by design. Akka schedules millions of actors over a small thread pool.
The trade-offs are real: message latency, eventual ordering, unbounded mailboxes need backpressure, and async flows are harder to debug. For a short critical section, a lock can be cleaner.
At National Bank of India we modeled each account as an actor so deposits and withdrawals from many channels are serialized by the mailbox, removing locks entirely.
Remember one sentence: an actor owns private state and processes immutable messages one at a time, eliminating shared mutable state so locks, data races, and deadlocks disappear by design.
---