4

Consider the following scenario:

  1. If the user isn't registered in the database, register them. Return their ID.
  2. If the user is in the database, return their ID.

The API endpoint for this may look something like POST mysite.com/api/users?LastName=Stack&RegisterId=1111

The consumer of the API does not want to first GET a userId and, if null is returned, then POST.

Adhering to CQRS architecture and respecting REST design, if we have the POST invoke a Query first, it seems odd to have a POST that is statistically more likely to be making database queries and returning results. This Query-invoking-POST is more often than not acting as a GET.

But if we decide to have the POST invoke a Command first, it seems like an anti-pattern to have that Command exist where it is, more often than not, just performing a Query - that is, checking for the user registration in the DB and possibly returning the ID if found, never actually creating, updating, or deleting something.

What is practical approach to this situation?

3
  • Is it ever valid to try and fetch a User and have it fail (ie 404)? Your scenario sounds like all Users are created on first access, in which case as far as the client is concerned a User can never not exist and you should just use GET requests with the side effect that a User is created in the DB if it doesn't first exist. – Cormac Mulhall May 2 at 9:23
  • It's valid to fail. Note the example endpoint, which accepts a few parameters. Creating a new users is based on interrogating a table in a separate database, finding some values, and using those values to create a new user. Imagine a "pre-registration" table, if you will. Im also personally not supportive of any GET operations which may result in intentional writes. – 8protons May 3 at 22:16
  • 1
    Thanks for clarifying, with that extra info I've added an answer tl;dr just use a POST :-) – Cormac Mulhall May 3 at 23:12
2

What is practical approach to this situation?

It sounds to me as though what you have is a command (RegisterUser) that will be a no-op in the case that the registration has already happened.

it seems like an anti-pattern

Not so much. In a distributed system where your message transport is unreliable, the capability to recognize messages that have already been handled successfully turns out to be really important if you want reliability.

So if you are going to be providing that capability anyway, then you need to re-assess the amount of additional value you get out of a pre-flight query.

3
  • That just feels weird to me though. You have a situation where a thousand customers a day are consuming your API endpoint /getId?myInfo and very rarely is one of those customers brand new. But in the very rare chance they’re new, this endpoint will create and save a new entity based on myInfo. And because of this very very rare situation, you decide to name the entire Command associated controller “RegisterUser”. Doesn’t that just seem weird to you? When your developer follows that Command, won’t they be surprised to find out that it rarely registers a user? – 8protons Apr 30 at 21:03
  • @8protons would you feel better if it were named assureRegistered? – candied_orange Apr 30 at 21:32
  • @8protons: Keep in mind that the validity of a logical construct is entirely unrelated to the naming of things, and our human inference of what a name means. If the action in question were called FloobOrDoob, with the exact same implementation, your reticent argument would cease to exist. – Flater May 4 at 8:24
2

The problem in focus here is one of semantical naming, not of technical implementation.

First of all, because I get the feeling that some commenters may have made a wrong inference, GetOrCreate is slightly misrepresentative name for an algorithm that should be called GetIdOrCreateAndThenGetId which is obviously not a great name, though pedantically more correct.

But the source of your problem is that you're trying to attribute both labels. It's a query (Get) and a command (Create).
This is why I'm calling it a semantical argument. If I rename your method CreateIfNotExists, and we simply understand that a create method returns the generated ID of the entity it just created, then we have exactly the same implementation and none of the labeling issues.

You don't have to rename it to CreateIfNotExists, but it proves the point that this algorithm at its heart is a command.


Secondly, never forget the purpose of CQRS. The goal is to make it possible to separate your write and read stores so they can be scaled independently. This scaling means that you run into eventual consistency issues, which means that the read store tends to not be instantly synced with the write store whenever a change is made, there is some time inbetween the two.

The "Query" in CQRS inherently refers to a query that is executed on the read store. It doesn't include any query that would be executed on the write store. And while it's not the prime focus of CQRS, there will generally remain to be reasons to query the write store, and your scenario is one of them.

While the Get of GetOrCreate is technically a query because it fetches data, that doesn't automatically mean that it should query the read store. In this specific case, the purpose of the fetching of information is to direct a write operation (i.e. choosing whether to create or not), and it should therefore query the write store to ensure that it has the most recent information.

The reason for querying the write store is because of the eventual consistency. This makes it possible that the read store is out of date with the write store, which means that the read store might not know of a freshly created entity yet.

A query on the read store would wrongly tell you that no such item exists, so you create a new one, and now you have two freshly created entities. A query on the write store, however, would have access to all the latest information, and thus would be more correct in its assessment that a particular entity does or doesn't exist.


Thirdly:

When your developer follows that Command, won’t they be surprised to find out that it rarely registers a user?

Whoever called the endpoint had one goal in mind: the requested resource should exist after the endpoint finishes its task.

This expectation is upheld at all times. Either it already did exist, or you ensure its creation so that from now it does exist.

Whether the resource existed before the endpoint was called is irrelevant. Clearly, the requester didn't know it either, or they wouldn't have bothered to call the endpoint if they already knew the resource existed - there'd be no point in retrieving an ID for a resources you already know.

You're conflating two very different things: the service you provide, and how you perform the internal actions. GetOrCreate describes the latter, not the former.

The requester only cares about the service you provide, not how you do it. They don't know or care how you decide to go about honoring their request, they just care that their request was honored.


Fourthly, I can't judge your architecture for you, but I do want to point out the difference between CQRS and the mediator pattern. These two are often conflated, because CQRS almost inherently makes itself a perfect fit for using a mediator to execute your queries and commands.

But that doesn't mean that any use of the mediator pattern must also adhere to CQRS. It's perfectly fine to not separate your read and write stores, while still maintaining the use of a mediator.
Hint: this is why Mediatr calls its handled actions "requests" and doesn't bother trying to label anything as a query or a command.

1

Adhering to CQRS architecture and respecting REST design, if we have the POST invoke a Query first, it seems odd to have a POST that is statistically more likely to be making database queries and returning results. This Query-invoking-POST is more often than not acting as a GET

So remember REST is about state transfer between the client and the server. How the server handles this state transfer internally is up to it, and not something the client should care about. This is in fact a specific design feature of REST, it is what allows client and server to be de-coupled. The server can be adhering to any CQRS architecture style it likes in its implementation but this should be irrelevant to the client. REST has no issues with side effect so long as the client doesn't care about them, and the client doesn't care about DB access on the server

So a REST client isn't thinking "is this saved to the DB or not". It is thinking has the server got my new User Resource. It returned a success 2xx response, so we are all good.

From the clients point of view the server saying

"yes I have your new User, never seen this before I'll save it to my DB"

and

"yes I have your new User, actually already had them already so I don't have to save anything"

should look identical. 200 (or possibly 201) is all the client sees or cares about.

You say in comment to VoiceOfUnreason

When your developer follows that Command, won’t they be surprised to find out that it rarely registers a user?

In REST that is actually something the developer working on the client should specifically not care about either way.

In REST you should focus on the outcome of the state transfer, not what any particular server implementation has to do to ensure that outcome.

This is always why the server writing to a DB on a GET is not something bad, so long as this still fits with the principle of state transfer. This is not relevant in your case since it is possible the User might not exist as a Resource until the client creates it, but in other cases where a Resource always 'exists' (even if it is not recorded in a DB) you can imagine writes happening on GET. For example an API that returns the daily weather might generate a weather report the first time the API end point for that date is requested via a GET and then save that report to a DB so it doesn't have to be generated each time. The point is that this looks identical to the client, and the client doesn't care what is happening on the server.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.