DEV Community

loading...

Discussion on: There is No U in CRUD

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

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

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.