a site about software

Binary and Source Backwards Compatibility

It’s pretty easy to break binary and source compatibility simultaneously. Remove one parameter from a public method or delete a method entirely and no existing code that used the method will compile or run. But how can we break just binary or just source compatibility but not both? Let’s look at examples of each in Java.

This change is source compatible, meaning all source that compiled against the previous version of this code will still compile. That makes sense, because every Collection is an Iterable, so every existing reference to this method is still valid.

This change is not binary compatible though. If you swap out the old jar for the new one without recompiling, you’ll get an error at runtime like this:

The bytecode of the client application refers to this method very specifically. You can see the method signature in the error message, and the type of the parameter is part of the signature. The method com.motlin.Printer.printAllElements(Ljava/util/Collection;)V no longer exists. To use the new library, client code must be recompiled. That might not sound so bad, but transitive dependencies need to be recompiled as well. That means if the client application uses a second framework which depends on the first, the second framework will also need to be recompiled. In a large dependency graph, the extra work can add up.

Binary Compatibility

How can we break source compatibility while preserving binary compatibility? One way is to add a method to an interface. Let’s write a tiny library with one interface and one concrete implementation. Here’s the complete source of version 1.

Version 2 adds a second method to MyInterface called doSomethingElse(). That’s not a source compatible change. MyInterface is public, which means any client can write their own implementations of it. When clients upgrade, their implementations won’t implement doSomethingElse() yet so they’ll get a compiler error like this:

com.motlin.MyImpl is not abstract and does not override abstract method doSomethingElse() in com.motlin.MyInterface

The rules for binary compatibility are quite complex and you can read the rules elsewhere. The reason why this change is binary compatible is analogous to why the previous change wasn’t. A client application compiled against version 1 has references in the bytecode to the method doSomething() which is void and takes no parameters. That method is still there in version 2, so it will work, simple as that. Internally, doSomething() can call doSomethingElse() in version 2 and it would still be ok. The internal implementation of a method does not change its signature, so everything is still fine. Binary compatibility is very dynamic. It is determined at runtime and difficult to reason about earlier.