Use-case Driven API Design (part 2)

Preface

In the previous post I talked about the importance of good design when making a published API. I ended by giving four ‘C’s of a good API: complete, compact, consistent and clear. Here I elaborate on them one by one.

Complete

An API design is complete if it allows customers to write client code that supports all use cases allowed by the business. This is a dynamic situation. With time the business may change their policy and allow new use cases against the same domain objects. Or it may introduce new domain object types. In either case a once complete API may suddenly become incomplete.

It would be a mistake to anticipate the business and to allow clients to do things that are not allowed by the business just in case it is allowed in the future. This type of thinking can lead to vulnerabilities and loopholes. In the worst case it can even lead to data corruption as business invariants may not be protected. For this reason it is best to start with an agreed set of business capabilities that can be developed into use cases and the API can be designed to support those.

Adding behavior or new types to a domain should not break existing client code. Breaking changes, though rare, can happen and the smart architect is prepared for these. This can be demonstrated with a concrete example.

Example. The Department of Motor Vehicles API may initially allow customers to manage their home address information with public APIs, but later decide that this business capability should no longer be supported. It would be much better for this to appear as a deprecation of an API rather than a reduction in the scope of a much broader API.

Now if the initial design had allowed the client code to supply a new customer info object (UpdateDriverInfo which included the customer’s address), this API would end up being clunky after the business change. The implementation would require new business logic (did the address change?) and failure modes. These changes would trigger changes to the documentation as well. While any breaking change is hard on client developers, adding failure modes to an existing API is worse since it entails extra work on the part of the client.

A much better initial design would be an API that changed only the customer address (UpdateDriverAddress) as this could simply be deprecated in one release and eliminated in another. It would also be easier for client developers to see the effects of the deprecation.

You may be wondering if the logical continuation of this reasoning is to have separate APIs for the customer’s street address, city, zip code, etc. Your intuition will tell you that this is wrong, but there is also a solid principle against such granularity. Go back to the business and ask them: would you ever allow the customer to change their city without changing the rest of their address? They will tell you that the likelihood of a customer moving from one city to another but keeping the same street address is so small that it is effectively zero. Some things tend to change together and can be treated as a single “value”. This also leads to a more compact set of APIs, which leads to our second consideration for good API design.

Compact

There are two divergent schools of thought regarding the design of an API. A humane interface provides many convenience methods. It leads to shorter client code and a more fluid program structure. A minimal interface is just as complete but with a minimum of redundancy. Client code has one clear approach that tends to be longer.

When designing a published API, it is good to keep in mind that changes are very risky once the API is published. A client may be using the API and removing it may cause the client code to break. As a result, API methods are often deprecated and not removed (Java 9 removed APIs that had been deprecated since Java 2 and the repercussions will last almost as long). For this reason it is better to design a minimal interface and set a very high bar for adding convenience methods in future releases.

In reality risk mitigation through compactness is advice squarely aimed at framework and library API sets where business constraints are non-existent or negligible (the business has no business changing the definition of a natural logarithm). For business objects, whose API are primarily defined by business needs and must preserve business invariants, compactness is not as high a priority.

Instead business domain APIs should be first factored along the lines drawn by use cases which allow mutation. Business data that is needed to support command use cases should be encapsulated within a sub-domain. Then within that sub-domain compactness can play a secondary role in the design of the APIs.

Example. In the library checkout service the business may decide that a checkout is allowed only if

  • the borrower has less than \$10.00 in fines
  • the borrower has fewer than 25 items out already
  • a copy of the item is not already being held by the borrower

An analysis of the use cases would show that the fines are primarily managed by another subdomain (library accounts). But the number and particulars of the held items belong to the same service that manages checkout. So for this reason a single API that returns the collection of all items held by a particular borrower would be both compact and appropriate. The same subdomain would also expose an API that allows a new item to be added the list (BorrowItem). Adding an item would be accomplished transactionally, so that two checkouts done at the same time from different clients would not both succeed if they would put that borrower over the 25 item limit or if they both attempted to add the same item.

Consistent

  • Naming.
  • Uniformity of behavior.
  • Error handling.
  • Event publishing.
  • Documentation.

Clear (aka Intuitive)

Closely related to paper testing. Proportional to how close your solution is to the true model. Warning not to overestimate your own intuition (confirmation bias).

Conclusion

In the third post in this series I will delve into the design process, continuing to use the on-line library service example.