Distributed Architectures are a lot like neural networks; all services that talk to each other need to share the I/O in and in a way that they can synchronize that information on the fly. The way the brain does is that each neuron that communicates with another has the other neuron fire back a neurotransmitter to synchronize and improve that communication in the future thus creating a pattern.
While this behavior is almost identical to what is known as a webhook in the API world, we do not follow the same principles for API design in distributed architectures. Since the API Pattern designed in the 1970’s for centralized architectures and NOT distributed architectures, it was never intended to be used in this way and creates an architectural cross-cutting concern when used in distributed services.
Discovery of the Architectural Cross-Cutting Concern in API’s
I first came across this in 2013, when I was on a contract at Cisco/Apple. I was building a web app and had to make 5 API requests (one after another) to get the data I needed. The entire function took about 30 seconds and I thought to myself ‘why hasn’t anyone invented an API Monad so that one can chain all these calls using one request/response?’ That question led me down a rabbit hole of discovery.
In looking at others attempts to do something similar to this, I only saw hardcoded chains or dynamic attempts that never resolved the redirect. When doing hardcoded chains, you still have to do a request/response with each call thus defeating the purpose and even doing it dynamically, you have to solve for the ‘redirect issue’ or you will still have to do a REQUEST/RESPONSE with each call.
I knew that if I could merely replace the redirect with a forward, I could effectively chain the calls without having to do a redirect (see forward vs. redirect). But that would require an internal communication loop. So, I started to investigate this.
In researching and in attempting to create the monad myself, I discovered why no one had done this: The original API pattern has a fatal flaw wherein it binds the communication logic to business logic. This flaw makes it so that communication cannot be shared without duplication of the I/O state; this becomes obvious when you realize that files such as RAML, API Blueprint, OpenAPI, Swagger, etc. are all attempting to duplicate data found in the business logic.
This issue is what is known as a ‘cross-cutting concern‘ since the communication logic/data (which needs to be shared in the distributed architecture) is being bound to the business logic because of the API Pattern. And because the API Pattern will only cause it to occur in a distributed architecture, I dubbed it an ‘architectural cross-cutting concern’… specifically because it had to do with shared I/O across distributed services.
I realized that no one to date had realized the issue, had isolated it or was working on resolving it; everyone had simply tried to create workarounds rather than trying to solve the underlying problem. But these workarounds created additional problems with:
- synchronization
- shared state
- uptime
- scalability
- speed
- performance
- development time
- maintenance
- logging
- and MUCH more
The issue at hand was that the API pattern was a pattern designed for centralized architectures, as it didn’t share data associated I/O (the request/response) with other services in the network due to the binding created by the API pattern. When I pointed this out to the API Development Manager at Netflix (and showed him the solution) he stated: “This fixes everything we are having issues with!“
The OLD API Pattern
You see the original API pattern was designed in the 1970’s as a way to standardize input/output when calling a service/library. But since you are forcing the library/service to handle the input/output at the same location in the code where you handle the business logic, you are creating a binding of communication layer to business logic.
Now in a ‘centralized architecture’, you will never see an issue with this because you don’t need to share I/O with external services on your network. You have created one single monolithic application on one server; the request/response only goes to one server and then back out to the client.
But this binding becomes blatantly apparent in a distributed architecture where it becomes impossible for I/O data to be shared across the network without duplication or entanglement. This is what is known as a cross-cutting concern. However since this occurs ONLY in a distributed architecture, I coined the term an ‘architectural cross-cutting concern’ as echoed by my peers years later.
This can be seen in a variety of ways in the fact that if any endpoint data changes, it must be DUPLICATED to all other services that share it. Data such as:
- ROLES/Privileges
- Request Method for endpoint
- Required REQUEST data for endpoint for each ROLE
- Required RESPONSE data for endpoint for each ROLE
- All available Versions of endpoints
- API Docs
The current methodologies DO NOT SYNCHRONIZE this data (ie OpenAPI, RAML, API Blueprint, Swagger, etc.) in any way and rely on humans to synchronize all data. This is highly unreliable and ineffective and can cause higher privileges, unsynchronized endpoints and inefficient periods of downtime.
Abstracting the Communication Layer
The solution is what I call API Abstraction. To solve this inherent error in the pattern, one has to abstract the communication logic and data away from the business logic into a separate layer. Then synchronize the data using a ‘middle-out’ pattern; since the ‘central-version of truth’ for the data is going to be the service where the REQUEST/RESPONSE meet, we can ‘CACHE and PUSH’ state from this central point to all subscribing services in the architecture.
First, we have to abstract all communication from the business logic. To do this we have to understand how the communication logic/data are bound to the business logic for API’s. In most modern MVC frameworks, this is done via extending a class at the controller or through annotations like so:
@Secured(['ROLE_ADMIN', ‘ROLE_USER'])
@RequestMapping(value="/create", method=RequestMethod.POST)
@ResponseBody
public ModelAndView createAddress(){
List authorities = springSecurityService.getPrincipal().getAuthorities()
User user
if(authorities.contains(‘ROLE_ADMIN’)){
if(params.id){
user = User.get(params.id.toLong())
}else{
render(status:HttpServletResponse.SC_BAD_REQUEST)
}
}else{
if(authorities.contains(‘ROLE_USER’)){
user = User.get(principal.id)
}
}
Address address = new Address(params)
…
address.user = user
…
public ModelAndView createAddress(){
User user= (params.id)?User.get(params.id.toLong()): User.get(principal.id
Address address = new Address(params)
address.user = user
…
}
API Abstraction with the HandlerInterceptor
As you saw in the image above for the distributed architecture, the ‘IO State’ for the API Server has the communication logic bound down at the controller. But we need to have the communication logic/data somewhere closer to where the request/response is. In addition, it would be even better if they could hand off to each other.
In Java, we call this functionality a HandlerInterceptor. In Python, Ruby, and other languages, it is often called a Filter (though these are not always abstracted away from the controller). The HandlerInterceptor provides us with several different functions (of which the following are used):
- preHandle: The ‘preHandle’ intercepts the REQUEST before it goes to the Controller so that preprocessing can take place.
- postHandle: The ‘postHandle’ intercepts the RESPONSE as it returns from the Controller so that postprocessing can take place.
The API NeuroTransmitter
Bringing this all together, how do we synchronize everything in our architecture without duplication of data? Well now that we have abstracted the Communication Logic, we can easily build a sharable API Object-based upon data similar to RAML, API Blueprint, and others. I like to call this I/O State.
I/O State contains all data separate from functionality for the API endpoints that is directly related to the REQUEST/RESPONSE.
What this enables us to do is the following:
- Load these files at runtime via a master API Server and build objects into a local cache.
- Share these objects with all subscribing services via webhooks.
- Reload these files ‘on-the-fly’ without having to take services down and synchronize entire architecture.
Should the ‘master’ API server go down, nothing in the architecture is affected, as it will have the last known state…AND the server will come up with the last known state as well.
This enables all services in your API architecture to stay synchronized, be more secure, maintain a higher uptime, etc.
In Conclusion…
If you are using a distributed architecture, you will notice these issues increasingly as you scale and I have increasingly been giving these talks more and more. My advice is to move towards this model ASAP.
If you wish to see a working example, you can visit the open source BeAPI Framework where you can download the plugin and a demo implementation, which has been tested out on EC2, Raspberry Pi and numerous other instances.
Last updated: May 31, 2024