On first sight, the architectural style of REST via HTTP seems to constrain us to simple CRUD APIs: We operate on "resources" which we often map rather exactly to our internal domain- / entity-types and we also mainly use the HTTP-Verbs GET (=retrieve), POST = (=Create + Update partly), PUT (=Update/Replace), and DELETE. But how can this fit if we need to implement some kind of business logic like validation, that is not directly related to one of the CRUD-operations? I want to show you a real life example and explain how I solved the problem.
The Real-life Challenge: Detect the Porting-Status of a "PhoneNumberBlock"
The challenge I was facing was the following: Our telecom system has a PhoneNumberBlock-Entity, which describes the start and the end of a block of contiguous phone numbers, for example:
- start = 8931144500
- end = 8931144599
=> As you can see, this is a block consisting of 100 contiguous phone numbers.
When a customer requests a new telephone connection, he can specify one or more PhoneNumberBlocks, that need to be associated with the new connection. The blocks can have any arbitrary size. Among other things, the system has to check, if the requested block is currently ported to another carrier. It might well be, that the phone numbers of a requested block have differing porting status, because a PhoneNumberBlock has a limited lifetime. Let’s assume the above PhoneNumberBlock was once associated to a connection, but that connection has been terminated in the meantime. The block gets removed 3 months after termination and the numbers of the block might have been given away separately or in smaller blocks or even as part of a bigger block for other connections.
At some point, we need some logic that determines the current PortingStatus of the requested block with the following Enum-Values:
The reason for this is, that the system must refuse the request if the PortingStatus is either PORTED_AWAY_PARTLY or PORTED_AWAY_AS_PART_OF_BIGGER_BLOCK.
Now the question is: How should this logic be properly designed with a RESTful API? What we have is a separate domain (a "porting-microservice") that encapsulates the access to our database which contains all the porting information. From this database we must derive the above PortingStatus of the requested block.
First Naive Solution Approach
As stated above, we have to think in terms of Resources and we only have the HTTP-Verbs, so the first idea was to offer a PortedPhoneNumberResource with a findAllPortedAwayPhoneNumbers() method.
In this case, it would be the caller’s responsibility to search through the returned numbers to detect the correct overall state of the block. But this is a bad solution in terms of "separation of concerns." The porting-microservice should not only deliver the data, it should – as far as possible – also implement the required logic around that data.
Second (Still Naive) Solution Approach With Constrained Find-Method
Even if we constrain the find-method from the first solution with the start and end phone number of the requested block as query-parameters (findAllPortedAwayPhoneNumbersOfBlock), we still need the logic for finding out the status on the client side.
Third Solution Approach: The Status as a Resource
From the point of view of the client, it would be most desirable, if the REST-call could simply return the PortingStatus of the block. But then the PortingStatus would need to be a resource of its own. This doesn’t seem to make sense from a logical point of view, because a Status simply isn’t a resource.
Fourth solution approach: The Status as a Sub-Resource
What about a Sub-Resource? This seems to be more logical, but if we ask for a Sub-Resource in REST, we learned, that we need the ID of the parent resource, e.g.
The problem is, that we don’t have the ID of the parent resource. Moreover, it is likely, that the block doesn’t even exist and thus there is in fact no ID that represents this block.
Are we stuck now? Can we throw away the RESTful style for things like this? The answer is "No!"
Fifth and Last Solution Approach: The Status as a Sub-Resource of a Logical Resource
A requested block has a business key: the start- and the end phone number. Why don’t we concatenate the two numbers to a logical id including some separation character:
Maybe we could even optimize the logical id by just telling the starting number and the size of the block, but that’s syntactic sugar. Anyway, the Response-Body would simply contain the String-Representation of the actual PortingStatus.
This is in my opinion a good solution as it combines both the RESTful API design style and the "separation of concerns" principle.
- REST-Resources do not necessarily have to exist 1:1 as a persistent entity in the underlying database. Instead, they can be a logical abstraction. In other words: a DTO exposed via REST does not necessarily need to look exactly like the corresponding domain object/the entity.
- Sub-Resources offer an additional way to make logical abstractions from the original resource: Another example of such a logical Sub-Resource for implementing Non-CRUD-logic would be the validation of a given Resource instance. My approach for this is to query for the validation messages as a subresource:
- POST http://myserver/rootcontext/resources/phonenumberblocks/8931144500-8931144599/validationmessages
- The resource to validate is in the request body.
- If validation is OK, we return 200 OK and an empty response body
- If validation fails, we return 422 "Unprocessable Entity" and the response body contains the list of validation messages (see https://en.wikipedia.org/wiki/List_of_HTTP_status_codes for status code 422).
- This kind of design enables us to keep the related business logic in the service itself without violating the RESTful style. The key is the combination between logical resources (also for transient resource instances) and a proper choice of subresources.
- List resources like "phonenumberblocks" in the above example might need to support both variants of IDs: The logical ID for calling business logic on transient objects and the real ID. Anyway, if your system is used by "unstrusted"/external clients, you should not expose your technical primary key from the database as an ID but instead use some unique business key, for example: a "customerNumber", "orderNumber", etc.