Introduction
While looking at the concepts of DDD (Domain Driven Design) I came across the event sourcing principle. After hassling through some theory on the subject (next site owned by Martin Fowler give some good insights on the subject: https://martinfowler.com/eaaDev/EventSourcing.html) I came to the idea to put my knowledge in practice by providing a simple C# based sample which briefly illustrates this pattern.
Background
The general idea of event sourcing is to ensure that every change to the state of an application is captured in an event object, and these event objects are themselves stored in the sequence they were applied. Simply said event sourcing contains a log of changes applied to an object. Recording these subsequent changes can become handy when you want to replay the changes made to your objects (for whatever reason ...)
The sample application
The sample application is kept quite simple. It is based on the Ship Tracking Service sample provided by Martin Fowler on his site (check Introduction for details). The remainder of this article explains the implementation of the sample code. Please note, as I only want to explain the pattern behind event sourcing I did not introduce any professional messaging infrastructures (e.g. NServiceBus) and associaties queueing mechanisms, as we should normally rely on in a production system.
The Use Case
The code case is quit simple. We have Ports (Harbors) and we have Ships. Next a Ship arrives (Arrivals) or leaves (Departures) a certain Harbor. When a ship leaves a Port, then it is AT SEA else it is in a specific Port or none of these in case the Ship is in maintenance mode (maintenance mode is out-of-scope here ...). Finally we have a Ship Tracking Service which Tracks Ship arrival or departure. The project has some "dummy" Ships and some "dummy" Ports defined and Port arrivals are taken at random (thus possible is the case that a Ship leaves and arrives multiple times in same Port, maybe when the crew forgot their boterhammekes on the Port ;-) ). (Boterhammekes is dutch translation for slices of bread).
The Domain Model
So our related domain model is quit simple. We have a userinterface which consists of a single form FormShipTrackingService, this form starts an instance of the ShipTrackingService which includes a timer that get's called every 2 seconds. We simulate an arrival or departure of a ship on each timer tick interval. The arrival/departure is called a ship tracking event. So on each ship tracking event, we record this event in the UI : arrival time, recording time, ship and port are recorded. Multiple arrivals/departures of a ship lead to multiple change-records in the processing log. Finally the tracking processor notifies the ship to update itself on each tracking event (port update). That's all what's happening in this simple use case. After knowing this, let's look at some of the implementation details.
The Models
The Ship Model
using TrackingService.DomainEvents;
namespace TrackingService.Models
{
public class Ship
{
#region Properties
public int ShipId { get; set; }
public string Name { get; set; }
public Port Location { get; set; }
#endregion Properties
#region Public Interface
public void HandleArrival(ArrivalEvent ev)
{
Location = ev.Port;
}
public void HandleDeparture(DepartureEvent ev)
{
Location = ev.Port;
}
#endregion Public Interface
}
}
Ship models has a unique id, name and location (port). It also contains two methods that will be called by the tracking processor to update the ship's state after been tracked.
The Port Model
namespace TrackingService.Models
{
public class Port
{
#region Properties
public int PortId { get; set; }
public string Name { get; set; }
#endregion Properties
#region Public Interface
public override string ToString()
{
return Name;
}
#endregion Public Interface
}
}
Each Port has a name and is defined by a unique id.
The Domain Event Classes
Next our sample contains some domain event classes.
DomainEvent
namespace TrackingService.DomainEvents
{
public abstract class DomainEvent
{
#region Private Storage
private DateTime _recorded, _occured;
#endregion Private Storage
#region Internal Interface
internal DomainEvent(DateTime occured)
{
this._occured = occured;
this._recorded = DateTime.Now;
}
abstract internal void Process();
#endregion Internal Interface
}
}
On Top of the hierarchy we have the DomainEvent
class This class defines common properties like occurence and recording times. It also defines the abstract Process
method which has to be implemented by depending classes.
ShippingEvent
using TrackingService.Models;
using TrackingService.Services;
namespace TrackingService.DomainEvents
{
public abstract class ShippingEvent : DomainEvent
{
#region Private Storage
private Port _port;
private Ship _ship;
private TrackingType _trackingType;
#endregion Private Storage
#region Public Properties
public Port Port
{
get { return _port; }
set { _port = value; }
}
public Ship Ship
{
get { return _ship; }
set { _ship = value; }
}
public TrackingType TrackingType
{
get { return _trackingType; }
set { _trackingType = value; }
}
#endregion Public Properties
#region Internal Interface
internal ShippingEvent(DateTime occured, Port port, Ship ship,TrackingType trackingType) : base(occured)
{
this._port = port;
this._ship = ship;
this._trackingType = trackingType;
}
#endregion Internal Interface
#region Public Interface
public override string ToString()
{
return $"TrackingType: {this.TrackingType} Ship: {this.Ship.Name} Port: {this.Port.Name}";
}
#endregion Public Interface
}
}
ShippingEvent
is the base Shipping class. It adds shipping specific properties and behavior to the parent domain event class. The shipping event class keeps track of the Ship, Port and Tracking Type (Arrival,Departure,None) and it uses it's base class to set the inherited member values.
Arrival and Departure Events
using TrackingService.Models;
using TrackingService.Services;
namespace TrackingService.DomainEvents
{
public class ArrivalEvent : ShippingEvent
{
#region Internal Interface
internal ArrivalEvent(DateTime arrivalTime, Port port, Ship ship, TrackingType trackingType) : base(arrivalTime, port, ship,trackingType)
{
}
internal override void Process()
{
Ship.HandleArrival(this);
}
#endregion Internal Interface
}
}
using TrackingService.Models;
using TrackingService.Services;
namespace TrackingService.DomainEvents
{
public class DepartureEvent : ShippingEvent
{
#region Internal Interface
internal DepartureEvent(DateTime departureTime, Port port, Ship ship,TrackingType trackingType) : base(departureTime,port,ship,trackingType)
{
}
internal override void Process()
{
Ship.HandleDeparture(this);
}
#endregion Internal Interface
}
}
The ArrivalEvent
and DepartureEvent
classes are quit similar. The first is used to notify the Ship
class from arrival at a Port
, the latter for departure from a Port
.
EventProcessor
namespace TrackingService.DomainEvents
{
public class EventProcessor<T> where T : ShippingEvent
{
#region Private Storage
private IList<T> _eventLogger = new List<T>();
#endregion Private Storage
#region Public Interface
public void ProcessEvent(T e)
{
e.Process();
_eventLogger.Add(e);
}
public int CountEventLogEntries()
{
return _eventLogger.Count;
}
public List<T> GetEvents()
{
return _eventLogger as List<T>;
}
#endregion Public Interface
}
}
The EventProcessor
class takes a generic type as parameter, in our case it has been restricted to items of type ShippingEvent
(because shipping event is the base class for logging). So this class get's events from the ShipTrackingService
and processes them. Important to note is that this processing class is the class who add's event sourcing (this by adding the received Arrival or Departure events to the event sourcing log).
Common Application Flow
Application Entry Point
private void FormShipTrackingService_Load(object sender, EventArgs e)
{
try
{
_trackingService = new Services.ShipTrackingService();
_eventProcessor = new EventProcessor<ShippingEvent>();
_trackingService.ShipTracked += _trackingService_ShipTracked;
this.SetDataSource();
SetTimer();
}
catch(Exception ex)
{
MessageBox.Show(ex.Message);
}
}
The application flow starts in the Load
event of the main form. First we create an instance of the TrackingService
and EventProcessor
. Next we set the DataSources
and start a tracking Timer
. With the tracking Timer wi simulate arrivals/departures of ships. The user interface also subscribes to the ShipTracked
event of the TrackingService. The mentioned items are more detailed below.
Set the DataSource
private void SetDataSource()
{
_shipsBindingSource.DataSource = null;
_shipsBindingSource.DataSource = _trackingService.Ships;
}
As we track a list of Ships
I used a BindingSource
as a datasource for the ships grid. The BindingSource
is simply bound to the static list of ships as defined by the tracking service.
Initialize the Tracking Simulation Timer
private void SetTimer()
{
_timer = new System.Windows.Forms.Timer();
_timer.Interval = 2000;
_timer.Tick += _timer_Tick;
_timer.Enabled = true;
}
We use a simple timer object to simulate ship arrival/departures. The timer has an interval of 2000ms (2sec).
Ship Arrival/Departure Tracking
private void _timer_Tick(object sender, EventArgs e)
{
if(_trackingService != null)
{
int maxShip = _trackingService.Ships.Count;
_selectedShipId = _randomShip.Next(1, maxShip);
_trackingService.TrackedShip = _trackingService.Ships[_selectedShipId];
_trackingService.TrackingType = _trackingService.TrackedShip.Location.PortId == 0 ? TrackingType.Arrival : TrackingType.Departure;
_trackingService.Recorded = DateTime.Now;
_trackingService.TrackingServiceId = Guid.NewGuid();
if(_trackingService.TrackingType == TrackingType.Arrival)
{
int maxPort = _trackingService.Ports.Count;
_selectedPortId = _randomPort.Next(1, maxPort);
}
else
{
_selectedPortId = 0;
}
_trackingService.SetPort = _trackingService.Ports[_selectedPortId];
_trackingService.RecordTracking(_eventProcessor);
_numberOfEventsCount.Text = _eventProcessor.CountEventLogEntries().ToString();
this.SetDataSource();
}
}
As already mentioned before, with the timer_tick
event we simulate the ship tracking. The coding comments should be clear enough to understand the behavior of this event.
Show the EventSource Log
private void toolStripButtonShowEvents_Click(object sender, EventArgs e)
{
this._eventsTextBox.Text = string.Empty;
try
{
Ship currentShip = (Ship)this._shipsBindingSource.Current;
if(currentShip != null)
{
List<ShippingEvent> events = _eventProcessor.GetEvents() as List<ShippingEvent>;
var filterByShip = events.Where(ev => ev.Ship.ShipId == currentShip.ShipId);
foreach (ShippingEvent ev in filterByShip)
{
this._eventsTextBox.Text += ev.ToString() + "\r\n";
}
}
}
catch(Exception ex)
{
MessageBox.Show(ex.Message);
}
}
Every ship event (arrival/departure) is logged by the EventProcessor
. This is the core of the Event Sourcing mechanism. The user can show a list of tracking events (which are changes in state of the selected ship), as shown next:
The Tracking Service
There is one piece of code that we didn't detail yet, and this is the behavior of the ShipTrackingService
itself. The tracking service and it's dependencies are stored in the Services folder. I will go briefly through each in the next sub sections.
Tracking Type Enum
namespace TrackingService.Services
{
public enum TrackingType { Arrival, Departure, None};
}
Enums which holds the different possible tracking types.
ShipTracked EventArgs
using TrackingService.Models;
namespace TrackingService.Services
{
public class ShipTrackedEventArgs : EventArgs
{
#region Public Properties
public Guid TrackingServiceId { get; set; }
public DateTime Recorded { get; set; }
public TrackingType TrackingType { get; set; }
public Ship TrackedShip { get; set; }
public Port OldLocation { get; set; }
public Port NewLocation { get; set; }
#endregion Public Properties
}
}
The ShipTrackedEventArgs
class will be used by the ShipTrackingService
to notify it's subscribers (the ShipTracking-UI in our case ...) that a ship tracking Record
took place. The UI will then use this data to refresh it's content.
Ship Tracking Service
As the number of coding lines for the ShipTrackingService
is to big, I will slice the codebase into different pieces.
ShipTrackingService Slice-1: Event Declaration
#region Event Declaration
public delegate void ShipTrackedEventHandler(object sender, ShipTrackedEventArgs e);
public event ShipTrackedEventHandler ShipTracked;
#endregion Event Declaration
We define a delegate which will provide the necessary interface that a subscriber (UI) can use to update it's content depending on the occured event in the ship tracking service
ShipTrackingService Slice-2: Instance Variables Declaration
#region Private Storage
private TrackingType _trackingType = TrackingType.None;
private Guid _trackingServiceId;
private DateTime _recorded;
private List<Port> _ports;
private List<Ship> _ships;
private Ship _trackedShip;
private Port _currentPort;
private Port _setPort;
#endregion Private Storage
The ShipTrackingService has some state:
Variable | Info |
TrackingType | Type of tracking (Arrival, Departure, None). |
TrackingServiceId | Unique identifier for the TrackingService. |
Recorded | Date/Time of tracking recording. |
Ports | Dummy Ports. |
Ships | Dummy Ships. |
TrackedShip | Reference to the Current Tracked Ship. |
CurrentPort | Reference to the Current Tracked Port. |
SetPort | Reference to the New Port destination in case of Arrival. |
Of course each backing variable has it's public property.
ShipTrackingService Slice-3: Initialization
#region C'tor
public ShipTrackingService()
{
_ports = new List<Port>()
{
new Port()
{
PortId = 0, Name = "AT Sea"
},
new Port()
{
PortId = 1, Name = "Port of Shangai"
},
new Port()
{
PortId = 2, Name = "Port of Antwerp"
},
new Port()
{
PortId = 3, Name = "Port of Singapore"
},
new Port()
{
PortId = 4, Name = "Port of Dover"
}
};
_ships = new List<Ship>()
{
new Ship()
{
ShipId = 1, Name = "Ship_1", Location = _ports[0]
},
new Ship()
{
ShipId = 2, Name = "Ship_2", Location = _ports[0]
}
,new Ship()
{
ShipId = 3, Name = "Ship_3", Location = _ports[0]
},
new Ship()
{
ShipId = 4, Name = "Ship_4", Location = _ports[0]
}
};
}
#endregion C'tor
In the constructor code we initialize our Ships
and Ports
. Please not that the Location
(Port) property refers to "AT-SEA" port which means that while starting up our ShipTrackingService
every ship is supposed to be AT-SEA (and not in a port ...).
ShipTrackingService Slice-4: Ship Tracking
#region Public Interface
public void RecordTracking(EventProcessor<ShippingEvent> eProc)
{
Port OldLocation = TrackedShip.Location;
ShippingEvent ev;
if (TrackingType == TrackingType.Arrival)
{
ev = new ArrivalEvent(DateTime.Now, SetPort, TrackedShip,TrackingType);
}
else
{
ev = new DepartureEvent(DateTime.Now, SetPort, TrackedShip, TrackingType);
}
eProc.ProcessEvent(ev);
ShipTrackedEventArgs args = new ShipTrackedEventArgs()
{
TrackingServiceId = TrackingServiceId,
Recorded = Recorded,
TrackingType = TrackingType,
TrackedShip = TrackedShip,
OldLocation = OldLocation,
NewLocation = SetPort,
};
OnShipTracked(args);
}
#endregion Public Interface
#region Protected Interface
protected virtual void OnShipTracked(ShipTrackedEventArgs args)
{
if (ShipTracked != null)
ShipTracked(this, args);
}
#endregion Protected Interface
The RecordTracking
method is the core of our Tracking Service. This method is called from the UI _timer_Tick
method and it does 2 things: first it creates and delegates a new Arrival or Departure event to the EvenProcessing engine. The EventProcessing engine wil instruct the concerned ship to update it's state depending on the supplied event data. Next it informs the attached subscribers (Tracking Service UI in our case) that a tracking event took place. Next the UI can take appropriate action to update it's state. The event handling code in the UI is shown below:
private void FormShipTrackingService_Load(object sender, EventArgs e)
{
try
{
...
_trackingService.ShipTracked += _trackingService_ShipTracked;
...
}
...
}
private void _trackingService_ShipTracked(object sender, ShipTrackedEventArgs e)
{
this.TrackingOccuredTextBox.Text +=
$"TrackingId: {e.TrackingServiceId}\r\n" +
$"RecordedAt: {e.Recorded.ToLongTimeString()}\r\n" +
$"TrackingType: {e.TrackingType}\r\n" +
$"Ship: {e.TrackedShip.Name} Id: {e.TrackedShip.ShipId}\r\n" +
$"Current Location : {e.OldLocation.Name}\r\n" +
$"New Location : {e.NewLocation.Name}" +
"\r\n\r\n";
}
Putting it all together
So now that we have a good understanding of the different parts that make up our application, let's have a look at the user interface. On top we have our list of ships. In bottom of the screen we have the tracking table which shows all the arrivals/departures in sequence of occurence. Next to demonstrate the events sourcing in the middle of the screen we have the log details of a single ship. Ship_3 in our case.
Some Optimalization Points
I tried to make the sample as simple as possible, so i used concrete classes for the implementation. In real environment, you should ommit to directly use concrete classes, because, in many cases we could have different forms of TrackingServices all sharing some common logic and adding specific behavoir. For this reasen the ShipTrackingService should be implemented by using an Interface and use a Dependency Injection mechanism to inject concrete instances of our service in our client class, as shown below: