How to Avoid Baking the Service Layer Into Your API

Today, Application Programming Interfaces (APIs) are associated with REST, but APIs have been around for decades. Programmers have been writing APIs since the ancient world of EJB, Messaging, and SOAP, and now REST.

The main benefits of modern web APIs arise from decoupling the backend from the client service. So, how do we design quality APIs that achieve this goal? In this article, we’ll consider methods that help you bake great interfaces independent of the service layer.

What Is an Application Programming Interface?

A technical API is defined by the keyword Interface in most programming languages. An Interface is a well-defined contract that is implemented by a service for a consumer. The Interface enables a service to perform a repeatable task with a specific outcome as per the contract.

The job could be to create an invoice, adjust the temperature, or apply for a loan. Modern REST APIs productize this task, repackaging it as a capability for a consumer outside of your team. The characteristics are very similar to this technical Interface.

The What and the How

An Interface defines what part of the task to initiate, and the Service is the how. For example, the consumer of an invoicing interface doesn’t know or care about what goes on behind the scenes to send an invoice. They just know that if they invoke the API, the invoice will be sent. Likewise, proper API documentation should focus on defining the contract and leave out the how.

With modern APIs, this differentiation is even more critical, especially as nuanced platforms and interaction methods emerge. The same capability should be actionable from a click of a button from the browser, or executable by events, or even invoked by a modern device fitting into your lightbulb. To meet these new constraints, we must bake an API that retains Interface and Service independence.

How to Bake an API

There are various ways to keep this separation of what and how in a modern API. With an interface, it was easily achieved with the technology. Layered Architecture is the most common technique to define these layers of Service and Interface. Most frameworks like Spring, Angular, and Node.js have made this distinction very clear and provided separate classes for Controller and Service. This is a great baking recipe for an API solution.

But with the controller and service being in the same framework, sometimes the distinction is overlooked, and developers merge the service with the controller. Here are a few tips to remember how to keep the behavior where it belongs.

So, How Much of the Behaviour Goes in an API?

An API can be a front-end controller that receives the command from a user action or a REST Controller. These two are very basic API examples; different API styles and protocols will often support other system integration formats. For example, the consumer can be FTPing an invoice or POSTing a JSON request from a web application. An internal web application can be efficient with gRPC.

So, how much business logic should be in the controller?
Zero. None. Nothing.

The API is the facade. It should not contain the actual business logic of the task at hand. Instead, the real behavior should remain within the underlying Service class.

An API Layer is the best place for cross-cutting concerns. It thus should only allow authenticated consumers into the service and authorize said users and systems. The API layer adds the traffic monitoring, sets a rate limit, logs calls, and tracks and audits the request for compliance.

Another design decision involves orchestration. Do we hardwire orchestration capabilities into the API layer? This could include calling different services to provide a new unified interface that is an aggregate response. A wiser way to achieve this is to create a dedicated orchestration service.

Different design patterns can guide this behavior. For instance, use the Adapter pattern to create interface-specific adapters. The controller class can be an adapter to transform an HTTP request into the input meant for service, and likewise, a service activator will transform a message. An Adapter will also transform output or exception from service into appropriate status codes. The service does not need to understand the nuance of the HTTP layer or messaging protocols.

Another useful pattern to consider in the API layer is the Decorator Pattern. The controller or service activator can attach new behaviors of cross-cuttings concerns around the service. An API Gateway provides an excellent helping hand to add some of these concerns.

What Is the Behavior of the Service?

The actual task runs in a Service. The Service is your business domain, your core layer. For an invoicing API, the Service would have the core logic of sending an invoice.

An excellent way to separate the service from the controller is to code the service in the most native language of the tech stack, minus any dependency from your runtimes, such as the web or app container.

For example, if you are a Java developer writing an enterprise application using any framework at any layer, code the service methods in Core Java. Avoid any import of HTTPRequest. This means HTTP Request/Response Messages are unknown to Service class/methods. In the Spring Framework, a Service is annotated with @Service. The advantage is that the service can be unit-tested independently with minimal mocking.

The logic in the service must be complete, from validating the input, calculating based on these inputs, and accessing other backend services like persistence and notification. In a few frameworks, including Spring, validation on request is already invoked before the controller is reached, but that is only semantic of the request. The business validations cannot be done by any framework. Sometimes, due to this, validations can become mixed up. It probably won’t harm to repeat the validation performed by the framework in the service.

All this doesn’t make a service procedural. Run your imagination for any design pattern, and it can fit into the service. A service is seldom completed in one class.

A Service must be reusable, such that it can be picked up easily for system integration in any other API style. To verify this, try to think of changes that will be needed to expose this core logic that is available today over HTTP in a radically different protocol, such as a command-line interface. If you have to pick up functionality from both the Controller and Service class, that’s a sign the service is baked with the API Controller.

The Service takes care of the transaction boundary. It defines the unit of work. In the Spring Framework, for example, a transaction boundary can be defined by @Transactional. It is wise to start it at the service. The Service will then talk to the persistence layer or any other backend resource manager.

Based on these pointers, the service becomes independent of the API and testing, and development becomes easier without getting into the nitty-gritty of different APIs and protocols.

Here is an example of a RestController that Delegates to Service. The Rest controller is the thinnest class in the repo.

Should the Service Be Coded Alongside the Controller?

One of the excellent design discussions for a team starting an API project will be whether or not to have a modular architecture to separate the service and API modules. This means that the service is built as another module, wholly separated from API.

There are various ways to accomplish this. It could be as simple as creating another maven module within the same repo or having a completely separated layer. A separate layer gives you more flexibility in adding a different API style to this service. The service can be included in a batch processor, a gRPC service, a subscriber, a producer, a command-line, a web application, or an HTTP.

At the same time, consider whether this service demands all these interfaces? In any of the design choices, it is wise to respect the layered architecture, to set this modularity and separation of concerns even within a single source repo. This gives us a lot of flexibility to add more APIs to a service without duplicating the effort.

Separate The Service Layer

As Martin Fowler mentions in his book, Patterns of Enterprise Application Architecture:

“The benefit of Service Layer is that it defines a common set of application operations available to many kinds of clients, and it coordinates an application’s response in each operation. The response may involve application logic that needs to be transacted atomically across multiple transactional resources. Thus, in an application with more than one kind of client of its business logic, and complex responses in its use cases involving multiple transactional resources, it makes a lot of sense to include a Service Layer with container-managed transactions, even in an undistributed architecture.”

So, bake a good API, and keep the service separated!