What is an idempotent API

Design APIs for microservices

  • 12 minutes to read

A good API design is an important aspect of a microservices architecture, as all data exchange between services is handled either via messages or via API calls. APIs need to be designed efficiently to avoid too much I / O. Because services are designed by independent teams, APIs must have well-defined semantics and versioning schemes so that other services are not affected by updates.

It is important to distinguish between two types of APIs:

  • Public APIs called by client applications
  • Back-end APIs used for communication between services

These two use cases have different requirements. A public API must be compatible with client applications. These are usually browser applications or native mobile applications. In most cases, the public API does this using REST over HTTP. With the back-end APIs, however, there is a need to consider network performance. Depending on the granularity of your services, the communication between services can lead to a high volume of data traffic on the network. Services can be quickly I / O linked. This makes aspects like serialization speed and payload size even more important. Popular alternatives to using REST over HTTP are gRPC, Apache Avro, and Apache Thrift. These protocols support binary serialization and are generally more efficient than HTTP.

Considerations

Here are a few things to consider when implementing an API.

REST or RPC: Balance the pros and cons of a REST-based interface against the pros and cons of an RPC-based interface.

  • REST models resources, which may naturally express your domain model. REST defines a uniform interface based on HTTP verbs, which benefits evolvability. The semantics are clearly defined in terms of idempotency, side effects and response codes. In addition, REST enforces stateless communication, which improves scalability.

  • With RPC, the focus is more on processes and commands. The similarity between RPC interfaces and local method calls can lead you to develop APIs with excessive communication. However, this does not mean that the use of RPC necessarily leads to excessive communication. The interface just needs to be carefully designed.

With a RESTful interface, the choice usually falls on REST over HTTP with JSON. Several popular frameworks such as gRPC, Apache Avro, and Apache Thrift are available for an RPC-based interface.

Efficiency. The efficiency depends on the speed, the memory and the payload size. A gRPC-based interface is usually faster than REST over HTTP.

Interface Definition Language (IDL): An IDL is used to define the methods, parameters and return values ​​of an API. You can use an IDL to generate client code, serialization code, and the API documentation. IDLs can also be used by API testing tools such as Postman. Frameworks like gRPC, Avro and Thrift define their own IDL specifications. While REST over HTTP does not have a standard IDL format, a common option is OpenAPI (formerly Swagger). You can also create an HTTP REST API without a formal definition language, but you lose the benefits of code generation and testing.

Serialization: How are objects serialized in transit? Text-based formats (mainly JSON) and binary formats such as log buffers are available. Binary formats are generally faster than text-based formats. However, JSON has advantages in terms of interoperability as most languages ​​and frameworks support JSON serialization. Some serialization formats require a fixed schema and some require a schema definition file to be compiled. If so, you'll need to incorporate this step into your build process.

Framework and language support: HTTP is supported in almost every framework and in almost every language. gRPC, Avro, and Thrift each have libraries for C ++, C #, Java, and Python. Thrift and gRPC also support Go.

Compatibility and Interoperability: If you choose a protocol like gRPC, you may need a protocol translation layer between the public API and the back end. This function can be taken over by a gateway. When using a service network, consider which protocols are compatible with the service network. For example, Linkerd has built-in support for HTTP, Thrift, and gRPC.

Basically, it is best to use REST over HTTP - unless you need the performance advantages of a binary protocol. No special libraries are required for REST over HTTP. The coupling is also limited, since callers do not need a client stub to communicate with the service. A wide range of tools is available to support schema definition, testing, and monitoring of RESTful HTTP endpoints. Also, HTTP is compatible with browser clients, so there is no need for a protocol translation layer between the client and the back end.

However, if you decide to use REST over HTTP, you should make sure at an early stage of the development process by means of performance and load tests that this variant is suitable for your scenario.

RESTful API design

There are many resources available for designing RESTful APIs. A small selection:

Here are a few things to keep in mind.

  • Look for APIs that expose internal implementation details or simply reflect an internal database schema. The API should model the domain. It's a contract between services and ideally should only change when new features are added - not because you redesigned code or normalized a database table.

  • Different types of clients (such as mobile applications and desktop web browsers) may require different payload sizes or interaction patterns. If necessary, use the Backends for Front Ends pattern to create separate backends for each client that provides an optimal interface for each client.

  • If necessary, make processes with side effects idempotent and implement them as PUT methods. This enables safe repetitions and can improve resilience. This problem is described in more detail in the article on communication between services.

  • HTTP methods can have asynchronous semantics so that while the method returns an immediate response, the service does the operation asynchronously. In this case, the method should return an HTTP 202 response code. This code indicates that the request was accepted for processing, but processing is not yet complete. For more information, see Asynchronous Request-Response Pattern.

Mapping REST to DDD patterns

Patterns such as entity, aggregate, and value object are used to apply certain restrictions to the objects in your domain model. In many DDD discussions, the patterns are modeled using object-oriented language concepts such as constructors or getter / setter elements for properties. An example: Objects of value must be immutable. In an object-oriented programming language, this is enforced by assigning the values ​​in the constructor and making the properties read-only:

Such programming methods are especially important when creating a traditional monolithic application. If the code base is large, the object may be used by many subsystems. Therefore, it is important for the object to enforce the correct behavior.

Another example is the repository pattern, which ensures that other parts of the application do not read or write directly to the data store:

However, in a microservices architecture, services do not share the same code base and do not share data stores. Instead, they communicate using APIs. An example: Suppose the “Scheduler” service requests information about a drone from the “Drone” service. The drone service has a model of a drone expressed by code. However, this is not available to the planning service. Instead, a presentation of the drone entity - returned - for example as a JSON object in an HTTP response.

The planning service cannot change the internal models of the drone service or write code to the data storage of the drone service. This means that the code that implements the drone service has a more compact available surface compared to a conventional monolithic application. If the drone service defines a class of type “Location” - the scope of that class is limited so that the class cannot be used directly by any other service.

For these reasons, in this guide we will not go into any further programming methods that relate to the DDD tactical patterns. However, it turns out that you can model many of the DDD patterns using REST APIs as well.

Example:

  • Aggregates can be created naturally resources map in REST. For example, the delivery aggregate is made available as a resource through the delivery API.

  • Aggregates are consistency limits. Aggregate operations should never cause an aggregate to be in an inconsistent state. Therefore, you should not create APIs that a client can use to change the internal state of an aggregate. Instead, use undifferentiated APIs that expose aggregates as resources.

  • Entities have unique identities. In REST, resources have unique identifiers in the form of URLs. Create resource URLs that correspond to an entity's domain identity. The assignment of URLs to domain identities does not have to be transparent to the client.

  • Subordinate entities of an aggregate can be reached via navigation via the root entity. If you adhere to the HATEOAS principles, subordinate entities can be reached via links in the representation of the superordinate entity.

  • Since value objects are immutable, the entire value object is replaced for updating. In REST, updates are implemented via PUT or PATCH requests.

  • A repository allows clients to query objects in a collection and add or remove objects from a collection, abstracting the details of the underlying data store. In REST, a collection can be a stand-alone resource - with methods for querying the collection or adding new entities.

When designing your APIs, keep in mind that they express not just the data within the model, but the domain model itself, as well as the business processes and constraints on the data.

DDD conceptREST equivalentexample
AggregateResource
IdentityUrl
Child EntitiesLeft
Update of value objectsPUT or PATCH
Repositorycollection

API versioning

An API is a contract between a service and the clients or consumers of that service. If an API changes, there is a risk that clients that depend on the API will no longer work. It doesn't matter whether these clients are external clients or other microservices. It is therefore advisable to make API changes as rarely as possible. Changes to the underlying implementation often do not require a change to the API. At some point, however, you may want to add new features or functionality that require changing an existing API.

API changes should be backward compatible whenever possible. For example, avoid removing a field from a model as this can cause clients expecting that field to stop working. Adding a field does not have a negative impact on compatibility, as clients should generally ignore fields that they do not understand in a response. However, the service must handle the case that the new field is bypassed in a request by an older client.

Support versioning in your API contract. When you introduce a fundamental API change, combine this with the introduction of a new API version. Keep supporting the previous version and let clients choose which version to access. This can be achieved in a number of ways. For example, you can simply make both versions available in the same service. Another option is to run two versions of the service in parallel and route requests to one of the versions based on HTTP routing rules.

The diagram consists of two parts. Service supports two versions shows the v1 client and the v2 client both pointing to a service. Side-by-side deployment shows the v1 client pointing to a v1 service and the v2 client pointing to a v2 service.

The support of multiple versions is associated with additional development time, additional tests and additional effort during operation. It is therefore advisable to take old versions out of service as soon as possible. For internal APIs, the team responsible for the API can work with other teams to help them migrate to the new version. A cross-team governance process is helpful here. With external (public) APIs, retirement of an API version can be more difficult - especially if the API is used by third parties or by native client applications.

When a service implementation changes, it is helpful to mark the change with a version. The version provides important information for error handling. Knowing exactly which version of the service has been accessed can be extremely helpful in root cause analysis. For service versions, consider using semantic versioning. The semantic versioning uses the format MAIN VERSION.MIDAL VERSION.PATCH. However, clients should only select an API based on the major version number - or, if necessary, based on the minor version, provided that significant changes have been made between minor versions, but these must not lead to an impairment. In other words, it makes sense for clients to choose between version 1 and version 2 of an API. The choice of version 2.1.3, however, does not make sense. With such granularity, you run the risk of having to support more and more versions.

For more information on API versioning, see Versioning a RESTful Web API.

Idempotent processes

A process is idempotentif it can be called multiple times without any side effects after the first call. Idempotency can be a useful resilience strategy because an upstream service can easily invoke a task multiple times. A description of this can be found under Distributed Transactions.

The HTTP specification states that GET, PUT and DELETE methods must be idempotent. POST methods do not guarantee idempotency. In general, when a POST method creates a new resource it is not guaranteed to be idempotent. In the specification, idempotency is defined as follows:

A request method is considered “idempotent” if the intended impact of multiple identical requests using this method on the server is the same as that of a single request of this type. (RFC 7231)

It is important to understand the difference between PUT and POST semantics when creating a new entity. In both cases, the client sends a representation of an entity in the request body. But the meaning of the URI is different.

  • For a POST method, the URI represents a higher-level resource of the new entity, e.g. a collection. For example, to create a new delivery, the URI could be. The server creates the entity and assigns it as a new URI, e.g.. This URI is returned in the address header of the response. Every time the client sends a request, the server creates a new entity with a new URI.

  • For a PUT method, the URI identifies the entity. If an entity with this URI already exists, the server replaces the existing entity with the version in the request. If there is no entity with this URI, the server will create it.Suppose the client sends a PUT request to. It is assumed that there is no delivery with this URI and the server creates a new delivery. Then when the client resends the same request, the server replaces the existing entity.

The implementation of the PUT method by the delivery service is specified here.

Most of the requests are expected to create a new entity. So the method optimistically calls on the repository object and then handles any duplicate resource exceptions by updating the resource instead.

Next Steps

In the next article, you will learn how to use an API gateway on the border between client applications and microservices.