Data Services vs. Business Services
So, we all write software systems, and these systems use other systems, mostly exposed as services. All is simple and clear, until you wander what is the role of those services from the standpoint of our system (system under work). Or, does it really matter?
Oh, it does matter if your system does not have a real database of its own where it holds the state; instead, it transiently retrieves, manipulates, displays, and stores the data owned by other services. Such applications maybe are rare, but they set the need for distinguishing between different sorts (or families) of services. If we can classify the services by their role towards our systems, then we can as well define the patterns and strategies for working with them, as those are derived from the difference. And since many systems use many services, these patterns and strategies can be beneficial for all those systems. So, let's get to the point.
Is There a Difference?
Let's agree on terminology.
Service is a general term which refers to a set of capabilities exposed through some sort of protocol which the software can use to communicate with it. I treat database as a service, as well as WCF and RESTful services. Every physically separated deployment node is a service. For the context of this discussion, I will not be referring to the in-process Service Layer and its classes as services, although these can represent the physically deployed service nodes, which are services.
I know that most applications have databases, and most of them also interact with the outside services. I will not emphasize on the difference between the outside services for now (even though I will do that very soon). I will only ask to classify the difference between the database and the outside service. I'm sure that you consider them as different beasts. You work with them differently after all, but maybe you have not caught yourself on this particular thought yet.
I'll uncover the sins - you don't really care much about your database, while you do care about the outside service. You almost never try-catch the database queries and CRUD operations, while you [should] handle all errors coming from the outside service. Oh, don't worry, I'm not blaming you. I do the same thing, and furthermore - it's absolutely normal.
This sets the ground for discussing these two as services of their kind.
Data services are implicitly available, can hold anything we give to it, and they give it back without modifications, which means they contain minimal or no logic on top of storing the data. This is the reason that working with data services is usually straightforward. This is also the reason why they are so performant as compared to the regular services (I'll call them business services later) with some logic in them.
An obvious example of a data service is a database. Other examples include NoSQL databases, AWS endpoints that store JSON data, and any other services that give no promise other than to store the data you give to it (these can include WCF or RESTful services which follow the criteria).
When working with data services, we need to put the application (business) logic in the client code, since that's the only place left, as the service is a bare storage. Obviously, many engineers will start thinking now that such storage services force our applications to be slower with more logic in them, but they also make our applications more scalable. Here is how.
If your application owns the logic, then it can execute the logic, and can hand over the data to the storage at the end. Furthermore, results of the most storage operations (Create-Update-Delete) are completely predictable - no denial, no refusal, no validations, no logic - so why wait on them? This opens up a huge opportunity for asynchronous storage operations. e.g. you could put an insertion command on the queue and return a user's request immediately with the success code, because you know the insertion will always succeed; later on, a command handler running in a different process will pick the command and execute the actual insertion command against the storage service. This approach is highly scalable because you can span up more and more numbers of command handlers that will take the load off the client code (i.e. the actual application interacting with the user).
Data services should meet basic criteria to be considered as part of this family, specifically:
- Create, Update, and Delete operations should be guaranteed to succeed.
- This in the extreme means to be schemaless; or in the simplest form it may mean an ability to compensate the operation to make it successful, such as cascading deletions.
- Create, Update, and Delete operations should not have return values for scalability, or those should not matter.
- For Create operations it means that IDs for the rows should be generated by the client and passed to the storage, instead of generating after the insertion and then passing it back.
- Update operations may modify revision (version, timestamp) values, but those can be retrieved by ID later when trying to Update or Delete the record.
- No additional logic except storing the data; if present, then it should be predictable and stable at least.
- If the client code cannot guess (predict) the result of the storage operation, then there is a need to wait for the response from the storage service, which makes it not scalable.
- With minor logic in storage services, client code can go with duplicating this logic to predict the operation result. However, if the logic is not stable and keeps changing often, this may become a catch-up game for the client. In such case, I'd recommend to stop treating the service as being a data service, as it really is a business service (described next).
Business services are what people simply call "services". These usually contain logic that our application is interested in, replicating which is difficult or not practical. Most importantly, results of the calls into the business services are usually important, so the code calling into those should wait for the response and often act on it. e.g. if we process credit cards through a service, we want to ensure that the charge was successful before shipping a product from the inventory; so we would check the response for the success flag.
Because the results of the business services are important, many times our calls to those are synchronous, even if technically it can be called asynchronously. It is synchronous conceptually - we won't proceed with the next step of our logic if we don't know the service returned the success flag. We can use asynchronous invocation constructs if our frameworks provide it, but that would only be done to free up the waiting thread to handle other requests while waiting for the response of the currently invoked service endpoint. Once the response from the service is received, we will continue handling the user's request from where it was before invoking the service. So, in short, business services are conceptually synchronous.
Synchronous services are harder to scale than asynchronous services. Data services can be considered as being asynchronous (for Create, Update, and Delete part), so they are better scalable than business services.
We can try to scale the usage of a business service by putting the requests on the queue (similar to what I suggested for the data services), but this comes with one or more drawbacks:
- If calling a business service is part of the user's request processing flow, then we can't really tell the user anything about the final result of the whole processing. We can simply take the request, process it until it's within the logic we own, and then promise the user that it will be handled later on.
- We could imitate the result of a successful processing by telling the user our predicted end result, but then we have to duplicate the logic of the business service which we are just about to call. Then we are in the catch-up situation, and who knows if our logic is up to date at all. Worst of all, we can imitate a success and later on the result may turn out to be a failure. Is it worth the frustration for the client? I'd just recommend to tell the user that the request is being picked for further processing, as that's the most accurate information you have... but that only if you at all decide to go with this route.
- If calling a business service is not part of the user's request (e.g. it's part of the background processing) then we can afford to wait for the service call, and so why bother with the queue at all?
In general, conclusion is that the limit will always be the performance of the business service, no matter what we do. Once we have realized this important point, we need to focus on the SLA (Service Level Agreement) numbers provided by the service owners and just live with it. If we have a luxury to interact with the actual service developers, then we can ask to improve things a bit, maybe suggest caching behind their endpoints, or load balancing behind their DNS (we call a single endpoint, it dispatches the call to one of the hundred internal servers), or anything of that sort - all behind their walls. Anything that we try to control on our end will be some sort of duplication of their logic. Even as simple thing as caching on our side means to understand how often the data is getting refreshed on their side, and what we lose if we cache it for specific periods of time.
All this discussion really goes nowhere if we don't become conscious about the number of services we use or want to use. Is it a data service? great, just plan your business logic for this. Is it a business service? great, just ask for SLA. Is it something in between? try to tighten it or classify it as either one or the other. Don't go with something in between, as that will come with the cost of both kinds of services, which is the worst outcome of all - you will end up writing additional logic in your client since the service is not completely a business service, and you will still depend on their SLAs, since you can't predict the results of the service operations.
When We Own the Service
Finally, sometimes it's up to us what kind of service we will have, e.g. when we are creating a subsystem of our complex software system. So, we are writing a service which we are going to use in other parts of our system.
In situations like this, you need to clearly decide which route you want to go. And in simple words, you are just deciding where the logic goes. If the logic is within the service, then it's really a business service. If the logic goes out of the service, then it can be a data service.
But I wish the decision was always simple, otherwise we would always choose to have more data services, but that would really be insane with the amount of logic our clients would need to hold. So, naturally, we want to design self-contained, smart, reusable business services, and I'm with you. For such cases, design it as a microservice. I think the notion of a "microservice" comes with the same sorts of problems that I've outlined for the business services, it's just nobody has said that clearly so far (maybe because the term is comparably new).
When you design your service as a microservice, then you are taking ownership of all the problems behind it, which includes reusability, scalability, and performance from inside the service implementation, not from outside. And this is exactly what I suggested for business services above. If you can't really do all this, then maybe you are not ready to create a business service and need to settle with the data service. Having a business service which is just another maintenance or consumer headache is simply not right; clients would rather create in-memory classes and put the logic in them - effect would be the same (synchronous calls), but no dealing with distribution and wires (even better performance).
That's all I wanted to say. Think about the services that you use and build, and try to classify those. If some of those are not a data service and not a business service either, then be careful with those.