0

Context

I have a multi-tiered architecture that contains 2 presentation layers, one business layer and one data layer. See image below for a visualisation.

multitier architecture

Here you can see that there are two components (applications) inside the presentation layer: App 1 and App 2.

Currently, I deploy all of these layers 'monolithically', meaning all at once. I do this to ensure that the two components in the presentation layer are compatible with the right business layer: v1 of App 1 or App 2 is compatible with v1 of the business layer.

To make it a bit more complex: the entire business and data layer run on-premise, which means that my clients decide which version of the business layer they want to run. This works well for App 2, since this is also an on-premise application, but App 1 is hosted by us. Therefore, I made it so that the version of the business layer decides which App 1 version is being started. See a simple flow below of the current situation.

enter image description here

Problem & Question

I want to decouple App 1 from this 'monolithic' deploy process, so that I can deploy this app faster & more frequently. So, App 1 will have it's own deploy process. Meaning App 1 will have its own version number. My current situation will not work anymore for this component, since v3 of App 1 may probably work on v1 of the business layer. How to make sure that App 1 communicates with the right business layer?

Two important restrictions is that I cannot change the way how all the layers are deployed, I can only change this for App 1. Also, the business layer is not really backwards compatible as well.

My tries

I couldn't find literature about this problem. Maybe I can't google, or maybe this problem is too specific. I did came up with a solution. This solution is to migrate the on-premise business layer to self-hosted by using a new application that acts as a gateway (for old business logic) and rest API for new business logic. Only problem with this solution is that the on-premise business layer & data layer are also being updated. Those updates are not backwards compatible unfortunately. So once a column has been removed from this database, my newly created endpoint on the new self-hosted API will break. Down below is a design of this solution.

flow of how solution works

I am hoping that here somebody would like to provide their ideas. Maybe I should explore a certain technical term? Has anyone ever solved this problem (e.g. is there certain literature)?

2 Answers 2

0

Most of what you need to independently deploy App 1, you already have in place with your current mechanism to start the version that matches the on-prem back-end. It just needs some tweaking.

As a start, give the interface (API) between the business layer and App 1 a version number of its own that is updated independently of the business layer and app versions. The API version should only be updated when something has changed in the communication between App 1 and the business layer. For backwards compatibility, start this API version at the current version of the business layer (so, if you now have v3 of the business layer, the API should also start with v3).

Next, make App 1 declare which API version(s) it supports, in a way that this information can be used by the startup logic. If possible, make this update also to older versions of App 1 that are still in use to interact with older business layers.

Finally, update the startup logic to first try to retrieve the API version and if that doesn't work the version of the business logic. Then start/redirect to the newest version of App 1 that has declared it can handle the API version of the business logic.

This assumes that when a new API version gets released, first the App 1 is deployed and the business layer after that, but that assumption was effectively already present in your current setup.

3
  • Allow me to restate what you said to validate my understandings. I do this using an example. To give more context: App 1 is a web-app and the business layer is an API that accepts requests using HTTP. The startup logic of the web app will basically contain/fetch a 'table' where two columns are present: API_VERSION and WEB_APP_VERSION. Here, one API_VERSION can contain multiple WEB_APP_VERSION entries. We fetch the latest compatible WEB_APP_VERSION of the API_VERSION our customer is using. Based on that version, the url will look like https://example.com/web_app/{WEB_APP_VERSION}.
    – Jeff
    May 4, 2023 at 9:45
  • If I look at this example, it looks like I do not need an intermediate layer at all. Is this new API between the business layer and App 1 necessary? You said this API should change when something has changed in the communication, what do you mean by that?
    – Jeff
    May 4, 2023 at 9:45
  • @Jeff, The API is not a new layer. It is a name that is typically used to refer to the set of HTTP requests that your business layer understands. The separation between API version and business layer version is to make it possible that you can do, for example, a reorganization of the data layer without affecting App 1. May 4, 2023 at 11:21
1

Given the little info we have, I find that you are already on a good path. Just rename it to API Gateway, and you will find literature to keep you busy for the next decade. You can think of it as a Proxy too. In both cases, we would be speaking about an intermediate layer capable of:

  • Handling outbound and inbound traffic to/from your versioned business layer
  • Transform (adapt) requests and responses
  • Routing requests
  • Version control
  • Authorization

So, How would it work?

1st step: The business layer (its corresponding API) is published and versioned through the API Gateway. There should not be other ways to reach the business. To this end, we have to enforce strict access policies. E.g. at the network level.

2nd step Each (deployed) client must be registered in the API Gateway so we can correlate clients with APIs. This correlation will be checked continuously by the API Gateway in every single request. For simplicity, let's issue an API Key per client. The API Key identifies all the APIs the client is allowed to consume.

3rd step Provide the app client with the API Key (or bearer token such as a JWT). Most likely via config file, or fetching the key during the CI/CD before the deployment.

4th step Developing a configurable SDK or client. Given an API Key (or a bearer token), the client can set which business layer and version is allowed to work with. I have mentioned JWT for this reason, we could use scopes and the payload to this end.

5th step It consists of adding the API client to the client app as a dependency.

Backwards compatibility between clients and services can be managed from the API Gateway. For example:

  • Checking the client's version so the gateway can redirect requests to compatible APIs
  • Checking the client's version so the gateway can adapt the requests to fulfil new contracts. E.g. formats, schemes, types, missing data, etc.
  • Minimum compatible version assertion so the gateway can deny access if the client's version is not within the API's compatibility range.

Note that I'm assuming the front and the back are decoupled. If both were monolithic, the solution would change. Dramatically. But still possible by implementing 4th and 5th. In both cases, the API client is key.

Those updates are not backwards compatible, unfortunately. So once a column has been removed from this database, my newly created endpoint on the new self-hosted API will break.

Few things to bear in mind.

Backward compatibility is also a discipline. In such a discipline, you can add columns/tables to an existing table/database, but you MUST not remove existing ones. Yes, it's a mess because deprecated columns/tables and values may live forever. The solution is enforcing clients' updates or strangling deprecated business layers to death.

So once a column has been removed from this database, my newly created endpoint on the new self-hosted API will break.

This must be tested continuously by automated tests. E.g. by test doubles or contract testing. Integrations (no matter which) must be backed by a failover strategy, so you can handle "gracefully" breaking changes in production. For example, if your integration test doesn't pass, you disable the integration (say via feature flag) and cast a denial service error.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.