Functionally Composing Service Oriented Architecture
4 min read
How do you manage service oriented architecure (SOA) when your services are at differing levels of maturity?
We encountered that question at the CTO summit in Sydney.
It turns out, that we can use the lens architecture as model to understand this problem and what the possible solutions are:
The situation is that there are 3 microservices, each at differing levels of maturity. From the client's perspective, it just sees a single state machine with a single input and output. The entry service would have to interact with its dependent microservice, and that dependent microservice would have its own dependency. Assuming you've read the lens article, you would realise that services are to be conceived as "automaton closures".
One of the claimed advantages of microservices is that you can scale your development team by matching the modularisation of your services with the modularisation of your human resources. Let's imagine you have 3 teams, each team working on one of the microservices. You often meet the situation where a feature in the upstream service depends on a feature on the downstream service, or the downstream service has a change that causes incompatibilities in the upstream service (and subsequently causes cascading failures all the way up the chain). Without some form of centralised coordination (i.e. a very good project manager that understands the critical path method), feature deployment can quickly become unsynchronised. The progress of your teams can become equivalent to stop-and-go traffic, where teams has to wait for other teams to synchronise their features to make their dependency chains complete. Even if this architecture was built bottom-up with downstream services built first, and upstream services layered on top like an onion kernel, these problems would still arise when it comes time to add new features.
Those with experience in object oriented programming (OOP) would start to see some parallels between this microservice topology and the concepts within unit-tests, mocking, dependency injection and inversion of control. One might suggest, to mock your dependencies. The downstream service writes to an interface, the middlestream service mocks the interface, the upstream service mocks the middlestream service, and all is well. But there is a problem with this method: mocking stateful networked services are difficult and almost akin to implementing the service. Mocks often become more and more complex the more realistic you want the service to behave, and the more comprehensive your unit tests have to be. To fully cover the functionality of your service, your unit test needs to integrate the closured dependent services, but this ends up meeting the same feature synchronisation problem as before.
This is a problem anyone will meet when attempting to do distributed object composition software development, whether that is a single monolithic OOP software, or in a distributed system. Regardless of mocking or unit tests or interfaces, it all comes down to proper critical path planning.
But there is a way to make this infrastructure easier to code, and easier to scale, and more in line with the original microservice philosophies. Instead of using OOP principles of object composition, can we instead change our perspective into functional composition?
What we want to do, is to see if we can decompose our nested closures by focusing on separating our concerns, make our software more cohesive, and design a pipeline that does the same functionality.
This has several effects:
- There's no need to mock your upstream or downstream services, all you need to do is simulate your input interface.
Make sure your service acts like a pure signal function properly with a
W -> W
value over time type. - Your architecture is more comprehensible and easier to reuse. (However there's nothing stopping object composed automatons to be shared/reused in other automaton closures, it's just more elegant when it's functionally composed).
- Your automatons can recurse if you want, just make sure to set a base case or else you're gonna get a run-away automaton.
- For anything more complicated than just transformations on primitive data, use the monad abstraction in this system. That being said, this is probably quite difficult until you have the Matrix Forge Architect language to help build those abstractions.
In summary, there's a common advice given in OOP: Composition over Inheritance. While this is true for OOP languages, in terms of SOA: One should instead try to first connect (functionally compose), before layering ( object composition), before inheriting (wrapping existing services), before modifying (writing the service code).