Levelwise
English
Actor Model

Akka.NET

The Akka.NET library brings the classic Actor model to .NET. You create the Actors yourself, in a tree of parents and children. Each parent supervises its children, and when one of them fails, it decides what to do with it.

Not reviewedWritten with AI helpReading time: 13 minOnline shop warehouse exampleC# code with .NET 10

Author: bezzad

The problem: the warehouse on a sale day

On a sale day, thousands of reservation requests reach the shop’s warehouse in one second. Each product has its own stock. We have two problems:

  1. Concurrency. Two requests must not reserve the last item at the same time.
  2. Failure. If the code for one product fails (for example, because of bad data), the whole warehouse must not stop working.

In the Actor model lesson, we saw that one Actor for each product solves the first problem. Akka.NET also solves the second problem with a simple idea: each Actor has a parent that looks after it.

The main idea: a tree of Actors

In Akka.NET, each Actor is created by another Actor. So all Actors form a tree.

/userStock manager/user/stockHeadphones actor/user/stock/p-7Phone actor/user/stock/p-2Book actor/user/stock/p-9The parent supervises the children
The stock manager creates one child for each product. Each Actor has an address like a file path.
  1. The Actor system (ActorSystem) is the root of the tree. Usually we have one in each app.
  2. Each Actor creates its own children. The stock manager creates the Actor for each product.
  3. Each Actor has an address. Other code has only a reference (IActorRef), not the object itself. So it cannot touch its state.
  4. Each parent supervises its children. This is the most important idea in Akka.

Code: a simple Actor

Messages are immutable records:

public sealed record Reserve(string OrderId, string ProductId, int Quantity);
public sealed record Reserved(string OrderId);
public sealed record OutOfStock(string OrderId);

The Actor for each product. With Receive, we say what to do for each message type:

using Akka.Actor;

public sealed class ProductStockActor : ReceiveActor
{
    private int _available;

    public ProductStockActor(string productId, IStockRepository stock)
    {
        _available = stock.GetAvailable(productId); // runs again after a restart
        Receive<Reserve>(msg =>
        {
            if (msg.Quantity > _available)
            {
                Sender.Tell(new OutOfStock(msg.OrderId));
                return;
            }

            _available -= msg.Quantity;
            Sender.Tell(new Reserved(msg.OrderId));
            if (_available == 0) Become(SoldOut); // change behavior for next messages
        });
    }

    private void SoldOut() =>
        Receive<Reserve>(msg => Sender.Tell(new OutOfStock(msg.OrderId)));
}

Two points in this code:

  • The Sender property is the Actor that sent the current message. We send the reply to it.
  • The Become method changes the Actor’s behavior for the next messages. After the stock runs out, the Actor does not even check the number any more.

The stock manager creates the child for each product the first time, and gives the message to it:

public sealed class StockManagerActor : ReceiveActor
{
    public StockManagerActor(IStockRepository stock)
    {
        Receive<Reserve>(msg =>
        {
            var child = Context.Child(msg.ProductId);
            if (child.Equals(ActorRefs.Nobody))
                child = Context.ActorOf(
                    Props.Create(() => new ProductStockActor(msg.ProductId, stock)),
                    msg.ProductId);

            child.Forward(msg); // keep the original sender, so the child replies to it
        });
    }
}

And using it from outside, for example in an API:

var system = ActorSystem.Create("shop");
var manager = system.ActorOf(Props.Create(() => new StockManagerActor(repository)), "stock");

// Tell: fire and forget.
manager.Tell(new Reserve("o-1", "p-7", 1));

// Ask: wait for one reply, with a timeout.
var reply = await manager.Ask<object>(new Reserve("o-2", "p-7", 1), TimeSpan.FromSeconds(3));
Tell or Ask? With Tell, you send a message and do not wait. This is the main way in Akka. Ask is for the border between normal code and Actors, for example inside an API. Between two Actors, use Tell as much as you can.

Supervision: let it crash

The Akka idea comes from Erlang: instead of try/catch everywhere, let the Actor crash and let its parent decide.

Headphones actorfailedfailure noticeStock managerdecidesResumeContinuestate is keptRestartCreate againmemory state is clearedStopStop itfor goodEscalatePass to my parentI do not know what to doThe default for most errors is to create it again
The child does not fix the error itself. It only reports it, and the parent decides.
  1. The child fails. Its current message is left half done.
  2. Its mailbox is paused, and the parent gets the failure notice.
  3. The parent makes one of four decisions. Continue, create again, stop, or pass it to the parent above.
  4. The rest of the warehouse keeps working. A failure in the headphones Actor has no effect on the phone Actor.
public sealed class StockManagerActor : ReceiveActor
{
    // ... the same constructor as before ...

    protected override SupervisorStrategy SupervisorStrategy() =>
        new OneForOneStrategy(
            maxNrOfRetries: 3,
            withinTimeRange: TimeSpan.FromMinutes(1),
            localOnlyDecider: ex => ex switch
            {
                InvalidDataException => Directive.Restart,  // reload clean state
                TimeoutException => Directive.Resume,        // state is still fine
                _ => Directive.Escalate
            });
}

The OneForOne strategy means the decision is applied only to the child that failed. Another strategy, named AllForOne, applies the decision to all the children.

Creating again clears the memory state. After a Restart, the Actor’s constructor runs again, and the state in memory is lost. So important state must be saved somewhere. In our example, the constructor reads the stock from the database. So after a Restart, the right number comes back. To save an Actor’s events, there is also the Akka.Persistence package.

Important rules

  1. Message order is guaranteed only between two specific Actors. If A sends two messages to B, B gets them in the same order. But there is no order between messages from A and from C.
  2. Message delivery is “at most once” by default. If a message is lost between two servers, Akka does not send it again by itself. For important work, ask for a reply and set a Timeout.
  3. Do not wait for long work inside an Actor. If you have a long await, the mailbox waits behind it. Start the work, and send the result back to the Actor itself as a new message with PipeTo.
  4. Do not give out the state. Send only immutable messages.

Akka.NET or Orleans?

Both are Actor models, but they have different philosophies:

Akka.NETCreateSend messageStopYou control the lifecycleHas a parent, supervisor and exact addressOrleansSend messageCreating and removingis automaticOnly an ID, no parent
In Orleans you think less. In Akka.NET you have more control.
Akka.NET Orleans
Creating Actors You create and stop them yourself. Automatic, with the first message.
Structure A tree of parents and children. No tree. Only an ID.
Error handling The parent, with a supervision strategy. The error goes back to the caller, like a normal method.
Communication Messages with Tell, and Ask when needed. Calling an async method on an interface.
Spreading over several servers The Cluster and Cluster Sharding packages. Built in and on by default.
Learning curve Steeper, more concepts. Gentler for a .NET developer.
Good for Complex message flows, precise control over lifecycle and errors. A large number of independent entities, like a cart or a player.
A simple rule: If you think more in terms of “each entity is an object that is always available”, Orleans is simpler. If you think in terms of “a network of Actors that work together with messages and handle errors”, Akka.NET fits better.

Common mistakes

Mistake Result Right way
Using Ask between all Actors Actors wait for each other, and Timeouts grow. Communicate with Tell, and reply as a message.
try/catch for all errors inside the Actor The real error is hidden, and the state stays broken. Leave the error to the parent and write a supervision strategy.
Thinking the state stays after a Restart The stock becomes zero or wrong. Save important state and read it again.
Sending a mutable object in a message Two Actors touch the same data. Only immutable records.
One Actor for all products It becomes a bottleneck. One Actor for each product.

Summary in six lines

  1. In Akka.NET you create the Actors yourself, and they form a tree.
  2. Each Actor defines its behavior for each message type with Receive, and changes its behavior with Become.
  3. The main communication is with Tell. Ask is for the border with normal code.
  4. Each parent supervises its children: continue, create again, stop, or pass it up.
  5. After a Restart, the memory state is cleared. Save important state.
  6. The Orleans library is simpler and more automatic. The Akka.NET library gives more control.