- A purely functional stack for building apps
- Benefits
- Granularity
- ACID
- Simple model for creating state
- Multi use events
- Out of the box integrations
- What the framework will do
- What the client programmer will need to do
The main difference of a functional app is that we don't mutate state directly. In fact current state is derived entirely from events that have been observed to happen, so we should be able to recover any state by replaying the events we have seen. The stream of events that has occurred is the golden source of data in the system and can be folded down into any other data source to express state.
In this section I'll cover what we want from a "purely functional" applications stack. At first appearances it can seem a lot of trouble so we should motivate all this with a look at the benefits.
Because we store what's happening at the level of granularity of events in the app that have semantic meaning, we never loose that granularity. This is in contrast to apps with a traditional CRUD back end where SQL commands are issued directly. In these apps you only see things as they are right now - you loose the flow of how things have got to be this way. If you're lucky in the old model you might have a "last updated" column or similar. You might also have bland data-centric event data in a WAL log or similar so that you can see what changes have happened in the database over time.
At the point of creation events will have access to a relational database and
will therefore be able to use all of the referential integrity tools to ensure
that the events being generated are valid. Let's say for example that you're
building a app that tracks to-dos as they relate to wider goals. If your
business logic states that a to-do must have a related goal you might have a
todo
table and a goal
table with a foreign key constrains between them in
your CreateToDo
command can simply attempt to insert into the todo
table. If
the inserts succeeds then the event is produced. If the foreign key (or any
other constraint) is violated it won't be.
At some point a single database for writing events will become an unnacceptable bottleneck. It's worth thinking a bit upfront about how we might change things at that point to embrace this Acid 2.0 idea.
ACID here refers to:
- Associative
- Commutative
- Idempotent
- Distributed
Where we have events that meet these properties it would be good to make use of them if possible.
Because we have all the information required to re-create state is in the events the conceptual model is simply a left fold:
foldlM :: (Foldable t, Monad m) => (state -> event -> m state) -> state -> t event -> m state
This should be pretty much all the client programmer has to provide. This also means that to make a representation of the data available in different systems as needed you only have to refold a source of events that you know to be valid. For example you might want to replicate the data to elastic for searching or some kind of data warehouse for analysis.
As alluded to in the last section, once you have a stream of valid events and a simple model for how to react to them the world opens up. It becomes very simple to write small apps at the edges of your systems that react to changes in the world in real time. In our experience even the UI can be built in this way.
Several ways of looking at event data are so useful that they will be provided out of the box. These include streaming events into elastic so that they can be used in kibana alongside what you might generate from system logs. We might also think about a bridge into something like snowplow for analytics.
The framework we will provide will take care of
We ought to be able to provide a mechanical (or at least very light weight) mapping of 3.1 to POST endpoints.
The framework will take care of making sure that all events produced from interpreting a command (or commands) will be associated with a transaction. So in the following scenario
composite = do
result <- command1
command2 result
Interpreting composite will associate the events produced by command1 and command2 with the same transaction id in the event stream.
It would be nice to have this generated from the data types (or vice versa)
Commands are a DSL interpreted in a free monad that gives the programmer access to the write database and any other environment required (see 2.2). The programmer will need to define the commands pertaining to their state and how the commands compile down to and interleaving of SQL statements and produced events.
Once a valid event stream is produced from command interpretation, deriving state (or the "Read Model") is just a foldM.
As mentioned earlier the view in the browser (or other type of UI) is just a special case of a fold over events. In this case the events will be pumped over an 3.2.2 but the principle is the same. The client programmer will need to write the views.