Today, I learnt about balancing and managing yet another engineering trade-off about APIs. To achieve a glory of REST, we would need to take a look at Richardson’s Maturity Model.
Richardson’s Maturity Model
Essentially, to achieve the glory of REST, the API exposed should be consistent, makes proper use of HTTP verbs (GET
, POST
, PUT
, etc) for resource interaction and offer hypermedia controls.
Not that you must reach the highest level of the model, but rather, you’ll only be able to exploit the full benefits of REST when you have reach the highest level.
Hypermedia Controls
Due to the nature of REST, links from one resource to another is not provided by default. Hypermedia controls can be achieve using libre formats and protocols such as HAL or JSON API. For the situation I’ve encountered, I was using JSON API.
As observed, you can see that there are links provided (first page, last page, self and related, etc.). This allows others to understand and transverse/navigate through your API.
CRUD
Create, Read, Update and Delete (CRUD) is a popular pattern in the RESTful world. A resource needs to expose CRUD methods that can be interacted through the respective HTTP verbs. For example, to create/register a user, you would do a HTTP POST
to /users
. And to delete a user, you would do a HTTP DELETE
to /users/1
. This is a simple way of expressing resource interactions. Hence, with applied consistency, CRUD can be very intuitive to developers, allowing ease of developer on-boarding. Querying the API would also be simple and intuitive for the API consumer.
Atomicity
Atomicity ensures that the data in a database is handled in an atomic manner — it either succeeds and commits all data or fails and commits no data. This prevents partially inserted or dropped data and ensures consistency of data. For example, in Laravel, you would wrap transaction creation in DB transaction.
// Credit was 0. Lets try to increment it to 1. DB::transaction(function () { DB::table('users')->update(['credit' => 1]); DB::table('companies')->update(['credit' => 1]); });
In the above example, if any of the encapsulated actions in a single DB transaction fails, all the action will roll-back to the state if was previously in (which is zero credits).
Problem
I encountered a problem which forces me into an awkward situation and making an engineering trade-off.
For a company registration page, the end user will need to create a User
resource, a Company
resource, a UserGroup
resource, a CompanyUser
resource and a CompanyUserPermission.
This is because the User
created needs to be added into a Group
that tells us that this User
is a company user. Other than that, a Company
needs to be created and the User
needs to be added into this Company
and given the correct CompanyUserPermission
. Below shows a visual breakdown of these resources.
This means that I would need to do a HTTP POST
request to at least 3 resource endpoints. If any of these requests ever fails, atomicity is not ensured and data gets partially inserted. This would result in the user not being able to get correct access rights.
Observable Retry?
A possible solution would be to do a retry on the client side. However, what if the client (browser) crashes? This is not a very good solution.
Registration Model?
Another possible solution is to create a resource called CompanyRegistration
. This resource would then do an atomic transaction of creating all the necessary resources. However, this would mean that it is no longer pure consistent CRUD but rather, more of a mix with Command Query Responsibility Segregation (CQRS) action.
Should I ensure consistency in CRUD or consistency in my data?
After discussing with Bob from Hackerspace SG, I was convinced that consistency in data was more important. This is because data is more important than having an intuitive RESTful CRUD API. Without atomicity, nothing can be guaranteed. Data can essentially be corrupted or wrong, which may result in loss of business. End users or customers may experience faults and stop using the platform. Although having intuitive developer experience is important, a working product takes precedence. And of course, in this case, developer experience is actually improved at the cost of intuition. (Lesser API calls required)
Conclusion
Always prioritize atomicity over consistency of your RESTful CRUD design. It may cause you to lose out on some beauty and consistency of your API but you’ll get a software that is more resilient to failures. Never sacrifice atomicity!