Previously we outlined the three pillars of the virtual actor: Ownership, Lifecycle and Transaction. I also briefly introduced a few techniques we can use to help us model our actors. Now, we will put it all together and model a system that allows us to track aircraft in real-time.
Introduction
ADS-B is a technology that allows aircraft to broadcast their position, altitude, velocity, and other data. The data is generally available to the public. Using Microsoft Orleans our goal is to:
Track aircraft from the available ADSB data: Current location,
previous location, heading, speed and altitude.
Track flight/trips that the aircraft makes from the available data: Start time, end time, start location, end location, and all of the points (the path the aircraft has taken) of the trip.
Note: You do not have to use Virtual Actors, but in this example that is what we will use. Feel free to use Orleans, Akka, Dapr, Darlean, Restate, Cloudflare etc. Virtual Actors are different to conventional Actor Model systems - read more here.
Dive in
Let’s start of and model the system in the most naïve fashion possible - a YAGNI approach. We can always come back and remodel when the time is right, and we understand the domain better.
The most straightforward approach could be to create a single actor type that represents an aircraft by its call sign. We can then feed ADS-B data in, parse it, and pass it to the appropriate actor. The AircraftActor can store the current state, and perhaps even some historical data.
Remember our actors are virtual. This means they exist when we first send requests to them (activation), and they will deactivate when that data flow becomes idle.
Immediately it becomes visible that we’re just treating the actor as a glorified database record; a container for current state. We’re using some good verbs and nouns (as per the domain), there is some state, and on the surface it looks okay… but we’re not truly thinking in actors. We're ignoring the fundamental strengths of the virtual actor model, namely lifecycle, state, behaviour, ownership, autonomy and aggregations. This naive implementation essentially recreates an anemic domain model in an RPC distributed manner. Let's explore how we can evolve this design by properly applying actor modeling principles.
Applying the Three Pillars of Actor Modeling
We need to apply a lens for each of the three pillars of actor modeling, which are:
Ownership
Which actor has exclusive authority to modify a particular piece of state?
What boundaries exist between different actors' responsibilities?
How are relationships between actors managed and by whom?
Who coordinates long running processes?
Which actor is the source of truth for a specific data point?
How does an actor aggregate and synthesise information from other actors?
Which actor owns the rules and policies for state transitions?
Who decides when and how state should be shared with other actors?
Lifecycle
What triggers an actor's activation and deactivation?
When is an actor's state persisted and restored?
When should historical state/data be archived or pruned?
What parts of the actor can operate entirely in memory, with no storage?
What happens if data floods into an actor, or one actor is a bottleneck?
What actions can be made idempotent?
How does the actor maintain its behavioural integrity across reactivations?
How are long-running processes managed across actor lifecycles?
What state aggregations need to persist across deactivations?
How does the actor react to lifecycle events of related actors?
Transactions
How are out-of-order messages handled?
How are failures in state updates managed?
What maintains consistency during cross-actor operations?
How does autonomous behaviour affect transaction boundaries?
What reactive responses should trigger new transactions?
How are aggregated state changes transacted?
When should state changes trigger autonomous behaviours?
…and that’s not even the full set of questions we could ask.
There’s quite a lot to unpack there. There are several concepts we need to summarise first:
Autonomous Behaviour — The ability of an actor to operate independently without requiring external control or intervention. Autonomous behaviour is effectively facilitated through the use of timers and reminders, which allow the actor to control its own invocation, like a living thing. The actor can remind itself every 5 minutes to check something, or setup a timer that runs every 1 minute to check if new data has arrived yet or not. Instead of reacting to events coming in, your actor is a long running living object.
Reactivity — The capability of actors to respond promptly and appropriately to incoming messages or events. Perhaps actors subscribe to stream of data, rather than being directly invoked.
State Transitions — How an actor's internal state changes in response to events or messages. An actor may have multiple internal state objects or state machines. It may be setup like a router or controller to send the request into a nested state machine. Nested state machines might roll up state and transitions and expose that back to other actors.
Idempotence — Ensures that processing the same message multiple times does not lead to inconsistent or unintended states. Request ID’s, state machine validation and message deduplication can assist.
Aggregation — Involves combining data from multiple sources or actors to form a comprehensive view or summary.
Systems built as Reactive Systems are more flexible, loosely-coupled and scalable. This makes them easier to develop and amenable to change. They are significantly more tolerant of failure and when failure does occur they meet it with elegance rather than disaster. Reactive Systems are highly responsive, giving users effective interactive feedback - The Reactive Manifesto
Mapping the skies
What happens when we apply these concepts to our flight tracker domain? In Part 2 we discussed modelling techniques such as Jobs to be done, Digital Twins, Aggregations. Considering the twin like nature of this aircraft tracking domain, we start to ask ourselves the following questions:
What state are we interested in displaying the most? I’d say it’s the current flight status of aircraft inside an area. This includes real-time data like position, altitude, speed, and heading.
What is the lifecycle of an aircraft? Do their identities ever change? Assuming the call sign is enough for now, and the carrier or owner information can change over time too.
Who owns the concept of a flight - should it be the aircraft actor or a separate flight actor? The AircraftActor manages the ongoing state and metadata of the aircraft itself, such as a reference to the active flight, the last n number of flights and meta like type, carrier, etc. By decoupling flights from the aircraft actor itself, we can track flights as their own state machines, with events like started, completed etc.
What happens to historical flight data when an aircraft goes inactive? We can keep the last n number of flights tracked inside the AircraftActor (let’s say 10) and we can also move them to an archival actor for querying.
How do we handle the transition when an aircraft crosses boundaries? As we will be tracking just 1 polygon/geo fence, what happens if the flight crosses a boundary? We can create regional actors (as aggregations) which track what flights are currently inside their boundaries. These RegionActor identities can be based on polygons or even Geohash.
AircraftActor
Identity: Call sign of the aircraft, for example VH-VYJ.
Technique: Digital Twin
Attributes: ICAO address (Hex code), Type, Country, Owner (optional).
Stores: Current active flight, Last known position (may not be linked to a flight in progress), Previous flight list.
Persistence: Save data after the position has changed or the actor deactivates.
Lifecycle: Activates upon receiving data and deactivates after a period of inactivity (15 minutes with no new data). The lifecycle of this actor is for the call sign of the aircraft.
States:
Active: Currently receiving and processing real-time data.
Inactive: No recent data received; may trigger state persistence or deactivation.
Transactional Concerns: Ensure state is protected from out of order, stale or duplicate data. Maintain consistent state transitions when associating with new or ending flights.
FlightActor
Identity: Call sign of the aircraft, and the flight identifier.
Technique: Digital Twin
Attributes: Flight identifier, Call sign.
Stores: Start time, end time, positional trail (potentially down sampled).
Persistence: Save data after the position has changed or the actor deactivates.
Lifecycle: Activates upon receiving data and deactivates after inactivity or when the flight has landed. The lifecycle of this actor should relate only to the flight.
States:
Initial: Has received its first data.
Active: The flight is in progress, and we are receiving data.
Completed: The flight has completed, and data is no longer being received for this flight identifier; may trigger state persistence or deactivation.
Inactive: We have stopped receiving data for this flight, but it has not been completed (loss of communication).
Transactional Concerns:
Updates: Ensure all positional data points are recorded without loss or duplication.
Consistency: Ensure flight start and end times are recorded.
Trail: Ensure the flight trail is accurately recorded even if the data is out of order.
RegionActor
Identity: GeoHash
Technique: Aggregation
Attributes: Level/Size (Geohash Level)
Stores: Current active flights, Last known position of each active flight.
Persistence: Initially do not save state (experiment with the region being in memory only).
Lifecycle: Activates upon receiving data and deactivates after a period of inactivity (30 minutes with no new data). The lifecycle of this actor should relate only to active flights that are contained within its boundary.
States:
Active: Currently tracking at least 1 active flight.
Inactive: Not tracking any active flights.
…these few actors we’ve modelled are a bit dull.
As a first iteration they’re fine, but as we continue to model and grow the system out, we will come back to compare them only to realise they’re quite anemic. That’s ok, it’s natural for this to happen as we discover our domain more and continuously improve.
Note: When modelling distributed actors, contention and pressure points are not often apparent until data flows into your system. Be sure to simulate the appropriate load and monitor actor-actor communication. Where possible use streams/flows to disconnect noisy actor-actor communication. See Orleans Streaming, Akka Flow, Restate Events.
Other Thoughts and Models
Alternative Designs and Variations
Whilst our simple design handles ADS-B data flowing through to track aircraft movements, there are several alternative approaches worth considering.
Time-Based Partitioning Actors
Introduce TimeWindowActor that manages flight data within specific time periods. This is useful for historical analysis and data aggregation and would help with data retention policies and archival strategies. Facilitates a future historical flight query functionality.
Weather-Aware Region Actors
Extend the RegionActor to incorporate weather conditions. You could then track or correlate weather patterns and data against flight behaviours and routes. Perhaps you could even predict flight delays?
Carrier-Based Aggregation
Introduce CarrierActor to manage fleet-wide analytics across commercial aircraft. Much like the RegionActor it could track operational metrics such as on time metrics, delays and events (based on squak codes). This aggregation could provide fleet-wide visibility and reporting.
Observer Pattern Actors
Instead of direct actor-actor communication, an observer pattern akin to a pub/sub system could be modelled internally inside the actor system, where aircraft state changes are published to observers that have registered their intent to receive that data. You could then model a ControlTowerActor for each airport. An EmergencyEventMonitor actor could monitor squak codes, abnormal data, sudden changes in speed and altitude and provide real time notifications on emergency situations. Some actor systems have these kinds of patterns built in, for example in Microsoft Orleans has a feature called Observers. These patterns help reduce direct actor-to-actor communication overhead by broadcasting state changes to multiple interested actors simultaneously. Subscribed actors can unsubscribe based on changing requirements, even autonomously. Alternatively, you can use streams such as Kafka and Redis to create similar systems.
Hierarchical Region Management
We could implement the RegionActors in a graph-like nested structure. Since they are based on GeoHashes (levels of sized rectangles) we can structure the creation and management of child levels on the parent. This could enable efficient querying of different geographical scales, direct from memory. The RegionActors when under load could branch out their work into the smaller next level GeoHash region, thus acting as coordinators, thus allowing the system to dynamically resize based on air traffic density.
A Call to Action
When we first approach actor modelling, it's tempting to fall into familiar patterns such as modelling everything as if it were in a relational database or making actors simple state containers that look like database records. The true power of actors comes from breaking free from database thinking and embrace the unique characteristics of our domain.
Don’t just think of the Actor Model as another architectural pattern, think of it as a fundamental shift in how we conceptualise and create systems that interact with each other. By embracing actors, you can centralise ownership, encapsulate state, and define clear boundaries of responsibility - sure you can do this in any programming language with discipline, but the Actor Model is actively helping you achieve this goal.
Well-designed actors don't just store and process data – they exhibit intelligence, autonomy, and purpose. They're living, breathing entities in your system's ecosystem.
Dare to rethink how you design and model systems, expand with:
Instead of asking "what data should this actor store?", ask "what decisions should this actor make?"
Rather than considering "what methods or APIs an actor should expose?", think "what behaviours and responsibilities define an actor's role?"
Instead of planning "how to manage an actor’s dependencies," envision "how actors can operate independently (even autonomously) and handle their own state transitions."
Rather than designing "actors based on existing service boundaries," ask "what natural boundaries exist in your domain that actors can encapsulate?"
Rather than thinking "how will other components call this actor?", consider "what should this actor proactively do?"
Instead of asking "how will actors communicate with each other?", ask "how can actors collaborate to achieve shared goals?"
Instead of focusing on "data consistency across actors," consider "how actors can achieve eventual consistency through their interactions."
Instead of modeling static relationships, envision dynamic collaborations between actors that evolve based on system needs, even abstract or time-based relationships
Challenge yourself to think beyond the obvious. Stop thinking about tables, rows and columns, and look for opportunities for autonomy. Seek out natural relationships that exist in your domain. The results might surprise you – not just in its elegance, but in its ability to naturally handle complex real-world scenarios.
Even if you don’t use an actor system or framework you can still use these techniques and thought processes to better model any system.
Up Next
Now that we have a rough model, we can begin to develop an actual system. In the next post we will walk through setting up Microsoft Orleans and we will create our first few Virtual Actors.
Comment if you have any modelling changes that you’d make!
TLDR
Three Pillars of Actor Modeling: Focus on Ownership, Lifecycle, and Transaction to define boundaries, manage state, define autonomy and ensure consistent interactions.
Robustness: Utilise autonomous behaviour, reactivity, state transitions, idempotence, and aggregation to create intelligent and resilient actors that can operate independently and handle complex workflows.
Responsibilities and Interactions: Focus on the decisions actors make, their behaviours, and their responsibilities.
Collaborative Actors: Move beyond simple state containers by enabling actors to make decisions, exhibit purposeful behaviours, and dynamically collaborate.
Autonomy: Enable actors to proactively manage their own state and respond to events, allowing for independent operation and reducing dependencies. This autonomy facilitates tasks such as periodic checks, self-healing, and dynamic modifications/adjustments based on real-time conditions.
Read more: