Versioning APIs and Managing Code
Problem
We often face the issue like the integration was broken since there is a change on the other side of software or API. We want the software integrations to work regardless of changes in the interfaces. Backward compatibility is very crucial for every software should consider seriously. Backward compatibility is a property of an operating system, software, real-world product, or technology that allows for interoperability with an older legacy system, or with input designed for such a system.
Modifying a system in a way that does not allow backward compatibility is sometimes called "breaking" backward compatibility. Such breaking usually incurs various types of costs, such as switching cost. The software is considered stable when its API that is used to invoke functions is stable across different versions. Any customers do not want to integrate with a software that keep changing its interface.
With microservices architecture, APIs are the key integration points where other microservices or systems are integrated. In order to maintain consistent API and support backward compatibility, the code should be structured in a certain way it will be easy to maintain and follow clean code principle.
Maintaining code for backward compatibility
Maintaining the code for backward compatibility is complex and error prone. A developer often faces issue like, how to maintain the source code for different version of API in a version control system? There are many strategies such as, maintain the source code is separate repositories for each version, or follow Gitflow model, where the source code is maintained is a different branch for each version of API etc.
The continuous integration and continuous deployment and with the trunk-based development, where a source code is maintained in single branch and there will be always a single source of truth. There is a constant flow of improvement is made to the system by constantly committing the code to git to the same branch again and again. We will explore the options of maintaining the source code in a single branch and support for backward compatibility.Layered Architecture
To understand how to manage the source code in a single repo, it is also essential to understand the architecture we follow for a .Net solution. The layers allow us to manage the source code without affecting other layers and follow the principle of loosely coupled architecture. Each layer has their own models.
The architecture we
follow is based on domain driven design and the solution has the below layers,
- API
layer
- Domain
layer
- Infrastructure layer
The below diagram shows the three different layers and the components associated within it.
![]() |
Figure 1 - Layered Architecture |
Let us understand more on the layers,
API Layer
The API layer contains the integration logic with other APIs. This layer has common functions like, authentication authorization, error handling, routing, configurations, versioning and API documentation etc. These modules or features are common across the services. This is the layer where we handle versioning of our endpoints.
Domain Layer
The domain layer represents the business domains or domain models. This layer handles business logic intended for the specific service. It is unique for every service and encapsulates the business logic regardless of the integration with other systems.
Infrastructure Layer
The infrastructure
layer deals with the integration with downward systems. Like persisting data to
databases, integration with other APIs, integration with message queue and file
handling etc.
The Bigger Picture
To better understand the concept, let us assume we have an order service for order processing. We identify the initial model as ‘Order’ and versioned it as v1. It all good and deployed v1 to production. But after some time, a new business requirement suggests that we need to change our ‘Order’ model to include more properties and identify another model ‘OrderItem’ as well.
With the introduction of new changes, within our code we may end up changing in multiple places such as controllers, domain models etc.
We don’t want the versioning propagated throughout our solution. It will also become very difficult to maintain and becomes cumbersome. To avoid the complication, we want to keep the versioning to be handled only at the API Layer and transform the API model to Domain models.
The below diagram shows
how the request is propagated through each layer,
![]() |
Figure 2 - Solution architecture for versioning API models |
When a there is a system call for our api ‘/api/v1/order’ endpoint. The request invoke the v1 ‘OrderController’ housed in the ‘OrderService.Controller.v1’ Then we use the Automapper profiler ‘OrderService.Maps.v1.MappingProfile’. Same as v1, when we invoke ‘/api/v2/order’ endpoint, it is going to invoke v2 ‘OrderController’ that in turn call the ‘OrderService.Maps.v2.MappingProfile’. The mapping profile convert and transform the both the versions of API model into domain model.
The domain models can be mutated if there is any addition of member. The changes to the domain models are in complete control of a developer. The service can be compiled and deployed again. This approach will not affect any integrations that was done to previous versions.
Namespaces
Namespaces are used to organize the classes. It helps to control the scope of methods and classes in larger .Net programming projects. We use namespaces to segregate the controllers and models of different version.
Folder Structure
Folders are used to organize
the different versions of controllers and API models.
There two level of
folder structure are,
- Base
folder
- Version folder
The base folder maintains all the base classes that is common regardless the versions. These classes are not changed often. This will avoid duplicating the controllers or module in all versions.
The version folder organized the version specific controllers or models. Every time a new version is introduced a new folder is created such as v1, v2, etc.
API Models
API Models servers as interface for consumers. The API models are versioned when there is a change in the members or properties. All models that are identified as common, which does not change in future can be a shared among the versions. Changes to the API models without versioning can directly affects the consumer integration endpoints.
In our example the API models are defined in OrderService.APIModels namespace under OrderService project.
The common models are
placed at the base folder. For example, the ‘Address’ is defined as follows,
Under v1 folder our
Order model is shown below,
Under v2 folder the
‘Order’ model is changes as follows. Note, there is an addition of new field
‘Currency’.
Domain Models
The domain models represent business domain. Domain modes are in complete control of the developer. It can be mutated and deployed, which does not affect any of the consumer integration. Any strategies between the API versions can be handled within the domain models. It is important to keep the API models immutable. Once released we should not change the models in its lifetime.
In our sample example project the domain models are defined under the project ‘OrderService.Domain’
The unified domain
model is shown below,
Controllers
Each version of API has separate controllers. Controllers will transform the API models into domain models with the help Mapping Profile. Shared controllers is used by shared API models.
Shared
‘AddressController’ code is shown below,
Note, both the API version attribute is defined at the top.
The versioned
controllers for ‘OrderController’ are placed under respective version folders.
For example, ‘OrderController’ for v1 is shown below,
In a similar fashion
the ‘OrderController’ for version 2 is placed under v2 folder.
Mapping Profiles
Mapping profiles transform models using Automapper. It transforms input model or API model to domain model. Input model represents the API model which changes between the versions. Domain model not to be versioned and can be mutated. Data received from each version of API model is transformed into domain models, thus the domain models or the business logic will be same regardless of new version of API models.
The mapping profile
for v1 ‘Order’ to domain model can be found at ‘OrderService.Maps.V1’
namespace,
Automapper will take care of copying the member to member. Any member differs in name or type, or any conversion should handle explicitly.
Transformation Magic
Look at the Post method for both v1 and v2 ‘OrderController’s
V1 Post method,
V2 Post method,
You barely notice any change in the code except the attribute ‘MapToVersion’. All the magic happens within the mapping profile and the namespace declaration at the top of the code. Notice the namespace ‘OrderService.ApiModel.v1’ and ‘OrderService.ApiModel.v2’ declared at the top for both ‘OrderController’ respectively.
When the request is received the data is fetched from the corresponding ‘Order’ version model. The code _mapper.Map<Domain.Order>(order) will call the appropriate mapping profile.
Role of Command
Handlers
The command handler is where the business logic for a domain is handled. The handlers were all in complete control of the developer. The command handler does not change regardless of the change in ApiModels. The helps the keep the business logic to follow clean code.
In our example the command handler for processing order is placed at ‘OrderService.Application.Feature.Order.Create’ namespace.
The sample code is as
follows,
Summary
In this article I have
portrayed how to handle he versioning of API at the code level by following
clean code principle. However, API versioning can also manage at the deployment
level. Containerization is a way to create an immutable build and deploy independently
regardless of any change in input models, both the version of services can run
parallelly. The sample source code for the above can be
downloaded from https://github.com/arunvambur/manage-api-versions
Comments
Post a Comment