I had this discussion in my workplace and wanted to share and get opinions from the folks here. (I suspect StackOverflow might not appreciate such open ended questions).
Context: We have a microservice involved in pricing signalling to our users. We have an endpoint which have the following:
- Input: an array of item ID’s
- Output: the expected final price of the given items.
The item prices are quite volatile (and no, it is not crypto related), and is dependent on things like instantaneous supply-demand, promotions, etc.
Since the prices change quite frequently, it became a requirement that we commit to the price that was shown to the user initially, up to a certain time period (eg 5 min after the price was calculated). This improves the UX since the user will be charged as according to what they expected at the start.
Currently, in our system, we achieve this via a JWT, which contains all the details in the request, the obligatory signature, and the expiry set to 5 min from the time it was generated.
After generating this receipt, the FE can then call the endpoint with the JWT which does the actual payment processing using the params encoded in the token. This way, we know that the params + the total cost that is quoted in the JWT originates from our service since we verify that we signed it.
And the system evolves once more. We see that in the system, there is this mechanism, that if the token is expired, we do not reject the request at the charging step. Instead, we call the price endpoint internally using the params provided, and check if the price is the same as in the expired JWT. If it is the same, we process it as normal despite the JWT being expired.
This is where the contention lies. I believe that we should force the user to procure another non-expired JWT and removing this complex logic while others believe in the value of this improved UX where the user doesn’t need to restart the whole flow again.
What do y’all think? Which way would y’all architect the endpoint? Or is there something fundamentally wrong with our design (maybe JWT is not the best suited for this use case)?
If I were a user, and the system told me that it was aware of what I wanted to do, and capable to do it, and it was in both of our financial best interests that the system fulfill my request, but it was deciding not to until I went back and jumped through an additional pointless hoop, before doing what I’d attempted to do in the first place… I definitely would be more irritated than not.
It might be worth having a prominent notification that the system was fulfilling the expired request, so it’s not confusing that the expired tickets work sometimes and not other times. Or, maybe just tell them the JWT they’ve got is expired, and ask them yes or no if they want the new (current) price instead, and update it transparently if they say yes. You can have a higher price if it’s higher, and depending on your relationship with the customers, you could either lower the price if it’s lower or just leave it at the current price and have them get what they get. But I would definitely make things easy and smooth for the customer in this type of situation as opposed to making the system easy to make, at the expense of having them have to click through a little circular runaround when the system is aware of exactly what they’re trying to do.
Or, maybe just tell them the JWT they’ve got is expired, and ask them yes or no if they want the new (current) price instead, and update it transparently if they say yes.
Based on this, it seems like you’re suggesting to move the logic closer to the frontend and leave the auto-refetching logic out of the backend?
The more I look at the responses, the more I feel this is a front-end problem to be solved rather than the backend’s.
they’re talking about an API though, which changes the game a bit. i agree regarding the UX of the situation, but i don’t think the API is the right place to do something like that
the API should follow the theory of least surprise, and always work the same rather than follow some unwritten rule in certain situations. i say this for 2 reasons (that i can think of right now):
- unwritten rules like not following expiry lead to “worked on my machine” and difficult to debug scenarios
- if you break standards like JWT, you can’t do things like offload auth validation to some kind of ingress router because you have extra rules that don’t follow the spec
you can implement the same functionality in the client app pretty easily which, IMO, is where UX lives. perhaps putting an expiry with “prices refresh in 5min” to get around the complexity of the fact that prices may or may not change or stay the same… i don’t think anyone enjoys when their ride share surge price changes after the app “times out” searching for driver (as in something out of their control, like an opaque timeout) but if they know the amount of time they have they feel in control
From an API perspective I agree with you. From a UX perspective I agree with them.
In the end, you’re writing software to benefit users, so user benefit is top priority.
Luckily, you can have it both ways. Keep the API pure and simple, returning a meaningful error to the client, and the client then procures a new JWT completely transparently to the user, and retries.
I suspect you’re overcomplicating things by using a JWT, but that kind of decision screams “confounding factors” that affect design decisions that you haven’t/can’t elaborate on. It’d just take some minor tweaks of the standard “shopping cart” API/DB design to get what you want, so I assume there is a reason you haven’t gone that route.
What are the alternatives to a JWT. I know it is a bit bloated and we could just use the HS256 signature itself, but that doesn’t really change the core problem of expiry vs auto-refetch
My comment about JWT wasn’t that it complicates the current problem, but that it complicates the design in general.
The alternative is to do it the traditional way, with DB records associated with the user, rather than pushing everything to the client. It wont solve the problem you’re describing, but it might make working on a solution easier.
The feature you’re describing is just a slight tweak of the standard shopping cart, so the standard tried-and-true shopping cart design would serve you well, barring extenuating circumstances like some kind of significant DB limitation. DB tables for the items, and for the shopping cart itself. When the user goes to check out the cart, you make an offer on the cart, which is basically just a clone of the shopping cart into a new table with all the item prices denormalized (and therefore locked in). Add a field for the offer expiration date and you’ve got a working design that is very similar to the standard well-worn design, and without any complicated JWT stuff. You still need the client to retry with a new offer if the current offer has expired, but adding retry logic to clients is a pretty normal thing for clients to have.
It’s a pretty significant departure from your current design, so it’s probably not actually a useful answer, but this is what I meant about the JWT making things more complicated in general.
I don’t really have an answer for you here, but isn’t having the expiry pointless now if you’re going to honour an elapsed token?
The argument has become “if price X mins ago is equal to price now, honour it” - so it could have expired 10 mins ago, or 10 days ago, etc. and you’d still be honouring it, right?
So, why have an expiry at all in this scenario? The question should become, if we’re going to honour it after 5 mins, will we honour it at 10 mins? 100 days? Which? A cut off needs to be defined.
I think the idea was that as long as it is within 5 min, our service can be certain that the price shouldn’t change and thus we can save the computation cost of having to compute the price.
It also is a user requirement, cause within that 5 min, even if the price is supposed to be changed, we will still use the price in the JWT.
Open 1000 sessions when the price is low then sell the JWTs when the price is higher
I think you missed the main feature point… The customer asks for a quote and for five minutes you honor the quote regardless of how the market changed - after five minutes if the price shifted they need a new quote.
Just my opinion here but using a signed JWT instead of a server local variable seems needlessly risky for communicating a price. Considering the potential liability to the company if your signing token is compromised I’d much rather send the prices to the user and keep a server local copy tied to the account/session/auth token. When the user tries to confirm the price we’d just pull the information from local storage.
In terms of your primary question though… I can see the UX advantage of honoring the expired token if prices were stable but I’d probably roll out an MVP without that feature fully developed but with some logging to flag how often it’d activate - throw the statistics at business people and let them ponder how often it’d activate.
That all said, this is an API not a GUI so I really don’t care as much about the UX since the consumer can just automate resubmitting for a new token - especially if we’re putting together an SDK or code samples for clients to run the requests and double especially if we’re controlling both ends with a distributed binary (but that doesn’t sound like the case here).
Actually, we are controlling both ends. But the issue is that frontend have rather limited bandwidth most of the time (sadly the truth is that despite that your own team wants to make things clean, other teams may not have the same stance).
The frontend should inform the user (that the “price lock” time has expired) and refresh the price automatically. Then when they click submit there should always be a non-expired token. You should also be storing details of these tokens server-side so you can validate that it’s not been tampered with (if someone gets the private key. They can submit orders for any price?)
I think the idea was that if they managed to get the private key, we have away bigger problems on our hands than them submitting fraudulent orders. Even with server-side tokens, the same could happen if someone get access to your machine.