DEV Community

Discussion on: There is No U in CRUD

Collapse
jlhcoder profile image
James Hood Author

Hi Franz. Good catch! There is a lot of information that is not shown by just listing the method and resource path for that API. You are correct that PUT must be an idempotent operation. This API is actually idempotent, but you can't tell just from the method + resource. In the request body definition, the client is required to pass in a referenceId that is unique. The behavior of the API is you can call it multiple times with the same refId and it will only apply the debit operation once. This makes it idempotent and safe to retry, which is critical in a distributed architecture.

I didn't want to detract from the main point of my article by explaining this in the post.

Hope this helps clarify my choice of HTTP verb.

Collapse
franzliedke profile image
Franz Liedke

Thanks for clarifying!

One more thing about the choice of PUT, though. The spec states:

The fundamental difference between the POST and PUT requests is reflected in the different meaning of the Request-URI. The URI in a POST request identifies the resource that will handle the enclosed entity. That resource might be a data-accepting process, a gateway to some other protocol, or a separate entity that accepts annotations. In contrast, the URI in a PUT request identifies the entity enclosed with the request -- the user agent knows what URI is intended and the server MUST NOT attempt to apply the request to some other resource.

...which your suggestion does not match. At that point, this might just be nit-picking, though, as I am not aware of any practical benefits of this principle, other than semantics (whereas the idempotency thing may very well be relevant in terms of intermediaries etc.)

Thread Thread
jlhcoder profile image
James Hood Author

Hmm, I'm failing to see how I'm not meeting that requirement...

For all of the PUT requests, I am specifying a specific resource to take the action on by passing the accountId. Are you saying I'm breaking the REST spec by following that with the operation in the URI (debit, credit, etc)?

If so, that was a pragmatic choice I made. If we want to be pedantic, I should have a single PUT URI that just ends with the accountId. But then how do you support multiple business operations? There are only so many verbs available. One option is to add an action query parameter and use that as a switch inside your resource method implementation. However that code gets messy, especially in a strongly typed language. You're basically hand-writing custom routing code. At that point, we decided it was cleaner to put the action in the URI and let the framework's routing layer take care of this for us. Plus, it made it easier to document the different operations in a swagger definition.

So I think what I'm doing is consistent with the REST spec, although as you alluded, we're probably in nit-picking territory at this point. I would be cautious about treating the spec as gospel. Sometimes bending the rules should win out if the pros outweigh the cons.

Thread Thread
franzliedke profile image
Franz Liedke

But then how do you support multiple business operations? There are only so many verbs available.

By expressing them in terms of nouns, not verbs (such as POST /transactions). The verbs are fixed, and you apply them to nouns whatever form they take (i.e. they are polymorphic).

Thread Thread
jlhcoder profile image
James Hood Author

I think we'd cut through a lot of back and forth if you propose an alternative set of REST resource methods to the ones I propose in my post and explain how they're better. If they're not better, but just different, that's fine. The reason I chose my resource Uris the way I did was because of DDD. I think it's cleaner to have actions that affect a single entity reference the entity you're performing the action on in the URI, rather than turning the action into a noun and making that the URI. However I do use that strategy for actions that happen across entities. For example, to do a transfer between accounts, I promote the transfer itself to a first-class entity in the system. At that point, transfer is its own URI.

Thread Thread
franzliedke profile image
Franz Liedke • Edited on

Sorry, took me a while to get back. Busy week...

Your suggestion:

  1. POST /account - open a new account.
  2. PUT /account/:id/close - close an existing account.
  3. PUT /account/:id/debit - remove money from an account.
  4. PUT /account/:id/credit - add money to an account.
  5. GET /account/:id - load single account by its account id
  6. GET /account/:id/transactions - list transaction history for an account.
  7. GET /accounts/query/customerId/:id - list accounts for the given customer id.

My suggestion, without putting too much thought into it.

  1. POST /accounts - open a new account.
  2. DELETE /accounts/:id - close an existing account.
  3. POST /accounts/:id/transactions - remove money from an account.
  4. POST /accounts/:id/transactions - add money to an account.
  5. GET /accounts/:id - load single account by its account id
  6. GET /accounts/:id/transactions - list transaction history for an account.
  7. GET /customers/:id/accounts - list accounts for the given customer id.

We basically have three sets of resources now: customers, (customers') accounts and (accounts') transactions. The given operations map very nicely to the existing HTTP verbs. :)

Thread Thread
jlhcoder profile image
James Hood Author

Thanks so much for taking the time to present this alternative endpoint design! First off, I want to say I'm a firm believer that there's no single "right" solution, so keep that in mind as I share my thoughts.

I think the main strength here is that your design sticks with resources as nouns, unlike mine where the URI can contain explicit business operations. I can definitely see how this is more in-line with the original REST concept.

Some potential weaknesses I see with it are

  1. Debit and Credit operations have been combined into a single transaction POST operation. Now this seems like a strength in that it's simpler from an interface standpoint (debit is just a transaction with a negative amount, credit is a positive amount, right?). However, there can be very different business rules around debits vs credits, to the point where they should be modeled in the domain layer as separate operations on an account. A negative credit could still need to be treated differently than a positive debit, depending on banking rules/regulations. With a single REST endpoint, now you're back to having to infer which operation they mean based on parameters. I feel very strongly against doing this, which is a key point I was trying to make in this article. With the URI scheme I propose, the operation is always explicit, which ends up being more maintainable as requirements change and/or new requirements surface.
  2. I'm going back and forth on customers as a base path entity for querying accounts. A customer could have several different concerns, so I could see that path subtree exploding (but maybe that's ok). I also think this is a pattern that doesn't hold up as well as requirements change. What if customer service UIs need an endpoint to query all accounts that have been closed in the last day? It seems cleaner to keep all account-related queries within the /account base path, rather than scatter them to different base paths, just because that particular query happens to fit there. This is imprecise reasoning, but it's paid off many times for me in the past to group all activities around a particular domain entity together. It creates very consistent patterns and keeps things organized, allowing you to scale as new requirements flow in. I'm not trying to pull the "I'm experienced, therefore I'm right" card. I think your design works, but I do still prefer mine.

Again, there's no single "right" solution, so just sharing my opinions. Thanks again for the follow up!

Thread Thread
franzliedke profile image
Franz Liedke

Hi James, allow me to get back once more. ;)

You write:

However, there can be very different business rules around debits vs credits, to the point where they should be modeled in the domain layer as separate operations on an account. A negative credit could still need to be treated differently than a positive debit, depending on banking rules/regulations.

The API is not the business layer. In fact, my experience tells me it is quite useful when the API hides/encapsulates as much of the business logic as possible. And I don't see how changing regulations would affect the interface of these operations in such a way that a single endpoint for both operations would make you less flexible. The operations should absolutely be modeled separately in your domain, but again: that's not the API.

It seems cleaner to keep all account-related queries within the /account base path, rather than scatter them to different base paths, just because that particular query happens to fit there.

Yep, that's always tricky. The way I do it at work is to provide one base endpoint (for /accounts in this case) that offers all of the filters and in addition have these named subresources (/customer/:id/accounts) - basically as an alias. The idea here is to split between real "filters" (e.g. accounts started after a certain date or having a certain balance) and "lenses" (for lack of a better word) - different use cases that might be mutually exclusive or warrant naming as their own resource.

Thanks for the interesting debate! I'm off for my vacation now. :)

Thread Thread
jlhcoder profile image
James Hood Author • Edited on

Likewise! Great discussion! 😊

Agreed that the API interface and internal implementation should be distinct concepts. However, I find when using microservices architecture and DDD, generally you at least start out with an implementation that closely mirrors the API model. The implementation may evolve over time, but I like to start out simple.

The alias idea is great. Again, I'm coming at this from a microservices architecture, which might be where some of the disconnects are coming from. There's the API at the microservices layer and then you front all of your microservices with an API Gateway layer. So I would keep my /account centric stuff at the microservices layer, then add the customer/:id/accounts at the API Gateway layer. So I think we're in agreement here.

Have a good vacation!

Thread Thread
renatomefi profile image
Renato Mefi

Hello James and Franz,

This thread is an awesome complement to this post, I'll follow with just one thing:

Debit and Credit operations have been combined into a single transaction POST operation. Now this seems like a strength in that it's simpler from an interface standpoint (debit is just a transaction with a negative amount, credit is a positive amount, right?). However, there can be very different business rules around debits vs credits, to the point where they should be modeled in the domain layer as separate operations on an account. A negative credit could still need to be treated differently than a positive debit, depending on banking rules/regulations. With a single REST endpoint, now you're back to having to infer which operation they mean based on parameters. I feel very strongly against doing this, which is a key point I was trying to make in this article. With the URI scheme I propose, the operation is always explicit, which ends up being more maintainable as requirements change and/or new requirements surface.

I completely agree with James on this one, keeping one noun as transactions might be more compliant to REST, in the other hand both client and server are going to (possibly) disrespect the single responsibility principle, the server will have to split the business logic internally while the client depending on its usage might have also to do the same due to having multiple operations in a single endpoint.

That's why I agree "bending the rules" are important as well since the advantage of the transactions endpoint is almost purely to follow the standards without giving much back to clients and servers.

Thread Thread
franzliedke profile image
Franz Liedke

Business rules need to be enforced on the server side. With James' example, I see nothing in there that requires the client to know the details of these rules. Hence, it should not have to deal with two separate endpoints because to it, the operations are the same. That, in my eyes, is the biggest benefit of a combined endpoint in this case. Depends on the concrete example, though.