10

Please settle an argument between me and a friend.

We'r currently designing a product API. Our Product entity looks like this

{
    "Id": "",
    "ProductName": "",
    "StockQuantity": 0
}

Product sales are handled by a 3rd party and they are obligated to inform us with the purchased quantity so StockQuantity field can be decreased.

My approach:

PUT /api/Product/{Id}/ --data { "StockQuantity": "{NewStockQuantity}" }

The 3rd party is responsible with querying the product, making the calculation based on current StockQuantity and purchased quantity and sending a PUT request with the new value.

My friend does not want the 3rd party to do the calculation. His approach

PUT /api/Product/{Id}/DecreaseStock --data { "PurchasedQuantity": "{PurchasedQuantity}" }

So we can make the calculation and update the StockQuantity

I don't want to create function based endpoints and he don't want to trust on 3rd party to make the calculations.

What would be the correct way for us to approach this problem?

1
  • Remember that PUT should be (in theory) idempotent. Option 1 would fit in the semantics. Option 2 would not. If to be REST compliant is important for you. You would try to keep PUT calls idempotent since it will save you from a lot of headaches. Same for DELETE. For command-like operations, honestly, I would give a chance to the Json or XML RPC. Both strategies (REST and RPC) can live together within the same Web API. It conveys with the principle of CQRS (command query responsibility segregation:-)
    – Laiv
    Commented Jun 13, 2018 at 15:21

6 Answers 6

21

You could let your 3rd party post sales to your product. For example:

POST /product/{id}/sale { "Quantity": 3 }

I agree with both your and your colleague's point. This is business logic, and shouldn't be left to the client of the API, but you should also avoid having "functions" as endpoints.

Sometimes solving such problems is as easy as calling it differently, admittedly not always.

8
  • 2
    This. Plus: seems like each sale also needs an object on database too. Having each sale as separate object in db enables traceability. Think, if something went wrong and final stock quantity is wrong and need to fix values. If you only have one column of final value then not much you can do. Hopefully there are any useful logs in system to figure out what went wrong. If you have sale objects with timestamps, usernames and possibly IP address attached, you have ability to delete certain records to fix data, and to trace which user/location it came from.
    – Ski
    Commented Jun 13, 2018 at 9:25
  • Thank you for the inputs. Sale/Order is resource of another team, it is not my responsibility to save or process them. Know this, is creating /sale endpoint still valid? Commented Jun 13, 2018 at 9:53
  • @SefaÜmitOray: The endpoints /sale and /product/{id}/sale are completely independent and the fact that they have similar names does not in any way imply that they refer to the same resource. Commented Jun 13, 2018 at 10:02
  • @BartvanIngenSchenau What I mean is, sale is not in my domain and it is not part of product. Does it still makes sense to create /product/{id}/sale while it's not representing any actual resource? Commented Jun 13, 2018 at 10:08
  • 6
    @SefaÜmitOray It is completely valid if it represents something meaningful in your context. It does not have to mean the same thing as in other contexts and it does not have to be anything that gets directly persisted to the database either. Domain != Database Tables, Resource != Database Tables. Commented Jun 13, 2018 at 10:34
3

There's no reason that you can't do either; or both.

In a point of sale context, tracking individual transactions makes a lot of sense. There, Robert's solution makes a lot of sense.

In a stock/warehouse context, you don't necessarily track transactions so much as "take inventory"; having an endpoint that allows the client to report their stock levels

I have 10 units I have 7 units I have 3 units I have 20 units

makes a lot of sense.

Stock levels change for reasons other than "sales"; just something to keep in mind.

In theory, the stock level should be computable from the changes; but in some domains that is precisely the assumption that you want to verify. You would want to be able to compute the stock level two different ways and check for discrepancies (aka "shrinkage").

So I don't think the semantics are clear cut, based on the context you have provided.

As for the HTTP part; PUT [target-uri] makes sense semantically when you are replacing one representation of a document with another. It's an UPSERT - the second PUT to a resource is asking to overwrite the existing representation.

PUT /sales { Quantity = 5 }
PUT /sales { Quantity = 2 }
PUT /sales { Quantity = 3 }

says that the quantity of units sold is 3, not 10.

PUT /sales/1 { Quantity = 5 }
PUT /sales/2 { Quantity = 2 }
PUT /sales/3 { Quantity = 3 }

That's what 10 looks like

PUT /sales { Quantity : [5] }
PUT /sales { Quantity : [5,2] }
PUT /sales { Quantity : [5,2,3] }

That's another way of spelling 10.

POST /sales { Quantity = 5 }
POST /sales { Quantity = 2 }
POST /sales { Quantity = 3 }

As far as HTTP is concerned, this is also acceptable. However, it's not a great choice on an unreliable network because messages are sometimes duplicated.

POST /sales { Quantity = 5 }
POST /sales { Quantity = 2 }
POST /sales { Quantity = 3 }
POST /sales { Quantity = 3 }

Is that 13? or 10?

PUT /sales/1 { Quantity = 5 }
PUT /sales/2 { Quantity = 2 }
PUT /sales/3 { Quantity = 3 }
PUT /sales/3 { Quantity = 3 }

That's unambiguously 10

PUT /sales { Quantity : [5,2,3] }
PUT /sales { Quantity : [5,2,3] }

That's unambiguously 10

PUT /sales/1 { Quantity = 5 }
PUT /sales/2 { Quantity = 2 }
PUT /sales/3 { Quantity = 3 }
PUT /sales/4 { Quantity = 3 }

That's unambigously 13

PUT /sales { Quantity : [5,2,3] }
PUT /sales { Quantity : [5,2,3,3] }

That's unambigously 13

POST /sales { TransactionId = 1 , Quantity = 5 }
POST /sales { TransactionId = 2 , Quantity = 2 }
POST /sales { TransactionId = 3 , Quantity = 3 }
POST /sales { TransactionId = 3 , Quantity = 3 }

10

POST /sales { TransactionId = 1 , Quantity = 5 }
POST /sales { TransactionId = 2 , Quantity = 2 }
POST /sales { TransactionId = 3 , Quantity = 3 }
POST /sales { TransactionId = 4 , Quantity = 3 }

13

(To be fair, HTTP does have support for conditional requests; you can lift some of the metadata from your domain specific protocol into the domain agnostic headers to eliminate some of the ambiguity -- if you can persuade the client to play along).

Of course, there are trade offs - HTML doesn't have native PUT support; if you are intending the clients of your API to be browsers, then you either need a protocol based on POST or you need code-on-demand extensions to convert the form submission from a POST to a PUT.

1
  • 1
    You don't have to track the individual sales, just because there is an endpoint for it. I.e. there is no need to be able to list previous sale calls, just because you can POST to it. You are right however, that there might be other use cases (we don't know), and you should define idempotent calls either with conditional calls, or with some other means. Commented Jun 13, 2018 at 13:44
2

This seems like a really bad design no matter how you slice it. I would never trust a third party to tell mey current inventory unless I've hired them to manage my warehouse.

Further, the function-looking approach is not RESTful at all and bound to create consternation among your consumers.

Finally, I can't imagine a scenario where the only thing you care about a sale is the resulting inventory you have left after it's done.

You're a lot better off having the third party post a Sale or Invoice resource to you (with info like what product, quantity, date, shipping method, customer info, etc). This let's you actually do real analysis and tracking of what you're selling, to whom, when, etc so you can actuallanage your business.

Even if your third party is doing total order fulfillment, you will want to track sales for accounting and customer demographics purposes if nothing else.

2

PUT /api/Product/{Id}/ --data { "StockQuantity": "{NewStockQuantity}" }

This kind of design has a major issue in that if you ever want to have more than one client thread running against your API, you are subject to dirty reads/writes. That is, between the time the client pulls down the current quantity, and calculates the new value, another client can pull that same prior value and calculate a different answer. The quantity you end up with will be whichever one updates last but neither is correct. For example say your current quantity is 10. Client A wants to sell 5 items and pulls the current quantity. At the same time, client B wants to sell 6 items and pulls the current quantity. Both see 10 items in stock. A calculates 5 items remaining. B calculates 4 remaining. Both update. You now show 4 or 5 items remaining depending on who's update was recorded last. However, you've actually sold more items that you actually have. What's worse is that there's no easy way to walk through and see what went wrong. All you have are two incorrect PUTs in your logs to look at.

In any real-world system of record, simply having a current total is not adequate. Consider if you go to a store and buy a number of items. You ask for a receipt and the cashier just hands you a slip with a single total on it. How would you show the total is correct from that receipt? How would you show you purchased an item if you wanted to return something?

Your friend's approach is better but I would suggest adding a transaction id into the mix. This addresses the real concerns VoiceOfUnreason mentions about duplicate transactions. One option is to provide a POST operation to create a new transaction and then PUT to that transaction to confirm it. At the point of confirmation you reduce the total stock or deny the request because there is not enough available.

1

Since sales are handled by 3rd party, you have to have control over your product inventory by not allowing them to update stock count.

For internal use, e.g. stock count purposes, you can have your approach, i.e. PUT /api/Product/{Id}/ --data { "StockQuantity": "{NewStockQuantity}" }.

For external use, you have to create a separate interface, e.g. /api/SalesOrder/ which takes a list of products and quantities, like:

POST /api/SalesOrder/ --data { [{"Id": 1, "Qty": 1}, {"Id": 2, "Qty": 3}] }

Based from SalesOrder sent by 3rd parties, the quantity of each product can be updated and assigned to the order or you can reject the order if there's not enough product available.

The processing and stock counting is internal process, 3rd party only requires interface so they can forward their orders to inventory. Basically, the SalesOrder is how the Sales, Finance and Warehouse communicate to complete a sale.

0

I would have a /sales endpoint and require clients to send their sale data. POST /sales {id, quantity}

and when receiving a sale data, you should also consider network problems and repeating calls, the same request could be repeated and you should not duplicate sale data and decrease quantity twice. For that reason, saleData should include a unique key (id or any other idempotency key), then you should check if it is already received

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