DJL
DJL

Reputation: 166

Handling idempotency for REST endpoints that affect multiple entities and can be partially successful?

Background:

I wrote and maintain a REST API that deals with restaurant orders and rewards programs.

A normal flow for earning rewards on orders goes as follows:

  1. An order is posted to our API.
  2. If the order payload contains a customer_id, we check that customer in our database for reward accounts, determine if they earned any rewards, and deposit the rewards to their account. Even if there is no customer on the check, we save all the orders anyways.
  3. Once a customer claims rewards for an order, that order is considered to be permanently claimed.

I am working on adding the ability to make retroactive claims (the endpoint in question), which would be structured something like this:

PUT orders/{order_id}/claim?customer_id={customer_id}

If, under the normal flow outlined above, an order was already claimed by one customer for rewards, we decided it would make sense for a retro claim attempt by a different customer to result in a 422 error. Only one customer shall be able to claim an order is a core requirement.

I'm struggling with how to best handle multiple attempts to claim (or retro claim) the same order by the same customer. At face value, a similar 422 response might make sense, but there is an additional complication: Customers may have multiple reward accounts. We represent this by having a one-to-many relationship between Customers and Customer Reward Accounts in our database, and when a customer makes a claim of an order...it's really the customer reward account(s) that the claim is processed for. A customer, when they claim an order, can earn "visit credits" at a rate of 1/order, which would allow them to say get a free meal every 10 visits, but also earn "spend points" at a rate of 5/dollar spent, which they can use for coupons.

The Problem:

It is a rare edge case, but there are conditions that make is possible for one of a customer's reward accounts to "claim" an order, while the other may not. Thus, calling the retro claim endpoint could result in a "partial success" or "partial claim". The order would still be "unclaimed" by one of the customer's reward accounts, and returning a 422 on any subsequent customer claim attempt, that would mean there would be no way for the second reward account to get their perks at a later date.

One approach could be to scope the claim endpoint to a "reward account" instead i.e.

PUT orders/{order_id}/claim?customer_reward_account_id={customer_reward_account_id}

This allows clear separation of reward account claims but violates a design principle of our API being customer centric. We don't want customers to have to query all the customer's reward accounts before making a claim (2..n API calls vs. 1). We want all API interactions to be by customer. In the response, we relay back which reward accounts claimed:

{
    "claiming_customer": {
        "id": "2",
        "reward_account_ids": [
            "3",
            "5"
        ]
    }
}

The solution I'm thinking of is to change the behavior of this endpoint, so it is idempotent from the perspective of the customer. On the first attempt, rewards might be earned for reward account 3. On the next attempt, nothing would happen for reward account 3, but reward account 5 could earn. On the 3rd, 4th, 100th attempt, nothing would happen in the backend, but the response would just show 200, with the ids of the customer and accounts that claimed.

Is this a reasonable solution?

I tried an implementation of this endpoint where a customer could only make a retro claim request for a particular order 1 time, and 1 time only. This solution just seemed deficient when taken into consideration that the customer itself really wasn't the one processing claims, but its individual reward accounts were. Just because a customer had already claimed an order didn't mean that all reward accounts under that customer necessarily had and would therefore be ineligible to in the future.

This is a true question of API best practices that we have not faced in writing other APIs in our ecosystem before.

Upvotes: 0

Views: 37

Answers (0)

Related Questions