1. Introduction
In this tutorial, we’re going to explore what APIs and ABIs are concerning our software, what they mean to other consumers and us, and what we need to consider whenever we change them.
2. What Is an API?
An API, or “Application Programming Interface”, defines how one piece of software can interact with another.
This might be between two different applications interacting over the Internet. For example, an HTTP API might be documented using OpenAPI, which allows other applications to interact with it consistently. This would then define the HTTP messages sent to the server and the responses that can be expected to be received.
Alternatively, software libraries also have APIs. These are a description of how applications can interact with the library. For example, the classes and methods that make up the accessible interface of the library are considered to be the API. This then defines how applications – or, indeed, other libraries – can interact with it correctly.
We have APIs at all levels of our application, and not always where we might expect to see them. For example, it’s relatively apparent that our database drivers expose an API that we use to communicate with the database.
However, our database schema is also an API – it describes what we can and can’t put into our database and ensures that the data remains consistent and valid. Changes to the schema will necessitate changes to the software that accesses it just as much as changes to the drivers would.
Our APIs needn’t always be synchronous either. Messages put onto a message queue or the contents of files sent via email can constitute an API just as much as function definitions in our code. These should all be considered, and the consequences of changing them weighed up as they can have unexpected side effects in our application or in applications that depend on them.
3. What Is an ABI?
An ABI, or “Application Binary Interface” is analogous to an API but expressed in compiled code instead of source code.
In a Java application, for example, the API of a library would be the library’s source code, and the ABI would be the class files that the application works in terms of.
Often, the ABI of a piece of software is directly linked to the API. This is not surprising since the ABI is simply the API but expressed in compiled form instead. However, the exact nature of the two means that sometimes changes to one do not cause changes to the other.
For example, type erasure in Java means that specific changes aren’t expressed in the compiled class files. The following two function definitions look different in the source code but are expressed identically in the class files, for example:
public List<String> getStrings() {}
public List<Integer> getInts() {}
In both cases, the actual class file will be:
public List getStrings() {}
public List getInts() {}
So we can see that changing the generic type of our return value is an important detail in the API, but isn’t reflected in the ABI at all.
Conversely, it is possible to have changes that affect the ABI without changing the API at all. This is a much rarer occurrence but still can happen if we’re not careful. One key example is compiling the same source code with a different compiler. If we build the same class into Java 8 or Java 17 bytecode, then the ABIs are different, even though the input API was identical.
3.1. APIs Without ABIs
Note that not all APIs will have a corresponding ABI. For example, any purely interpreted language such as JavaScript or Python will not have any compiled output to consider as the ABI.
Equally, not all ABIs are obvious. An API expressed over HTTP will have an ABI – the literal bytes that are transmitted between machines – but this is often not considered an important detail when describing how to interact with the remote system.
In this case, the exact details are often hidden from us because we use other software layers to perform our interactions. We don’t need to care about TCP/IP packets or SSL encryption when we make a call to a remote system. We depend on the API of our HTTP client, and it does all of that for us. This means that our remote service’s exact ABI is less important to our application.
4. API and ABI Compatibility
When the API or ABI of any dependent software changes, we might have to change our software to account for this. The changes we need to make will depend on what has changed in the dependent software.
If the ABI of a dependent library either hasn’t changed or changes in only inconsequential ways, then we will not even need to recompile our application to work with the changes. The ABI staying the same means that the changes can be used as a direct replacement.
If the ABI of a dependent library has changed, but the API hasn’t, then we’ll need to recompile our application without making any changes to it. The API staying the same means that the changes are functionally the same, but the ABI being different means that some low-level details have changed – for example, the exact memory layout of some structures or the exact values of some constants.
If the API has changed in a critical way, we will have to go further and change our software to work with the new API. For example, if a method has had a parameter added or removed.
4.1. Semantic Versioning
In order for consumers of our software to understand the consequences of our changes, it’s important that we have a consistent versioning strategy. Semantic Versioning is one such strategy that has become very popular.
Note that, as discussed above, we might have to version our API and ABI independently of each other. However, it’s often enough to version the API only and allow consumers to infer the ABI compatibility.
When using semantic versioning, our version numbers have three key components – major, minor, and patch:
If the major version changes, this indicates an incompatible change to the API or ABI. This means that the consumer will have to change their code to move up to this new version.
For example, removing a method, or adding or removing parameters to a method would count as a significant version change. Any code that depends on this method will no longer compile or work and must be changed.
If the major version is unchanged but the minor version changes, this indicates that there are functional changes but not backward incompatible. This typically means that new functionality has been added to the API but that existing functionality has not changed. Often, though not always, this would mean that the ABI has changed, but the API is fully compatible with the old version.
For example, adding a new optional field to a bean would count as a minor version change. Code depending on it will not need to be changed, but the layout of the bean in memory has changed, so the code might need to be recompiled to continue working.
If only the patch version changes, then this indicates that the API is unchanged. This typically means that there is a slight change in the implementation of the API, but not to the API itself. This probably means that the ABI is also unchanged, though not always.
For example, relaxing the rules around which fields can be null might count as a patch change. Nothing in either the API or ABI has changed, and all old code will continue to function the same without any changes necessary.
5. Conclusion
Here we’ve seen a brief overview of what constitutes an API and an ABI and when we need to think about changes to them – either changes that we’ve made ourselves or changes in libraries or applications that we depend on.
We’ve also looked at how we can express these changes to other people through a sensible versioning strategy.