How to design good APIs

If you haven’t already read Why to Design a Good API and Characteristics of a well designed API I recommend doing so. As discussed in the article, APIs are essentially permanent and should not be modified once distributed. You mostly have one chance to get it right.

When designing an API, functionality and readability must be the top priorities. Object names should be self-explanatory, avoiding abbreviations, and striving for visual symmetry to enhance readability and ease of use. It is crucial to keep the API footprint as small as possible while ensuring it meets all necessary requirements. Stripping it of essential features can compromise its functionality and readability.

In cases of uncertainty regarding the implementation strategy or functionality of any aspect of the API, it is better to drop the idea. It’s easier to add items later than to remove them after distribution. Emphasising quality over quantity in code and features is beneficial in the long run, as features can be added in future development cycles.

A clear definition of API features and implementation routines is essential to provide developers with the highest flexibility. While implementation routines may be included within the API, they should be well segregated. Information hiding should be a guiding principle; avoid needlessly declaring public objects unless highlighted in the specification. This approach reduces the possibility of bugs and allows for independent testing and debugging.

Comprehensive documentation is integral to a good API. Every aspect, including internal codes, should be well-documented, following platform-native standards. A well-designed API should not be bloated. Performance should be a primary focus, ensuring the API blends seamlessly with the platform and supports other products built around it. Good design inherently promotes good performance.

APIs should be designed to coexist with the platform and other products. Leveraging native runtimes and data types enhances performance, readability, and platform integration. Using platform-native generics, enumerations, and default arguments can simplify development and reduce bugs. Additionally, targeting popular development IDEs for the platform and utilising features like code completion, code snippets, and source versioning can enhance the development experience.

Flexibility should not be compromised unless explicitly required by the API specification. Avoid codes like finalisers and read-only properties that reduce flexibility.

Guidelines for Individual Stages of API Development

Reconnaissance

Setting realistic expectations for the development cycle is crucial, particularly for the first iteration. It’s important to adhere to the core characteristics of a good API, bearing in mind that you cannot please everyone — sometimes, it’s best to “displease everyone equally.”

Gather initial requirements with a flexible mindset, which allows for collecting extensive requirements that can later be refined into a more feasible specification sheet. Creating use cases or Jobs To Be Done (JTBDs) is a vital step in this process. Strive for generalisation and better abstraction of the specifications rather than detailed granularity; abstractions can be simplified later, but it’s challenging to abstract overly detailed implementations.

Develop the specification sheet in multiple stages, starting with a preliminary version. Seek input from developers, users, testers, and usability experts to create concise, adaptable specifications. Begin coding as soon as you complete the second level of the specification sheet. Focus on creating abstract classes, object hierarchies, and basic method designs even if the full specification isn’t ready. Additionally, write intermediate test codes and have them reviewed by usability analysts.

Designing Classes, Class Hierarchy, and Interfaces

When designing the class hierarchy, base it on real-world models and only when it makes sense. Avoid creating subclasses merely for grouping or categorisation; instead, create subclasses with methods exhibiting nearly similar behaviour. Minimising the mutability of objects within a class is crucial as it makes them thread-safe, simple, and reusable. Refer to resources such as the JavaRanch article on immutability to understand its benefits.

Ensure your classes have a small, well-defined state space, regardless of spatial or temporal complexity. Avoid using mutable methods like getLocation() and currentPosition(). Replace them with properties that have appropriate getter and setter methods. Internally, ensure that users can directly work with property results without worrying about performance penalties. When retrieving property values, return a shallow copy of an internal variable without performing any calculations, enabling users to repeatedly use property values without instantiating a local variable.

Designing Methods, Properties, and Access Variables

Avoid designs where users must repeatedly use the same intermediate codes to achieve results. Encapsulate such codes within the API to reduce unnecessary bugs and make the end-developer code more readable. For example:

// bad example
for(int i = 0; i < 360; i++) myAPI.drawEllipsePoint(i);
// good example
myAPI.drawEllipseArc(int startingPoint, int endingPoint);;

Name methods with verbs, considering the end-developer’s perspective rather than the API’s perspective. For example:

// bad example
boolean myAPITimer.stopped();

// good example
myAPITimer.stopTicking();

When overloading methods, avoid having two overloaded methods with the same number of parameters. If unavoidable, ensure that the overloaded functions exhibit similar behaviour. Specify method parameter/argument data types clearly to help users understand the method’s nature without reading the documentation. This also converts runtime errors to compilation errors, which are easier to debug. For example:

// bad example
string myAPI.gErr(string state);

// good example
ErrorStateResult myAPI.getErrorState(ErrorState currentErrorState);

For parameters requiring fixed states or a finite set of values, use enumerations and constants. Long parameter lists are less readable, so aim for three or fewer parameters. If a long list is necessary, consider breaking the method into two or more methods. Alternatively, use a helper class to generate a list of parameters if the API design restricts partitioning of methods.

End-developers should not need to handle exceptions for any return value from your API. For example, a method returning the length of a string should return 0 when the string is empty rather than null, avoiding the need for null checks. Avoid using low-precision variables unless required by the API specification, as the spatial overhead is negligible compared to the usability loss for end-developers.

When errors occur, raise or throw them immediately rather than waiting for an execution endpoint to raise a cumulative error. Do not suppress exceptions or fail silently. When throwing exceptions, be as verbose as possible, including an error reference number and a detailed error capture message.


These guidelines are informed by the expertise of experienced developers. All credit goes to those who have shared their knowledge and educated me.

Leave a comment