Collaborating Services by Functional Composition

Small streams make large rivers

Do you have existing services that need to collaborate but currently do not, or are you considering breaking down a monolithic system? Perhaps you are starting a new project and aim to set it up correctly from the outset. Regardless of your situation, several essential questions need answers:

  • How should the functionality be divided among these services?

  • What strategies can be employed to minimize and manage the impact of changes on client synchronization with evolving services?

  • How can development efforts and time be minimized to launch functionality sooner?

  • What methods can be implemented to rectify errors in decision-making effectively?

In this “discussion”, we will focus on addressing the practical aspects related to the decisions made to tackle these questions.

Your services will provide APIs that various clients will utilize. Nowadays, it's common for each service to be accessed by multiple client types, such as web applications, mobile apps on different platforms, and other backend services, including automated processes. Moreover, you may expose these services, along with their APIs, to your customers, enabling better automation and integration with their systems. Each of these client categories might need tailored versions to suit their specific needs. Managing all these diverse clients, many of which are beyond direct control, can be challenging. Given this context, let's proceed to address our questions.

Who should see what?

Let's initiate our thought process by considering the approach where each service directly exposes its API to clients. We’ll skip basic API gateways or fundamental network infrastructure, such as load balancers. Say we begin with RESTful/HATEOAS APIs. These can easily manage the distribution of load to dedicated services if they employ full link URLs in responses, referring to content in other services. However, challenges may arise.

  • Should authentication and authorization be standardized?

  • Should every client be granted direct access to every service?

  • Should clients adjust their behaviour based on the endpoint address or media type?

  • How does one update all the clients when services are refactored (split or merged)?

Some attempted to tackle those challenges by creating "umbrella" gateways that "orchestrate" underlying services. While this umbrella can shield clients from internal service modifications, it essentially functions as a monolithic service, demanding ongoing maintenance. To accommodate the diverse requirements of numerous clients, the umbrella would either have to directly cater to each use case (resulting in considerable complexity) or be adaptable and responsive to clients’ needs (which also presents complexity).

Suppose those underlying services evolve and expose new functionality. It won’t immediately be available to the clients until the umbrella system is fully developed. Creating and managing this umbrella demands expertise that matches or exceeds that needed for individual services.

Consistency?

Should services be uniform and consistent in fundamental aspects, eliminating the need for data transformation between one service's response and another service's request? The "shared nothing" approach implies that clients must translate between services, whether these services are integrated into a single umbrella system or not. This might not be a big issue if there is only one client. However, when dealing with multiple clients, it results in duplicated efforts, requiring the reimplementation of basic access to all underlying services while managing the necessary logic to connect them for each client type and the "shared nothing" concept becomes problematic.

Establishing basic access and common connections between services is essential. Creating consistency in authentication, authorization, data types, structures, and links across all services need not impede progress; instead, it can simplify and streamline everyone's work.

Performance & Efficiency

How many network interactions will complex, diverse clients need to make to complete a use case? We have to consider the network bandwidth, sizing and locations for each service they interact with. If they are only a few, and if the clients are physically close (in networking terms) to the services this may not sound like a problem. Yet, even in such cases, the fragmentation of requests hinders the possibility of optimizing efficiency by bringing processing closer to or into the service(s). How could we optimize this, in theory? Perhaps:

  • Not wasting time on what isn’t needed.

  • Doing everything that is needed while there, not requiring extra interactions.

  • Ability to do the above for multiple services in the same trip.

Truly RESTful designs that adhere to an "all or nothing" approach pose challenges unless we redefine a "resource" to be precisely what is required for each specific use case, as known during the development phase. It’s not that simple any more, as discussed in my earlier posts.

In response, some implemented ways for client to specify what is to be excluded from a default state representation. As clients cannot exclude data they are unaware of that comes in newer versions, such data has to be excluded by default. This mixture of defaults yields convoluted combinations of both "include this" and "exclude that" that only increase the complexity.

The ablest empower clients to dynamically combine multiple requests within a single trip at runtime, avoiding the assumption that they know about every possible combination during the service's development phase. Would you do this? How expensive would it be to develop and maintain? What format/representation would you use for this “batching”? Must contained operations be entirely independent or can they depend on each other’s output to avoid more network trips? In most scenarios where batching could assist ordinary clients, these operations tend to be "dependent." Let’s consider this some more…

Perspective

Let’s try to describe what we want:

The clients should see all the services. They should not be affected by (re)factoring of those services as long as they are there. Instead of having to transform data between service calls, we want the outputs of services to be directly usable as inputs of other services. We don’t want to be forced to come again for related processing that can easily be done while we’re already there. In fact, clients should be able to request all needed processing, not just one and not just pre-imagined combinations thereof.

Let’s now rephrase that with a new word:

The clients should see all the functions of all services. They should not be affected by (re)factoring of functions of those services as long as they are there. Instead of having to transform data between service calls, we want the results of functions to be directly usable as arguments of other functions. We don’t want to be forced to come again for related functions that can easily be done while we’re already there. In fact, clients should be able to request all needed functions, not just one and not just pre-imagined combinations thereof.

Does this ring a bell? Does it sound a little bit like functional programming? What if we make our API functional, top to bottom – everything is a function, including access to the smallest piece of data? We can clearly specify each function, its inputs and outputs, while putting some effort into data/type consistency.

With such an approach clients could pass any number of functional expressions to a service for “evaluation”. Such a service does not necessarily need to be able to implement or even understand all the details of any requested function – all it needs to know is who to call if it doesn’t directly support it. Generic “umbrella” services become very much possible and feasible. They just need to be able to discover which service supports which functions. As this discovery process can be made dynamic, any new function exposed by any service becomes available as soon as it is deployed, requiring no coding effort for the umbrella service.

Too bad this doesn’t exist, right? I mean, creating a brand new standard, community and tools around it takes a lot of time, effort and investment. It’s not like this hasn’t been tried.

Well…

What if I told you this already exists?

Animated system topology diagrams showing increasing coupling complexity over time as the number of component grows until the final slide. It ends with a single piece, a new form of orchestrator, replacing many pieces itself.

Yep. It does. And it is very popular, very powerful, with a huge community and countless tools for more than a fair share of programming languages. And, yes, there are multiple generic “umbrella” gateways implementations for you to pick, both open source and commercial. According to Google Trends the approach is significantly trendier than RESTful or others.

Even if you know about it you might not be aware of what I’m thinking of as the name implies something completely different and sounds (completely needlessly) scary to some. The name does not reflect what it is. The terms in its specification do not match the terms in this document. Yet it is almost a perfect match. I’m talking about GraphQL and will be writing more about it and surrounding confusion.

If you are in a rush to find your next umbrella gateway, look at the following (in alphabetical order):

  • Apollo GraphQL Federation, with “Router” being the newer product, “Gateway” being the older.

  • Bramble GraphQL Federation Gateway

  • The Guild’s Mesh

  • Nautilus Gateway

  • Tyk API Gateway

I apologize if I missed any. I do not mean to give advantage to any.

 

 

Previous
Previous

”Divide & Conquer” in Software Development

Next
Next

REST API (3): Tug of War