API Design is a Craft… and You Don't Have It!

Stephen Connolly's picture

This is somewhat of a rant post. It happens some times ;-) I am told they are more fun to read though!

There are some skills that people think they have, but actually don’t.

API design is one of those skills. Crafting a great API is very difficult to do. Having tried to do this several times I can say I am not great… but that is just the product of learning:

Learning

When you start off learning something, you can only see a small distance away from what is outside of what you know, you don’t know much, so you don’t see much more to learn. You feel that maybe you know 50% of what there is left to learn… but as you learn more you start to realise that there is still more out there… and the surface area gets bigger and bigger. To move your knowledge out further requires an ever increasing amount of learning.

Therefore, the first time you design an API you think: This is easy…

The second time you design an API you think: This is a tad tricky… 

And it just gets harder as time goes on…

Aside
Unless you are a pure software architect, who gets to set out the designs and never has to live with the consequences of those bad designs… then you permanently think API design is easy… but that is a topic for another post

Here are some things I have learned about API design.

  • Try to abstract away the specifics and imagine what could come next - it’s ok if you get this wrong, but what is important is to ensure that your API is flexible enough to handle change.

  • If you are designing a client-server API, try to keep it agnostic of the client… you don’t know what the client will be

  • Backwards compatibility is king.

One, not completely sucky, API I was responsible for designing thought me a few valuable lessons.

The story of it’s development is partially relevant.

When I joined my previous employers, there was an API for controlling conference calls. Sadly it was a case where the API was designed for one specific conferencing bridge and support for other bridges was bolted on. Then through company mergers, a completely different conferencing bridge had a completely separate implementation written, and then a “let’s rewrite from scratch” bridge got it’s own new implementation. But fundamental to the whole API were certain key concepts which in the new VoIP world just were no longer true.

Through various re-orgs, I got landed with supporting this API. As any developer tasked with supporting a legacy code base eventually complains:

That code is a pile of shite and should be thrown out and rewritten in a completely different way from scratch! 

Now, you never imagine that management is actually going to listen to you saying this (my former colleagues, or anyone who knows me may prefer to replace the word “saying” with “shouting” but that is just a volume control mismatch) but, shock horror, they actually did.

I was given the task of re-imaging a conference control API for the new VoIP world… we even got to hire some new people to help with the development and also the fact that lots of customers were using the old API.

Aside
So I gave Bill the task I didn’t want to do, namely writing the adapter layer to provide backwards compatibility for users of the old API. His first task was to write tests against the old API such that those tests did everything strange that he could think of with the API and verified its current behaviour (did I mention that the old API had very little tests, what little we had would only pass against a very specific conference bridge with some specific configuration which was lost during a re-org). Once Graham and I had the API mostly locked down then Bill got the fun task of re-implementing the existing API using our new API until all his tests passed again. 
Backwards compatibility is that important 

One of the issues with the old API was that it bundled log4j classes in the .jar. Cue classpath hell if you were using log4j in your client application. Cue logging framework hell if you wanted to use a different logging implementation. Ordinarily, in Java I would just use slf4j, but of course we had an Architect’s directive from on-high that mandated all new development to conform to an accessible logging format. [Now there are good reasons, if you are a multi-national telecoms company, for accessibility of logging. You need INFO and above logging to be i18n and l10n aware and you need all your products logging to get forwarded to a centralised system. So I am not criticising this specific edict from on high]. But we didn’t just have internal users, this API would be used by customers… who, from my supporting the old API, have different logging requirements from our own internal users… So of course we had to write our own version of slf4j… but in so doing, I learned a key take-home:

When you are designing a Client library for an API, keep the external dependencies to an absolute minimum.

This is important because you do not know where the client library is going to be used. If your code has a hard dependency on log4j and the customer does not want log4j, you are giving them pain. If your code has a hard dependency on log4j and the customer is using a forked log4j implementation (the internal customers we had) which has some different method signatures, you are giving yourself pain.

This does not only apply to logging frameworks… your client library accesses a HTTP service, so you use Apache HttpClient, or you use Netty or you use AsyncHttpClient… that’s fine and dandy… but you are forcing classpath hell if there is an overlap with a different version of those same libraries in the customers application.

There is only really one third party dependency library that I have seen do evolution right (Jackson 1.x to 2.x) where the Java package name was changed when they made a breaking API change (and the Maven coordinates were changed too… very sensible actually) That allows the two versions to co-exist peacfully. OSGi is supposed to be a solution for this.. but you should not assume that the customers of your API are using an OSGi container.

This is not just restricted to Java… you have the same issue in any language… Have you ever had a conflict in required versions of jQuery (why do you think jQuery has the noConflict method?)… and that lovely AMD and CommonJS set of specifications are just a sort of OSGi for JavaScript.

So when you are writing your client library, ensure that the tree of third party dependencies is as small as absolutely possible. In the case of that conferencing API we were able to ensure there were no external dependencies. But things are not always that easy. Take for example a CloudBees client access library which uses Apache Commons HTTP Client 3.1 and XStream 1.4.1 and Jettison. We hit problems ourselves trying to integrate this into Jenkins because Jenkins uses different versions of some of these libraries, so we need to use the Maven Shade plugin to create a version where these hard dependencies are shaded into a different package such that it will peacefully co-exist with the different versions that are available in the many different releases of Jenkins.

If your client library uses bigger trees of dependencies I am going to want to have you shot. Some of the more problematic dependencies to use are those which wire up with XML descriptors (making shading harder)

Aside

I’ve already tipped my hat to the great Java war that is logging frameworks and http client libraries… can I get the third Java war in… shall we see?

Don’t get me started on the JavaEE vs Spring war. If your client library requires dependencies from either side of that war, you are not my friend. You are effectively alienating everyone. You are running the risk of forward-backwards incompatibilities with anyone who is on your side of that war and using your client library unless they are willing to tie their ship to your blessed version… and what happens when they have a different client library to integrate… and people on the other side are, at best, facing the bloat of a thousand JARs in their application, or at worst cannot even use your client library.

And this brings me to my final point for today:

You almost never want to write a client library, you want to have your API exposed via a REST like API and let experts in each language write their own mapping of your exposed API into that languages metaphors.

If your API is a REST API using standard technologies, it should be trivially easy for customers to use their existing favourite HTTP client library to connect to your API and, oh look, all that pain is gone.

If your API is a REST API using standard technologies, it is language agnostic, each language can map the API concepts into their own native language constructs, and people can have a nice fluent and intuitive native API

If your API is a REST API using standard technologies and your customers are complaining that it is too hard to write clients, then that indicates that you have designed your API wrong… because it should be easy… if it’s not easy… you are doing it wrong!

And here is my humble take-home message:

I have designed quite a few APIs and I now consider myself barely competent at API design (having started out thinking I was a f*cking genius).

If you think you are good at API design you are only fooling yourself.

—Stephen Connolly
CloudBees
www.cloudbees.com

Stephen Connolly has nearly 20 years experience in software development. He is involved in a number of open source projects, including Jenkins. Stephen was one of the first non-Sun committers to the Jenkins project and developed the weather icons. Stephen lives in Dublin, Ireland - where the weather icons are particularly useful. Follow Stephen on Twitter and on his blog.

Comments

"Known knowns, known unknowns and unknown unknowns" Or "shit you know", "shit you know you don't know", "shit don't know you don't know" -- learning is about reducing the last one.

Add new comment