This is a little known pattern I learned a few years ago that is taking more value these days.
When attending BuildStuff second edition in 2013, I had to choose a workshop, and Michael Feathers' one drew my attention. The title Error Proofing Software was a pledge we would dig into the dreaded areas of software development, and this is what I was looking for.
I was amazed by the soundness of Michael's insights in these intricate matters. It changed my programming understanding deeply.
I was already interested in errors through my DDD approach, like in this 2009 post Business Errors are Just Ordinary Events.
But we went further in the classification.
What happened: The business process reached a corner case
What should we do ? Alert the business to take care of the customer
Handling: An automatic or manual business process will take care of it
What happened : The infrastructure had a problem, a resource cannot be reached...
What should we do ? Alert the Ops to fix the infrastructure.
Handling: Retry until it's fixed or degrade the service gently
What happened : Something that was totally unexpected happened and there is no code to handle it.
What should we do ? Alert the developers
Handling: Fix the bug and release to production
One of the challenge is then to categorize errors.
Most errors are triggered through exceptions, and exceptions are notoriously difficult to filter correctly.
Some languages like Java force exception declaration in methods signature, which is a good thing to document what could go wrong, but also appears to be difficult to manage since any adding a new exception becomes a breaking change.
Filtering exceptions based on classes and/or error message is very fragile, and frankly tedious.
One of the promises of Hexagonal Architecture is to have a clean domain, void of any infrastructural or technical concern.
By providing interfaces to the domain, it can be abstracted to run on any infrastructure.
It makes it also easier to test with Fakes and Mocks.
One of the problem with Hexagonal Architecture, is that when the Domain code calls a method on an interface, some technical concerns can leak under the form of exceptions or errors... What should the domain do ?
In functional programming, functions are Pure.
- The output is always the same for the same inputs. It cannot depend on hidden state, internal or external (IO).
- It doesn't cause any observable side effects.
We can get rid of the Hexagonal Architecture problems by doing things like that:
1: 2: 3: 4: 5: 6: 7:
In a pure functional language, the fetchData and saveData parts will use the IO monad for IO effects. In F# or non functional languages, developers have to take care to write side effect free code in the business function. In F# it's quite easy since types are immutable by default.
The structure of the service function is really interesting from a Error handling point of view :
- The fetchData function just fetch data, and can fail only for technical reasons.
- The business function computes values. It can return a result indicating a business corner case, or raise an exception in case of programming error, a bug.
- The saveData function just saves the data, send messages etc, and can fail also for technical reasons.
It is also really interesting from a transactional perspective, since no external side effects are produced before the saveData call. It is easy to put it in a transaction and rollback in case of failure.
Most functional language have a way to return business corner cases gently from the business function. We use the Either monad in Haskell or the Result type in F#:
1: 2: 3:
You can read Scott Wlaschin's excellent post on the subject: Railway oriented programming
By implementing a functional core, we can easily differentiate errors, adapt error handling strategy in each part fetchData/business/saveData, have a clear transaction strategy.
The code is also easier to test since Pure function have very deterministic behaviors, can be run in parallel and easily fit with Property Based Testing. And getting rid of Fakes and Mocks is always a win.
This done, the business function can easily be run in different environments by placing it between different versions of fetchData/saveData.
One of the best scenario for this is Azure Functions / AWS lambdas or any serverless environments.
This architectures already implement the data in/data out shell. Just add some code to feed function input to the business function, and transform output slightly before returning it.
You can watch my talk on the subject at NewCrafts Paris 2017: Lambdas, the Architectural Way to Serverless
One usual question with this pattern is:
What if I need business rules to know what data to fetch ?
The answer is simple:
Do it in several steps.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12:
The code with error handling/reporting should look like
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20:
More about the this pattern in Part 2
union case BusinessInput.BusinessInput: BusinessInput
type BusinessInput = | BusinessInput
union case BusinessOutput.BusinessOutput: BusinessOutput
type BusinessOutput = | BusinessOutput
| Ok of 'a
| Error of 'e