As we mentioned previously, now that we have open sourced Reactive Trader® at ReactConf, we have got a few blog posts lined up to go into it in more detail. Eventually, we will have ported Reactive Trader® to HTML completely, and we will do another series of posts on the techniques we used to do that. But, until then, we will expand on our talk at ReactConf in more detail.
The themes we thought we would cover in the coming weeks include
- Asynchrony & Concurrency
- All Requests Result in Streams
- System Health and Failures
Further to above, and in more technical detail, we thought we would do a few posts on data access and patterns in asynchronous user interfaces. The difference between retrieving your domain model via a synchronous request-response ORM API and an asynchronous pub-sub message-oriented-middleware is quite substantial, and you need a number of different data access patterns to make your app testable and performant.
Asynchrony & Concurrency
A recurring theme throughout our talk was of the need to embrace asynchrony in all its forms. Reactive Extensions lets us do that, but we also need to manage concurrency, and to do that we typically introduce it via our own API rather than going directly to the .NET Framework's concurrency primitives. Those classes that introduce concurrency do so via this API, so it is easy to review and check that they are doing it correctly. To keep things simple, we would usually only expose two or three models of concurrency. In .NET we have a well implemented process-wide thread-pool that is used to perform background work, as well as our UI thread event loop (dispatcher). So, our IConcurrencyService would have a ThreadPool and a UiDispatcher scheduler exposed on it.
You can see an example of this pattern below, taken direct from Reactive Trader.
The .NET platform has, over its time, introduced and discontinued a number of different models of asynchrony. The two most recent ones are Rx and Task. I won't go into either of those here, but the best introduction to Rx is Intro to Rx. For those that aren't familiar with Task, it is the .NET implementation of Futures and Promises. For our style of application, where nearly all asynchronous calls result in streams, and the fact that we need to compose over a number of different streams, it is probably no surprise that we have chosen to use Rx.
Go Observable Early
Having made the decision to use Rx, it is very important to force all other forms of asynchrony into that model for ease of consumption. For UI programming, this is typically some form of event based API, which fortunately maps very well to Rx. So, the API for streams of events from the network is Rx, as is the API for streams of events (button clicks, text box entries, etc) from the UI.
But having a single API for your asynchrony doesn't mean you can't embrace and work with different semantics for different parts of the system. For example, there are streams (which return a series of events), request-response style calls that return one event and complete, data lookups which often return immediately if the value can be found or may error if the value is invalid or cannot be found, and a signal style event which may wait a very long time before eventually yielding.
All of these different styles of observable streams are exposed via one interface, and we simply use naming conventions to distinguish between them. We have in the past tried to signal this via the type of the stream, but the .NET type system is not as powerful as others, and we quickly lose the intent of the initial stream when we compose with other streams.
As your usage of Rx becomes more advanced, you begin to realise the benefits of a more functional approach to types. Coercing related event streams into the same stream with the use of an Either or Tuple-style type makes reasoning about your application logic that much easier. Indeed, we often find that the hard work of working with a number of asynchronous streams is coercing them into the same stream. Once that has been done, the correct behaviour for your application is easily implemented.
A complex example of this is below, where we take a stream of T and turn it into a stream of IStale<T>. The contract for this operator is to inject an IStale<T> after some period of silence on the stream. This IStale<T> is an instance containing no actual T value, identical in behaviour to the Maybe<T> functional type, but with more semantics appropriate for our use case.
Asynchronous Application Initialisation
Getting the application up and running can be complex and involve a central orchestrator initialising network calls and waiting for responses. Doing this in an isolated a fashion as possible can be complex, but the important thing is to try to hide the complexity of application lifecycle from your individual components. Indeed, application start up which results in a large number of requests to back end services to load in various entitlement and configuration data can be the most complex part of the app's lifecycle, far more complex than when it is running in steady-state.
Again, it is important to embrace asynchrony everywhere. Users want the application to load and become responsive as quickly as possible, which means that you need to do a form of progressive enablement to turn on the various parts of your application that require data supplied by the server as soon as it has arrived. For example, we would disable a client selection drop down until we had received the list of clients but would enable streaming prices as soon as we start receiving them. We would not block synchronously at start up until all data had been retrieved from the server, or not, before allowing the user to continue. This means that all data retrieval APIs in your application must be asynchronous, even if at a lower layer the data is cached in your app and so can return synchronously. More on this topic in a later blog post.
Something we haven't implemented (yet!) in Reactive Trader® is a specific user experience (UX) to tell the user that data is currently being loaded from the server. This would take the simple form of a spinner over the blotter area, for example, until we had received the initial set of trades. For some users, who have no trades, this spinner would go and just leave an empty blotter - but we would have re-assured them that all known trades had finished loading. Remember, silence on a stream - in this case, no current trades - is just as an important business event as activity on a stream.
CEO and founder,
Adaptive Financial Consulting