The idea of decomposing application into layers (logical vertical split) or tiers (physical vertical split) isn't new - it's bread'n'butter of systems architecture. Reasons for such a split are still the same (as they were in 90s, 80s & beyond):
- to reduce complexity & separate concerns
- to scale tiers independently
- to enable / simplify usage of different tech suited for each layer / tier
No-brainer, right? Good.
Problem appears when you consider the boundary between layers / tiers as a boundary between separate systems / applications. Developed & maintained:
- by separate teams
- with separate backlogs, ...
- ... separate priorities, ...
- ... separate product (service) owners, etc.
What happens is - upper tier is very heavily dependant on lower tier, because pretty much every scenario is a spike (goes through all the layers):
- this has a very serious impact on quality assurance of upper tier (all errors not detected in QA of lower tiers will cumulate there)
- downstream propagation of all changes in lower tiers is absolutely crucial & even if done properly, such changes can quite likely impair the development of upper tier
- lower tiers may have a big problem with business ownership, due to limited visibility (as products)
- as the access to lower tiers is made mainly via upper tiers, testing it may be a challenge
X-layer / x-tier comm is not an interface (or API)
"Wait, in multi-tier scenario the upper tier just calls lower tier using some sort of communication standard: REST, WebService, MOM, etc. The contract is agreed, data structures are confirmed - doesn't it sound like a typical on-line interface?"
In general, there's some truth in this statement:
- such an x-tier contract is a form of abstraction that hides the implementation behind itself
- it's also dedicated for the sole purpose of fullfiling upper tier's needs - if there's another 'client', it can easily get another contract
- actual usage pattern is very simple & interface-like: you know who's the caller, who's provide the 'service', so it's also quite easy to set up the (theoretical) responsibility
However, there are some differences as well:
- there's completely different velocity of change - when there's a functionality to get developed, in most cases all parts (upper tier, lower tier & the contract) get modified
- granularity of operations is different - they are very atomic & vivid: this kind of contract gets changed pretty much on daily basis - I don't mean just data structure changes, but also behavior coupling changes - the expectations on the functionality behind the contract that impact the functionality in the upper tier(s)
- in the usual cases, when two modules (microservices, separate applications) depend on each other heavily (each action / service calls actions / services of other application / module) it means that we have a problem with coupling. In this particular case it's not high coupling, but rather a high cohesion -> these tiers BELONG together (domain-wise): so you have to leverage that (developer together, test together, deploy together) instead of forcing the decoupling
The lesser evil(s)
Sometimes your reality is just broken & the split is already there: tiers belong to different teams, org units or even companies (sic!). To make it inconvenient:
- set very clear responsibility areas
- establish & mantain the communication high-way
- make these people cooperate as closely as possible
- use feature toggling for desynced workstreams
- regardless of local test automation, set automated testing for the communication itself
- distinguish subset of upper tier automated tests that HAVE to run within lower tier CI loop (cover that in responsibility areas)
But these are just workarounds & anesthetics. In the end there's just one solution to the problem:
MERGE. TEAMS; MIX. SKILLS.
... to make sure that the full spike will be fully covered within one, interdisciplinary, cross-functional team.
- different skills required
- different technologies used
- different composition (a bit of re-factoring won't kill ya, right?)
This will help you to:
- enable separate (& independent) workstreams
- limit the number of unnecessary dependencies between teams
- set proper boundaries between actual subdomains (easy to abstract as products), not within them