A modern multi-user web application imposes a lot of restrictions on the actions that users can perform. In other words, the actions require authority. For example, a user can only change its own personal data, and only members of a group can post content to that group. In a classic monolith application, such restrictions are easily enforced by joining several database tables and acting according to the results of queries. However, with microservices, it becomes much less clear where and how such limitations should be handled.
For the sake of argument, consider a Facebook clone. The whole application consists of several parts:
- A front-end, written in JS and other web technologies
- A backend consisting of a number of microservices
- An API for retrieving and submitting data to the backend, i.e. a gateway
As for the business logic, there are (among others) two well-known entities:
- Events (as in concerts, birthday parties etc.)
- Posts (text entries existing on walls, pages, events etc.)
Suppose that these two entities are managed by separate services, EventService and PostService. Then consider the following constraint:
A post to an event can be deleted by two kinds of users: the author of the post, and the host(s) of the event.
In a monolith, this constraint would've been conceptually very easy to deal with. Upon receiving a request to delete a post, supplying the post id and user id,
- Fetch the event which the post belongs to.
- Check if the user is the author of the post.
- If yes, delete the post. If not, fetch the hosts of the event.
- Check if the user is among the hosts.
- If yes, delete the post.
However, with a microservice strategy, I have a hard time figuring out how to divide the responsibilities of an operation like this across the services.
Alternative 1
An easy way around it would be to put logic like this in the gateway. That way, the same procedure as described above could essentially be performed, but with calls to the services instead of directly to the database. Rough sketch:
// Given postId and userId
// Synchronous solution for presentational purposes
const post = postClient('GET', `/posts/${postId}`);
const hosts = eventClient('GET', `/events/${post.parentId}/hosts`);
const isHost = hosts.find(host => host.id == userId);
if (isHost) {
postClient('DELETE', `/posts/${postId}`);
}
However, I'm not happy with this solution. Once I start putting logic like this in the gateway, it'll become very tempting to always do it, as it's a quick and simple way to get things done. All business logic would eventually amass in the gateway, and the services would become "stupid" CRUD endpoints. This would defeat the purpose of having separate services with well-defined areas of responsibility. Furthermore, it could be slow as it could give rise to a high number of calls to the services when operations are getting more complex.
I would essentially be reinventing the monolith, replacing database queries with slow and limited network calls.
Alternative 2
Another option would be to allow unlimited communication between services, allowing PostService to simply ask EventService whether the user is a host of the event in question before performing the delete. However, I'm afraid that having a potentially large number of microservices communicating with each other could introduce a lot of coupling in the longer run. Experts seem to generally advise against direct inter-service communication.
Alternative 3
With a solid system for publishing and subscribing to events, the services could stay updated about what happens in other services. For example, every time a user is promoted to host in EventService, an event would be posted (e.g. events.participant-status-changed, {userId: 14323, eventId: 12321, status: 'host'}). PostService could subscribe to the event and remembering this fact when a request to delete a post is received.
However, I'm not quite happy with this one either. It'd create a very intricate and error-prone system, where an unhandled (but potentially rare) event could make services go out of sync. Also, there's a risk that logic would end up in the wrong place. For example, the constraint in this question would be handled by PostService even though conceptually it's a property of the event entity.
I should stress though that I'm very optimistic about the usefulness of events when implementing applications using microservices. I'm just not sure they are the answer to this category of problems.
How would you tackle this hypothetical, but quite realistic difficulty?