Context, problem, and forces
Cloud-native, Reactive systems are composed of bounded isolated components, which provide proper bulkheads to enable the components to be responsive, resilient, and elastic. All inter-component communication is accomplished via asynchronous event streaming and components leverage materialized views to cache upstream data locally. To increase availability for an increasingly mobile user base, we leverage an offline-first database to store data on devices and synchronize with the cloud. The boundary components leverage an API gateway to expose a synchronous interface, which is used to interact with a user interface. Cloud-native systems typically have multiple user bases with different user experiences, delivered over multiple channels and to multiple device types.
Our overarching objective with cloud-native is to empower self-sufficient, full-stack teams to rapidly and continuously deliver innovation with confidence. To achieve this goal, we must ensure we have proper bulkheads between system components. However, with so many user experience, channel, and device permutations, it can be difficult to strike the right balance with regard to the composition of backend components needed to support the frontends. The classic solution is to create a generic backend, which attempts to support all the permutations. However, this solution tends to implode, as the logic becomes tangled, as a single team endeavors to support all the requirements of all the other teams. The single component becomes the bottleneck to all advancement and a single point of failure, as there are no functional bulkheads between all the competing requirements.
At the other end of the spectrum, we create a separate backend component for each and every permutation. This can quickly become unmanageable as the number of permutations increases. It also tends to create a level of duplication that is hard to justify, even though duplication is not necessarily a bad thing; just as reuse is not always a good thing either. Reuse is synonymous with coupling, which limits flexibility and creates single points of failure. The generic backend component is the extreme example of the fallacies of reuse. Somewhere in the middle of the spectrum, we create smaller shared components, which each support a specific feature, and then we wrap them in fewer frontend-specific backend component layers. These layers reintroduce the synchronous inter-component communication that we aim to eliminate and reestablish coupling between teams. It is a difficult balancing act and over and over again I hear teams say they would lean the opposite way on the spectrum if they had the opportunity for a redo.