We’ve moved from a world where most products were single large monoliths to one where there are a whole swarm of microservices within Bounded Contexts, all potentially trying to communicate with each other. Initial REST communication architectures solved some problems but also introduced many others. With the addition of Event Driven Architecture, many of those shortcomings have been addressed. This combination of communication methods has come together as a Trilateral API architecture.
How communication worked without a Trilateral API
As our monolith was decomposed into multiple bounded contexts, those contexts needed to communicate with each other. One easy model was to add REST endpoints which are internally accessible, thereby preventing neighboring bounded contexts from directly accessing remote databases. Adding all these REST endpoints solves some problems but then introduce others:
- Internal services often need new endpoints to perform elevated capabilities (i.e deletions) which should not be exposed to customers. Merging these two endpoint capabilities adds considerable code and maintenance complication. Internal services may need polling capabilities to be aware of any data which changes within a bounded context.
- Example: A user changed their email address. Another bounded context builds its own local store of the customer information. It would be unaware of the changes unless it performed continual change polling.
- These new endpoints needed just for internal communication have an added different, and potentially more complicated, security AuthN/AuthZ requirements than customer facing endpoints.
- When one bounded context needs to link to another context, there are often latency and availability concerns. If context A needs to talk to context B which needs to talk to context C to fulfil one request, there is a latency and availability chain. Any outages or delays in any context can compound and result in an unacceptable customer experience. Added complex circuit breaker and service feature degradation logic needs to be added to all pathways in the code base.
- When two bounded contexts interact, services in the target context need to be scaled to be performant and available to support the synchronous burst needs of every calling services — many of which could all happen at the same time. Any degradation of any link in the complex chain could again cause serious performance issues.
The Trilateral API
With a Trilateral API and using Event Driven Architecture (EDA), we can get around many of these issues. I first read about the concept of the Trilateral API from Architecting Cloud Native Applications.
With a Trilateral API, for a given service within a bounded context, we have 3 main interfaces:
- REST Interface used for general external services (often customer-facing) making explicit synchronous requests.
- Event consumption interface to read from queues with information the bounded context is interested in.
- Event production interface to publish information to queues which other outside services may be interested in.
Services within the bounded context access their shared persistent storage backend (database, kv, etc)
This architecture solves many of the design changes we saw from earlier:
- No (or at least minimal) internal endpoints need to be created. Most REST endpoints are for external services (e.g. React interfaces) which don’t have their own data representation and are unable to consume from the event queues. Polling by external services would no longer be needed since events would be published to event queues which interested services are able to listen to. External bounded contexts don’t need to have command and control REST endpoints since those commands can be events consumed from event queues.
- There is no latency or availability coupling since information travels between the bounded contexts via asynchronous event queues. If the producing bounded context is down or slow, interested services just don’t receive new data until recovered. If the consuming bounded context is down or slow, events are buffered in the event queues.
- Producing and consuming services within a bounded context are scaled based upon the volume of data they need to process and the speed at which they can process it.
Redox has moved many microservices to using this general model as we’ve moved to decompose our core monolith. We use Kafka/MSK for the event bus and have multiple services producing and consuming from the topics. We do certainly have a bunch of legacy services still using the legacy REST model but are moving away from that. Because many of our services all need the same data from one main bounded context, we’re moving to abstract away much of the topic based consumption and production to a DistributedCache.