skip to Main Content

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,

  1. Fetch the event which the post belongs to.
  2. Check if the user is the author of the post.
  3. If yes, delete the post. If not, fetch the hosts of the event.
  4. Check if the user is among the hosts.
  5. 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?

2

Answers


  1. Chosen as BEST ANSWER

    Followup:

    I've been mulling intensely over this the during last week, and maybe I've discovered a flaw in my way of reasoning about the problem. I've thought about the post entity as residing exclusively in the post service, but maybe that's a troubling oversimplification.

    What if each service (i.e. bounded context) has its own concept of what a post is, and consequently arranges for the storage thereof? The event service keeps a table of posts posted in events, the wall service records posts on walls, etc.

    Such entities would be quite thin, consisting mostly of a GUID, identity of the poster, maybe its contents. They could also contain special attributes that are only used in that context. For example, events, but no other services, may allow posts to be pinned.

    (Nota bene: below the term "event" is concurrently used in a totally different mening, namely a message that is sent between processes using e.g. Apache Kafka, describing something that happened.)

    Every time a post is submitted to a service, an event is posted to the event bus. For example, when a user posts in an event, the event service creates a post entity and issues events.post-posted {id: ..., authorId: ..., contents: ...}. Similarly, the wall would post wall.post-posted {id: ..., authorId: ..., receiverId: ..., contents: ...}.

    The post service, in turn, listens for all such events. Each time a post is posted to another service, a corresponding post entity is created in the post service, sharing the ID of the original post. This is the "smart" post entity, with all features that are common to posts accross the application. It could deal with sending notifications, arranging threads, discovering abuse, recording likes etc.

    This means that each service has much more freedom in dealing with its post entities, as they no longer refer to a single source of information residing in a single service. It allows the gateway to choose among several ways of retrieving post data, depending on the situation. For example, in order to tell the UI that a post is pinned, it needs to talk to the event service, but in order to get the textual content, it might have to talk to the post service. Perhaps the post entity in the wall service has special options to deal with birthday wishes posted on peoples' walls.

    Returning to the original question: this takes away the need for the services to communicate when dealing with the deletion of an event. Instead of deleting through the post service, it's the event service's job to receive requests to delete posts. Since it has information about both the author of the post and the hostship of the event, it can make the decision itself.

    Criticism to this idea:

    Although I feel I'm on the right track here, I have two main concerns.

    The first one is that this obviously doesn't quite answer the original question about how to deal with authority when implementing a web application with microservices. Maybe this is just the answer to one hypothetical scenario, whereas other slight variations of the problem aren't alleviated at all by this approach.

    My second concern is that I'm falling into the passive-aggressive event trap. I'm describing what's happening as a series of events, but maybe I'm really issuing commands? After all, the reason for posting the event.post-posted event is to trigger the creation of an event in the post service. On the other hand, the application wouldn't break if these events weren't listened to; events would just have really dry posts.


    What are your thoughts on this approach?


  2. 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.

    So the first thing I would start with is identifying where the authority for deleting a post resides — using as a guiding principal the idea that there should be a single writer responsible for maintaining any given invariant.

    In this case, it would seem to be the Post service, reasonably enough.

    I’ll presume that the plumbing includes some mechanism to detect that the user is who they say they are — the authenticated identity being an input to the service.

    For the case where the author is deleting the post, verifying that the rule is satisfied should be trivial, in that we have the authority for creating and deleting the post in the same place. There’s no collaboration required here.

    So the tricky part is determining if the authenticated identity belongs to the event host. Presumably, the authority to determine the event host lives in the Event service.

    Now, a reality check: if you query the event service to find out who the host of the event is, without holding a lock on that information, then the owner can be changing concurrently with the processing of the delete command. In other words, there’s potentially a data race between a ChangeOwner command and a DeletePost command.

    Udi Dahan observed:

    A microsecond difference in timing shouldn’t make a difference to core business behaviors.

    In particular, the correctness of the behavior shouldn’t depend on the order that your message transport system happens to deliver the commands.

    Your event sourced approach is the one that is closest to this idea.

    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.

    Almost so — the key idea is this: if the Post service is the authority for post lifetimes, then it must be permitted to announce that it has changed its mind. You build into your design the notion that the authority makes its best decision with the information it has available, and applies corrections (sometimes referred to as compensating events) when new information would invalidate an earlier choice.

    So when the delete command arrives, you check to see if it is from the host. If so, you can mark the post right away. If it isn’t from the host, you remember that somebody wanted to delete the post, and if it later turns out an update to the event informs you that same somebody is the host, then you can apply the mark.

    And the same approach works in the opposite case – the delete came from the host, so the post was marked. Whoops! we just found out that imposter wasn’t the host. OK, so show the post again.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search