Best Practices for Designing Akka.NET Domain Events and Commands
How to Make Akka.NET Programming Easier by Designing Events Well
In this blog post we’re going to cover some best practices you can use when designing domain events and objects intended to work with Akka.NET. If you follow these best practices you’ll run into fewer errors, clearer log messages, and a better extensibility experience.
Use Marker / Identity Interfaces Generously
This first tip is designed to make it easier to extend your messaging systems without having to manually update the Receive<T>
statements on a large number of actors.
Suppose I’m working the code from our new Akka.Cluster Workshop - in this application we have a large number of domain events for the purpose of trading stocks:
Bid
- offer to buy N units of stock at a specific price point;Ask
- offer to sell N units of stock at a specific price point;Match
- the stock trading system has matched anAsk
order with aBid
; andFill
- some amount of aBid
orAsk
order has been filled by aMatch
.
All of these events have several common identifiers and properties that can be really useful for routing, sharding, or distributing these messages:
- The stock ticker symbol (MSFT, TEAM, AMD, etc…);
- The id of the order; and
- They all represent live trading events happening as a result of trader activity - this is distinct from an event emitted by the exchange indicating what the newest “market price” for a specific ticker symbol is.
Well, in order to make my system more extensible and easier to debug I’m going to introduce some common marker interfaces - for instance, an IWithStockId
interface:
/// <summary>
/// Marker interface used for routing messages for specific stock IDs
/// </summary>
public interface IWithStockId
{
/// <summary>
/// The ticker symbol for a specific stock.
/// </summary>
string StockId { get; }
}
All events and commands that pertain to a specific stock ticker symbol should be marked with this interface. This allows us to design routers, actors, and so on to handle the marker interface itself, rather than every message that implements it.
For example:
/// <summary>
/// Child-per entity parent for order books.
/// </summary>
public sealed class OrderBookMasterActor : ReceiveActor
{
public OrderBookMasterActor()
{
Receive<IWithStockId>(s =>
{
var orderbookActor = Context.Child(s.StockId).GetOrElse(() => StartChild(s.StockId));
orderbookActor.Forward(s);
});
}
private IActorRef StartChild(string stockTickerSymbol)
{
var pub = new InMemoryTradeEventPublisher();
return Context.ActorOf(Props.Create(() => new OrderBookActor(stockTickerSymbol, pub)), stockTickerSymbol);
}
}
If we need to add new domain events in the future that pertain to specific stock ticker symbols, such as a Cancel
event to cancel an un-filled order, we could introduce this new type and have it implement IWithStockId
. Any of the actors involved in routing IWithStockId
messages don’t have to be updated in order to ensure these events end up in the right place - the actor who actually does the concrete processing.
In a real event inside one of Petabridge’s applications, we might have multiple marker interfaces on our events:
public sealed class Bid : IWithStockId, IWithOrderId
{
public Bid(string stockId, string orderId, decimal bidPrice,
double bidQuantity, DateTimeOffset timeIssued)
{
StockId = stockId;
BidPrice = bidPrice;
BidQuantity = bidQuantity;
TimeIssued = timeIssued;
OrderId = orderId;
}
public string StockId { get; }
public decimal BidPrice { get; }
public double BidQuantity { get; }
public DateTimeOffset TimeIssued { get; }
public string OrderId { get; }
}
IWithStockId
allows us to route messages based on the ticker symbol, but IWithOrderId
allows us to route messages based on the ID of the trade order being placed. This event is related to both domain concerns (stocks and orders) and thus it makes perfect sense to mark it with both interfaces.
Always Make Your Domain Messages Immutable
In all of our posts and presentations where we introduce the actor model, such as “Akka.NET: What is an Actor?” we mention very quickly that all of the messages sent between actors are supposed to be immutable.
This is because in the event that you’re sending the same message to many actors running locally inside the same process - all of those actors receive a reference to the same copy of the message. In the event that you made this message type mutable, if one actor modifies any of the properties or fields on this message, those changes will be propagated to all of the other actors processing the same message - this is what’s known as a “side effect” in concurrent programming.
This is undesirable and works against the inherent thread-safety built into Akka.NET actors - therefore, what we really want is to ensure that our messages are always immutable.
If an actor modifies an immutable message there are no side effects. This preserves the automatic thread safety provided by Akka.NET actors.
Therefore, we need to ensure that all of our domain messages sent between actors are immutable by default. Here’s how to do that:
- Make all properties and fields read-only - the only way the values can be set should be through the message’s constructor;
- Never expose a normal collection, i.e.
List<T>
orDictionary<K,V>
, as a property on a message class - always exposeSystem.Collections.Generic
collections asIReadOnlyCollection<T>
,IReadOnlyList<T>
,IReadOnlyDictionary<K,V>
, and so on; - Ensure that the objects passed into the message’s constructor are immutable themselves.
Here’s a better example for item number three:
public class MyMsg{
public MyMsg(IReadOnlyList<int> items){
Items = items;
}
public IReadOnlyList<int> Items {get;}
}
public class MyActor : ReceiveActor{
private readonly List<int> _myItems = new List<int>();
public MyActor(){
Receive<int>(i => {
_myItems.Add(i);
});
Receive<GetItems>(_ => {
Sender.Tell(new MyMsg(_myItems));
});
}
}
When we create the new MyMsg
instance via new MyMsg(_myItems)
, even though MyMsg.Items
is a read-only collection and a read-only property, the underlying collection we’re passing into the message can still be modified by the MyActor
instance. Therefore, this message is still not safe.
To fix this issue, we should create a new copy of the List<int>
before we copy it into MyMsg
:
Receive<GetItems>(_ => {
Sender.Tell(new MyMsg(_myItems.ToArray())); // copy list into new array
});
Now with that change we can guarantee that all of the MyMsg
instances sent by any MyActor
are immutable.
Make it Easy to Copy Your Domain Entities into Immutable Messages
Per item 3 in our “how to make your messages immutable” checklist, ensuring that the data your actor shares with other actors via immutable messages is also immutable itself is a good practice.
For performance reasons it’s often desirable to have an actor’s private state designed as a mutable class. For instance, the MatchingEngine
from our Akka.CQRS sample is responsible for matching Bid
and Ask
orders - it’d be memory-expensive if we had to continuously copy the entire order book state every time we had to process a new trade order, hence why it’s designed to be mutable.
However, there’s going to be situations where it’s necessary for the OrderBookActor
, who houses a MatchingEngine
instance for a specific stock ticker, to share a copy of its order book state with other actors. In order for us to do this safely we must design the MatchingEngine
to copy its internal state into an immutable point-in-time snapshot.
/// <summary>
/// Creates a point-in-time snapshot of the current order book data.
/// </summary>
/// <returns>Returns a new <see cref="OrderbookSnapshot"/> instance.</returns>
public OrderbookSnapshot GetSnapshot()
{
var snapshot = new OrderbookSnapshot(StockId, _timestamper.Now, AskTrades.Values.Sum(x => x.RemainingQuantity),
BidTrades.Values.Sum(x => x.RemainingQuantity),
AsksByPrice.ToList(), BidsByPrice.ToList());
return snapshot;
}
That’s exactly what we do in the MatchingEngine.GetSnapshot()
method - we copy all of the internal state into new collections and return it inside an immutable message. This is especially useful when we’re using Akka.Persistence to periodically back up this actor’s state to a database or cloud file-system periodically.
Override the ToString()
Method to Pretty-Print Domain Events
When troubleshooting production Akka.NET systems developers often rely on Akka.NET’s logging infrastructure and tools like Phobos actor tracing - but getting good information out of those systems often requires verbosely printing out the contents of critical messages propagated throughout the system.
The easiest way to manage this is to override the object.ToString()
method on each of your domain events and customize them to “pretty print” the state of your events, like so:
/// <summary>
/// Concrete <see cref="IPriceUpdate"/> implementation.
/// </summary>
public sealed class PriceChanged : IPriceUpdate, IComparable<PriceChanged>
{
public PriceChanged(string stockId, decimal currentAvgPrice, DateTimeOffset timestamp)
{
StockId = stockId;
CurrentAvgPrice = currentAvgPrice;
Timestamp = timestamp;
}
public DateTimeOffset Timestamp { get; }
public decimal CurrentAvgPrice { get; }
public string StockId { get; }
public int CompareTo(PriceChanged other)
{
if (ReferenceEquals(this, other)) return 0;
if (ReferenceEquals(null, other)) return 1;
return Timestamp.CompareTo(other.Timestamp);
}
public int CompareTo(IPriceUpdate other)
{
if (other is PriceChanged c)
{
return CompareTo(c);
}
throw new ArgumentException();
}
public override string ToString()
{
return $"[{StockId}][{Timestamp}] - $[{CurrentAvgPrice}]";
}
}
When you follow this approach all of the logic on how to render domain events as text becomes self-contained as part of the message itself, and many different pieces of infrastructure which might all have to print out the content of the message (logging, exception handling, etc…) can all use a consistent display format if ToString()
is implemented correctly.
- Read more about:
- Akka.NET
- Case Studies
- Videos
Observe and Monitor Your Akka.NET Applications with Phobos
Did you know that Phobos can automatically instrument your Akka.NET applications with OpenTelemetry?
Click here to learn more.