skip to content
Relatively General .NET

Backend API design principles: Don’t mirror your data

by Oren Eini

posted on: February 27, 2023

It’s very common to model your backend API as a set of endpoints that mirror your internal data model. For example, consider a blog engine, which may have: GET /users/{id}: retrieves information about a specific user, where {id} is the ID of the user GET /users/{id}/posts: retrieves a list of all posts made by a specific user, where {id} is the ID of the user POST /users/{id}/posts: creates a new post for a specific user, where {id} is the ID of the user GET /posts/{id}/comments: retrieves a list of all comments for a specific post, where {id} is the ID of the post POST /posts/{id}/comments: creates a new comment for a specific post, where {id} is the ID of the post This mirrors the internal structure pretty closely, and it is very likely that you’ll get to an API similar to this if you’ll start writing a blog backend. This represents the usual set of operations very clearly and easily. The problem is that the blog example is so attractive because it is inherently limited. There isn’t really that much going on in a blog from a data modeling perspective. Let’s consider a restaurant and how its API would look like: GET /menu: Retrieves the restaurant's menu POST /orders: Creates a new order POST /orders/{order_id}/items: Adds items to an existing order POST /payments: Allows the customer to pay their bill using a credit card This looks okay, right? We sit at a table, grab the menu and start ordering. From REST perspective, we need to take into account that multiple users may add items to the same order concurrently. That matters, because we may have bundles to take into account. John ordered the salad & juice and Jane the omelet, and Derek just got coffee. But coffee is already included in Jane’s order, so no separate charge for that. Here is what this will look like: ┌────┐┌────┐┌─────┐┌──────────────────────┐ │John││Jane││Derek││POST /orders/234/items│ └─┬──┘└─┬──┘└──┬──┘└─────┬────────────────┘ │ │ │ │ │ Salad & Juice │ │─────────────────────>│ │ │ │ │ │ │ Omelet │ │ │───────────────>│ │ │ │ │ │ │ │ Coffee │ │ │ │────────>│ The actual record we have in the end, on the other hand, looks like: Salad & Juice Omelet & Coffee In this case, we want the concurrent nature of separate requests, since each user will be ordering at the same time, but the end result should be the final tally, not just an aggregation of the separate totals. In the same sense, how would we handle payments? Can we do that in the same manner? ┌────┐┌────┐┌─────┐┌──────────────────┐ │John││Jane││Derek││POST /payments/234│ └─┬──┘└─┬──┘└──┬──┘└────────┬─────────┘ │ │ │ │ │ │ $10 │ │────────────────────────>│ │ │ │ │ │ │ │ $10 │ │ │──────────────────>│ │ │ │ │ │ │ │ $10 │ │ │ │───────────>│ In this case, however, we are in a very different state. What happens in this scenario if one of those charges were declined? What happens if they put too much. What happens if there is a concurrent request to add an item to the order while the payment is underway? When you have separate operations, you have to somehow manage all of that. Maybe a distributed transaction coordinator or by trusting the operator or by dumb luck, for a while. But this is actually an incredibly complex topic. And a lot of that isn’t inherent to the problem itself, but instead about how we modeled the interaction with the server. Here is the life cycle of an order: POST /orders: Creates a new order – returns the new order id ** POST /orders/{order_id}/items: Adds / removes items to an existing order ** POST /orders/{order_id}/submit: Submits all pending order items to the kitchen POST /orders/{order_id}/bill: Close the order, compute the total charge POST /payments/{order_id}: Handle the actual payment (or payments) I have marked with ** the two endpoints that may be called multiple times. Everything else can only be called once. Consider the transactional behavior around this sort of interaction. Adding / removing items from the order can be done concurrently. But submitting the pending orders to the kitchen is a boundary, a concurrent item addition would either be included (if it happened before the submission) or not (and then it will just be added to the pending items). We are also not going to make any determination on the offers / options that were selected by the diners until they actually move to the payment portion. Even the payment itself is handled via two interactions. First, we ask to get the bill for the order. This is the point when we’ll compute orders, and figure out what bundles, discounts, etc we have. The result of that call is the final tally. Second, we have the call to actually handle the payment. Note that this is one call, and the idea is that the content of this is going to be something like the following: { "order_id": "789", "total": 30.0, "payments": [ { "amount": 15.0, "payment_method": "credit_card", "card_number": "****-****-****-3456", "expiration_date": "12/22", "cvv": "123" }, { "amount": 10.0, "payment_method": "cash" }, { "amount": 5.0, "payment_method": "credit_card", "card_number": "****-****-****-5678", "expiration_date": "12/23", "cvv": "456" } ] } The idea is that by submitting it all at once, we are removing a lot of complexity from the backend. We don’t need to worry about complex interactions, race conditions, etc. We can deal with just the issue of handling the payment, which is complicated enough on its own, no need to borrow trouble. Consider the case that the second credit card fails the charge. What do we do then? We already charged the previous one, and we don’t want to issue a refund, naturally. The result here is a partial error, meaning that there will be a secondary submission to handle the remainder payment. From an architectural perspective, it makes the system a lot simpler to deal with, since you have well-defined scopes. I probably made it more complex than I should have, to be honest. We can simply make the entire process serial and forbid actual concurrency throughout the process. If we are dealing with humans, that is easy enough, since the latencies involved are short enough that they won’t be noticed. But I wanted to add the bit about making a part of the process fully concurrent, to deal with the more complex scenarios. In truth, we haven’t done a big change in the system, we simply restructured the set of calls and the way you interact with the backend. But the end result of that is the amount of code and complexity that you have to juggle for your infrastructure needs are far more lightweight. On real-world systems, that also has the impact of reducing your latencies, because you are aggregating multiple operations and submitting them as a single shot. The backend will also make things easier, because you don’t need complex transaction coordination or distributed locking. It is a simple change, on its face, but it has profound implications.

Extending the System Menu to add advanced commands in .NET

by Gérald Barré

posted on: February 27, 2023

Table Of ContentsWhat's the system menu?WinFormsWPF#What's the system menu?The system menu is the menu that appears when you right-click on the title bar of a window or when you press Alt+Space. It contains commands like 'Close', 'Move', 'Size', 'Minimize', 'Maximize', 'Restore', 'Help', etc. This

RavenDB with Oren Eini on Coding Cat Dev

by Oren Eini

posted on: February 24, 2023

Building the final RequestDelegate

by Andrew Lock

posted on: February 21, 2023

Behind the scenes of minimal APIs - Part 7

Creating a custom Main method in a WPF application

by Gérald Barré

posted on: February 20, 2023

WPF generates a default Main method for you. This method starts the WPF application. In my case, I wanted to ensure only one instance of the application is running. You can make the check in the Startup event, but this means that your code will be executed once all WPF DLLs are loaded and some of t

Recording

by Oren Eini

posted on: February 17, 2023

Thoughts on 'What is .NET, and why should you choose it?'

by Andrew Lock

posted on: February 14, 2023

In this post I discus the first post in Microsoft's 'What is .NET, and why should you choose it?' series and give my thoughts…

Prevent accidental disclosure of configuration secrets

by Gérald Barré

posted on: February 13, 2023

An application often uses secrets to access databases or external services. The secrets are usually provided using environment variables, configuration files, or Vault (Azure Vault, Google Secret Manager, etc.). These secrets are often bound as string making it easy to accidentally disclose. For ex

The cost of timing out

by Oren Eini

posted on: February 07, 2023

Let’s assume that you want to make a remote call to another server. Your code looks something like this: var response = await httpClient.GetAsync("https://api.myservice.app/v1/create-snap", cancellationTokenSource.Token); This is simple, and it works, until you realize that you have a problem. By default, this request will time out in 100 seconds. You can set it to a shorter timeout using HttpClient.Timeout property, but that will lead to other problems. The problem is that internally, inside HttpClient, if you are using a Timeout, it will call CancellationTokenSource.CancelAfter(). That is... what we want to do, no? Well, in theory, but there is a problem with this approach. Let's sa look at how this actually works, shall we? It ends up setting up a Timer instance, as you can see in the code. The problem is that this will modify a global value (well, one of them, there are by default N timers in the process, where N is the number of CPUs that you have on the machine. What that means is that in order to register a timeout, you need to take a look. If you have a high concurrency situation, setting up the timeouts may be incredibly expensive. Given that the timeout is usually a fixed value, within RavenDB we solved that using a different manner. We set up a set of timers that will go off periodically and then use this instead. We can request a task that will be completed on the next timeout duration. This way, we'll not be contending on the global locks, and we'll have a single value to set when the timeout happens. The code we use ends up being somewhat more complex: var sendTask = httpClient.GetAsync("https://api.myservice.app/v1/create-snap", cancellationTokenSource.Token); var waitTask = TimeoutManager.WaitFor(TimeSpan.FromSeconds(15), cancellationTokenSource.Token); if (Task.WaitAny(sendTask, waitTask) == 1) { throw new TimeoutException("The request to the service timed out."); } Because we aren't spending a lot of time doing setup for a (rare) event, we can complete things a lot faster. I don't like this approach, to be honest. I would rather have a better system in place, but it is a good workaround for a serious problem when you are dealing with high-performance systems. You can see how we implemented the TimeoutManager inside RavenDB, the goal was to get roughly the same time frame, but we are absolutely fine with doing roughly the right thing, rather than pay the full cost of doing this exactly as needed. For our scenario, roughly is more than accurate enough.

Generating the response writing expression for RequestDelegate

by Andrew Lock

posted on: February 07, 2023

Behind the scenes of minimal APIs - Part 6