Sometimes during domain modeling you come across something that isn’t a thing. These operations that don’t quite belong to an object are called services. Services often live in a separate, service layer. The service layer lies between controllers and models, defining an application’s interface, its API.
Designing with services and a service layer is popular in the Java J2EE/Spring community. However, it’s not common in the Rails world. Is this because of the Ruby community’s general backlash against the complexity of Java? Or has the rise of HTTP-based JSON APIs made this architecture obsolete? To answer these questions, let’s take a deeper look at services and the benefits of a service layer.
What is a Service?
In Domain-Driven Design, Evans defines a service as an operation offered as an interface that stands alone in the model. In other words, a service is an action, not a thing. And instead of forcing the operation into an existing object, we should encapsulate it in a separate, stateless service.
It’s not always clear what logic constitutes a service. Taken to the extreme, domain models could be stripped entirely of their domain logic, and turned into nothing but simple data structures (an AnemicDomainModel). A good heuristic is to look for large, domain objects doing too much work and try to break their multiple responsibilities into separate services. At other times it may be your own intuition telling you that some logic just doesn’t feel to be in the right place. To get started, it’s helpful to classify services into different types.
Types of Services
Evans defines three types of services:
An application service is a service that provides non-infrastructure related operations that wouldn’t come up when discussing the domain model outside the context of software, i.e., in its natural environment.
Exporting account transactions as a CSV file in a banking app is an example of an application service because a file format, CSV, has no meaning in the domain of banking.
Application services improve the cohesiveness of your domain model by preventing “software” implementation details from leaking into it.
A domain service scripts a use-case involving multiple domain objects. Forcing this logic into a domain object is awkward because these use-cases often involve rules outside the responsibility of any single object.
Continuing with the banking app, a funds transfer would be an example of a domain service. Transferring funds doesn’t quite fit in either of the involved account objects, or perhaps a customer object, so it becomes a standalone service.
An alternative implementation, could be an instance method on
Account, taking the amount and destination account as parameters. However, transferring money between accounts feels like the responsibility of another object, not a core responsibility of an account. Domain services like this one really help maintain the core responsibilities of your domain objects.
Another example of a domain service is a sitewide search across multiple domain models. Since it searches across multiple domain models it doesn’t conceptually fit in any one particular model. Instead, it should live in a separate search service.
Simple CRUD operations are not domain services. For example, I would consider the following an anti-pattern:
Sure, this does make the
UserController independent of the domain model, but this type of flexibility is premature. Instead, the controller should interact directly with the domain model when creating a new
User. Domain services should be short scripts, not simple 1-liners that delegate to an almost identical domain model interface.
An infrastructure service encapsulates access to an external system. Infrastructure services are often used by application and domain services.
E-mailing and message queuing are two examples of infrastructure logic that has no place in a domain model.
Infrastructure services make your domain model more reusable by separating out non-domain, “software” concerns.
What is a Service Layer?
In PoEAA, Stafford defines a service layer as an application’s boundary and set of available operations from the perspective of interfacing client layers. In other words, it’s your application’s API.
Let’s take a look at some of the benefits of a service layer to see if they are useful in the Rails world.
Supporting Multiple Clients
In most web apps, the API mainly consists of simple CRUD use-cases. Encapsulating your application’s API, allows it to be reused across multiple client controllers, for example, an HTML and a Thrift client. The resulting controllers are thinner, and more cohesive.
I see this reuse across multiple clients as less and less of a benefit because of the popularity of HTTP-based JSON APIs. Most apps will typically have an HTML client and a few additional clients, for example, an iPhone and a console app, but the same controllers will serve both HTML and JSON. In a way, the controllers have become the services because all clients talk to the app over HTTP.
This reuse was more valuable when you could have non-HTTP based clients, for example, an Adobe Flash app using Hessian.
Encapsulating Application Logic
Stafford separates business logic into domain and application logic. Domain logic being purely the problem domain. Application logic dealing with technical responsibilities, for example, coordinating a workflow or sending an email.
In order to build a more cohesive and reusable domain model, application logic should be handled by infrastructure services, possibly in a service layer. Putting application logic into a domain model that’s used by multiple clients may have unwanted side effects. For example, if you want HTML clients to be emailed after user registration but don’t want your JSON API clients to be notified at all. So instead of emailing users from the domain model…
…let your controller ask a service to do it.
The Rails community definitely has gone back and forth on where to put application logic. Creating a service layer for it is too much though. Instead, put it the appropriate
respond_to block in your controllers.
Applying Cross-Cutting Concerns
Java web apps using the Spring Framework, commonly use services to apply cross-cutting concerns, for example, transaction management and security.
Transaction management is a common justification for a service layer, specifically transaction management spanning multiple transactional resources. For example, in the Spring Framework, it’s possible to atomically write to a database, send an email, and enqueue a message.
In a Rails app,
ActiveRecord provides automatic, and if necessary manual, transaction management. I can’t say I’ve ever thought much about transaction management across multiple transactional resources, but I see no reason for it not to belong in a controller. I’m not entirely sure why Spring discouraged transaction management at the controller level. Maybe for easier testing.
When Should I Use A Service Layer
The only reason to use a service layer is if you have to support multiple clients using different protocols. HTTP and JSON have become the de facto client communication protocol and format, eliminating the need for a service layer. The additional indirection and flexibility of a service layer should only be considered when you have to add a client that doesn’t speak HTTP. However, it’s still beneficial to use Evans-like services from your controllers in order to encapsulate domain, application, and infrastructure logic and preserve the cohesiveness of your domain model. Always remember to only build for today, not what could happen tomorrow.