Skip to content

Instantly share code, notes, and snippets.

@swlaschin
Last active December 15, 2020 22:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save swlaschin/ed9cae164e18e22ebae8f438b81db0e8 to your computer and use it in GitHub Desktop.
Save swlaschin/ed9cae164e18e22ebae8f438b81db0e8 to your computer and use it in GitHub Desktop.
Functional approach to Logging and Transactions

"What is a functional approach to logging and transactions" is a question I get asked sometimes. So here's my take.

First, there is never a perfect answer! It depends on your tolerance for impurity, what's compatible with the rest of the code and what other maintainers expect.

Logging

I use F#, which generally has a pragmatic approach to this kind of thing.

In Haskell or purist FP Scala (e.g. with Cats/Scalaz) the standard approach would be to use a writer monad, but for F#/OCaml/ReasonML etc, that would probably be overkill.

So, for logging I normally just have a global ILogger instance that I talk to. It's a lot easier than (a) passing in a logger to every function, or (b) using a writer monad in an impure language.

For unit tests, I either set the logger to a dummy version, or I initialize it to use the same logger that I use in production (e.g. Serilog).

Transactions

For transactions, my approach depends.

If you're using a pipeline approach where all I/O is at the edges (as explained in my talk), then I do transactions as part of the I/O so my core code doesn't see it. If anything fails, a rollback occurs and nothing is saved.

If you have a more complicated design with lots of IO interleaved with business logic, the Interpreter/FreeMonad approach can be useful. (my version for turtles). Again, you would start a transaction when you start interpreting the data structure, and if anything fails, a rollback occurs and the whole transaction is aborted.

If you are using a transaction to make sure that two different services have been written to (e.g a database and a message queue), then I would avoid a two-phase commit ("Starbucks Does Not Use Two-Phase Commit") or Distributed Transaction Coordinator and instead use the so called "Transactional Outbox" pattern.

And if the domain is even more complicated, then you might even need a special broker to do the coordination. The "Enterprise Integration Patterns" book has some good stuff for this.

Finally, you can take a leaf from real-world transactions that are distributed over time and space (e.g in banking and accounting). In these systems, there are no long-duration transactions. Instead, there is a separate process that checks that everything matches up ("reconciliation") and if anything goes wrong a "compensating transaction" occurs (such as reversing a credit when a check bounces!)

Udi Dahan did a talk at DDDEU on this topic which has some useful ideas.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment