Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / event

Event Stream Accounting

4.67/5 (2 votes)
11 Jan 2015CPOL4 min read 20.7K  
Use of the Event Source application architecture in financial software

Introduction

Event sourcing is an architecture model based on the storage of the sequence of events that have occurred in an objects' lifetime rather than storage of a point-in-time state of that object.

It lends itself well to financial applications because (typically) any account balance is a result of the events leading to that balance and we want the ability, both to see the underlying derivation of a figure and also to be able to play that valuation as of a given point in time.

Event sourcing allows us to perform “what if” analysis by filtering out events or by playing in alternate streams.

Quick Example – A Simple Bank Account

A bank account is probably the simplest financial stream consisting solely of deposits and withdrawals of a single currency. In this model, an event is simply an amount by which the balance is adjusted – deposits being positive and withdrawals being negative.

[ - - +50 - - +50 - - -100  - ->]

In this example, the valuation changes only when the actual deposit or withdrawal occurs – so playing the event stream through to the end, we can see that the current balance is now 0.

Second Example – A Shareholding Account

For a shareholding account, there are two streams. There is a positions stream where purchases or sales of the shareholding are recorded and there is a pricing stream where prices for the underlying share are recorded.

[ - - +50 - - +50 - - -100  - ->]
[178.1 - - - - 178.0 - - - - 178.9 - - >]

These can be combined into a valuation stream which is the combination of the current holding multiplied by the most recent price. Both a holding event and/or a pricing event will trigger a valuation.

[ 0 - - 8905.00 - - 17810.00 - - 17800.00 - - 0.00 - - 0.00 ]

Third Example – Holdings in a Non-Base Currency

If your shareholdings are held in a non-base currency, then the changes in the exchange rate between the holding currency and the account base currency also affect the valuation.

[ - - +50- - +50- -  -100- -> ]
[178.1 - - - - 178.0 - - - - 178.9 -> ]
[1.1- - -  1.2 - - -  1.3 - - -  1.4 -> ] 

Image 1

Events

For any event, you need to know – when did it occur, what stream was it in, what was its value. From the above example, you can also see that the value can be an absolute amount (for example, a price or an exchange rate) or it can be a change (or delta) amount.

For an event stream that is a stream of delta amounts, it can be helpful to write in periodic snapshots of the current value. This is to allow us to play back the stream to any given point by going to the snapshot prior to that point, then playing in any delta events post that snapshot time.

Derived Events

Streams can be source data or derived by functions of one or more source streams. For derived events, a function is used to calculate the value from its constituent streams and this is triggered for any event on any of those streams.

Double-Entry Accounting

In double entry accounting, holding events are recorded in two (or more) streams – one representing each of the ledger accounts the event impacts (most commonly, one for assets and one for liabilities). Traditionally, this is done using posting rules on the input stream to post to the relevant two (or more) account streams.

Snapshots

In order to keep the read-access time to a minimum, the architecture underlying this should support reading and creating snapshots. These snapshots can be treated as data transfer objects for the purpose of integration into any system based on them (such as an MVC application).

The interfaces to define the operations required would look like:

C#
/// <summary>
/// Repository to write to objects that are held in snapshot form
/// </summary>
/// <typeparam name="TEntity">
/// The type of entity in the repository
/// </typeparam>
/// <typeparam name="TKey">
/// The type of the key to uniquely identify the entity
/// </typeparam>
/// <remarks>
/// This does not inherit from IRepositoryWrite
/// because it is possible that some background task will be
/// creating the snapshots independent of front-end write requests
/// </remarks>
public interface IRepositoryWriteSnapshot<TKey, TEntity> where TEntity : IKeyedEntity<TKey>
{
    /// <summary>
    /// Request that a snapshot as-of-now be taken for the given entity aggregate
    /// </summary>
    /// <param name="key">
    /// The unique identifier of the entity that we want snapshotted
    /// </param>
    /// <remarks>
    /// This does not return any value as it is designed to
    /// operate asynchronously by setting a "needs snapshot" flag to prevent
    /// the snapshot generator system being flooded
    /// </remarks>
    void RequestSnapshot(TKey key);

    /// <summary>
    /// Delete and regenerate any snapshots post the as-of synchronization value
    /// </summary>
    /// <param name="key">
    /// The unique identifier of the entity that we want snapshotted
    /// </param>
    /// <param name="synchronization">
    /// The synchronization point for which we want the data regenerated
    /// </param>
    void RegenerateFromAsOf(TKey key, long synchronisation);
}

And the read side can be implemented as an extension to the usual repository pattern:

C#
/// <summary>
/// Repository to read objects that are held in snapshot form
/// </summary>
/// <typeparam name="TEntity">
/// The key-identified type of entity we are reading
/// </typeparam>
/// <typeparam name="TKey">
/// The type of the key
/// </typeparam>
/// <remarks>
/// Where an object is based on one or more event streams,
/// this allows for retrieval of the state of that object as at a given
/// snapshot time.  This extends repository-read
/// </remarks>
public interface IRepositoryReadSnaphot<TKey, TEntity>
    : IRepositoryRead<TKey, TEntity>  where TEntity : IKeyedEntity<TKey>
{
    /// <summary>
    /// Did any record matching the key exist as at the given point in time
    /// </summary>
    /// <param name="key">
    /// The unique aggregate identifier of the record we are seeking
    /// </param>
    /// <returns>
    /// True if a matching record is found
    /// </returns>
    bool ExistedAsOf(TKey key, long synchronisation);

    /// <summary>
    /// Get the entity state as it was at the given point in time
    /// </summary>
    /// <param name="key">
    /// The aggregate identifier of the record for which we want a point-in-time view
    /// </param>
    /// <param name="synchronisation">
    /// The synchronisation point for which we want the data
    /// </param>
    /// <returns></returns>
    Nullable<TEntity> GetByKeyAsOf(TKey key, long synchronisation);
}

In practice, audit or legal requirements may require you to keep old snapshots if they were sent externally. A versioning mechanism would be needed to make sure any display (UI or print) uses the latest version of the snapshot.

Points of Interest

One of the biggest things to consider with this type of architecture is idempotency - making the system immune to error if an event is run more than one time. In practice, this tends to mean replacing messages that perform a delta (such as "reduce the account by $50") with messages that perform an absolute (such as "set the account holding to $745.20").

However rather than artificially making absolute events, you are better to drill down until you get to events that are inherently absolute. In the above example, having absolute deposit and withdrawal events is preferable to having a "set balance to x" derived event.

Another challenge is sequencing - it is important that events are added to the stream in the correct chronological order. In practice, a universally accepted timestamp will need to be used.

History

  • 2nd May, 2014: Initial concept

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)