Musing #138: Service First Design
An application programming interface (API) is a means to an end. Consumers of APIs ultimately care about what actions they can perform and the output they can obtain to support their needs. Good API design ensures that the API's methods and output match the mental model of the consumers within the domain as well as accomodate the common usage patterns of the consumers.
I will clarify that I am referring to the *primary API of an application, not API design for client libraries to an existing interface.*
Until recently, my natural instinct when thinking about designing a new API was to start coding something in my favorite language. Hypocritically, when I read about a new project that solves one of my needs and I discover it is written in a language I am not familiar with, I typically close the tab.
Designing an API in a programming language immediately limits the audience that can or are willing to use it.
However, it is rarely the language itself that is the concern. All languages follow certain idioms and have various constraints that influences an API's design. Most languages require a runtime to be installed in order to execute code written in the language. Runtimes may behave slightly differently across platforms. The language's community may even have a cultural impact on how the API is developed over time.
Futhermore, your favorite language may not be anyone elses. Anyone who is not you or one of the core developers of the API are consumers. I've heard this response many times from my manager:
Oh, you want to use language X for this project? The rest of the team doesn't know language X. Either use a language they all know or make sure you set aside time to teach the rest of the team the language.
The culmination of these observations led me to the following claim:
The primary API of an application should be a service accessible on the Web.
Why? The Web is the most ubiquitious platform and will likely always be in some form or another. For context, the concepts that converged into this musing include (not necessarily in chronological order):
- Jeff Bezos mandated for "service interfaces" at Amazon in 2002. AWS followed.
- Martin Folwer introduces the concept of microservices to replace traditional monolithic SOA approaches.
- Russ Miles extends the concept of microservices by designing for "antifragility"
- Vagrant and now Docker have completely changed how we think about "environments" by abstracting it away from the consumer. This has led to simpler and less fragile deployments making continous integration and deployment not only possible, but the new "norm".
- Analogously, the rapid rise of mobile devices has necessitated the mobile first movement in client-side Web development.
- The increase of Internet-ready devices such as mobile devices and "wearables" constitute the Internet of Things (IoT)
- Many startups build "mashup" applications) that depend almost entirely on other Web services and general cloud services.
- Webhooks are being used to create communication pipelines across services on the Web.
It is obvious that designing a service for the Web is advantageous due to the breadth and depth of potential consumers, but more importantly the Web uses a "language" (protocol) understood and spoken by all consumers. The "runtime" of the Web is built-in to these devices. The conventions and idioms are well understood and converging on architectures such as REST which relies on the HATEOAS constraint, HAL and URI Templates to ensure consumers don't hit a dead end.
VMs and containers now make it possible to develop a service in any programming language you, developer, want with any set of libraries, system services, etc. without having to expose any of it to the consumers. Although Docker containers certainly can run multiple processes, it is encouraged to only expose one of those as a service to consumers, either users or other containers.
The culmination of this idea can be translated into two practical steps:
- Develop containers for each of the systems or processes used by the service. This takes the microservices approach which makes it possible to scale individual proceses by running more of its containers.
- Develop a "consumer" container that exposes the Web API for the service. This container talks to the systems in the other containers, but should be completely stateless. This enables running multiple independent "consumer" containers in parallel to handle more traffic or to faciliate different environments without needing to coordinating data between them.
Anyone who has built a Web service will likely say, "um, yea.. that is how you architect a service normally". The difference is that since containers abstract away the host system and are cheap to run, there is no longer a requirement to deploy the service into "the cloud", a laptop will work just fine. This "run anywhere, scale everywhere" environment makes Web services less daunting to deploy and therefore more plausible as the primary API for your application.
This will certainly be an uncomfortable transition at first, however building consumer-facing applications in this day and age must be accessible on the Internet in some form or another. Might as well make it the first API you design for.