How To Make Your API Idempotent To Stop Duplicate Requests

Поделиться
HTML-код
  • Опубликовано: 19 окт 2024

Комментарии • 116

  • @MilanJovanovicTech
    @MilanJovanovicTech  Год назад

    Want to master Clean Architecture? Go here: bit.ly/3PupkOJ
    Want to unlock Modular Monoliths? Go here: bit.ly/3SXlzSt

  • @hydtechietalks3607
    @hydtechietalks3607 Год назад +3

    Hats Off Milan.. Lot of people explain concept but YOU DO CODE in right way, Thank you Man!!

  • @awright18
    @awright18 Год назад +7

    There is one fairly significant issue with this implementation, and you sort of talked about it. The problem is that the idempotency key storage isn't in a transaction with the execution of the product creation command. As you said if they both fail, fine, but if storing the idempotentcy key succeeds and executing the create product command failed then the caller should be allowed to submit the command again. Also I noticed you left out a solution for how you might handle this on the client side, perhaps that will be in a future video.

    • @MilanJovanovicTech
      @MilanJovanovicTech  Год назад +3

      The caller can submit the command again - but with a different idempotency key

  • @cansozbir
    @cansozbir Год назад +12

    Thanks for the video, Milan! I try to watch all your videos; they're really helpful. I'm following your tutorials to build a CQRS + Clean Architecture web API, and this week, I revisited your earlier videos to get a better grasp.
    It would be awesome if you could make a 1 or 2-hour-long video where you develop a simple CQRS + Clean Architecture web API project from start to finish. Your way of explaining concepts is fantastic, and I think this kind of video would be especially beneficial for junior developers. There are other RUclipsrs making similar content, but what sets you apart is your understanding that there's no one-size-fits-all approach to implementing clean architecture. I'd love to follow your method, and once I gain some experience, I can start experimenting with my ideas.
    By the way, have you considered creating a paid Udemy course on this topic? I'd definitely be interested!
    Also, it'd be great if you could set up a Discord server for your RUclips channel. Having a community where viewers can connect and help each other would be awesome!

    • @MilanJovanovicTech
      @MilanJovanovicTech  Год назад +9

      Actually yes, I'm going to release a Clean Architecture course in 2-3 weeks. 🚀

    • @cansozbir
      @cansozbir Год назад

      @@MilanJovanovicTech I can't wait :)

    • @jeancarlosreyesencarnacion
      @jeancarlosreyesencarnacion Год назад

      @@MilanJovanovicTech I couldn't wait to watch it. 😎💙

    • @olaoluwaolaniran6713
      @olaoluwaolaniran6713 Год назад +1

      @Milan, thank you for this piece, really useful and helpful

  • @taner-saydam
    @taner-saydam Год назад +3

    It was a helpful video. We really need this in real project. Because someone can press the save button repeatedly causing the same data occur more than once. 🤗

    • @geraldmaale
      @geraldmaale Год назад +4

      Disable the save button when processing the data. In addition, add a rate limiter to the endpoint. Some solutions can be simple instead of complications. MHO

    • @taner-saydam
      @taner-saydam Год назад

      @@geraldmaale not always 🫣

    • @jeremiahgavin9687
      @jeremiahgavin9687 Год назад

      @@geraldmaale Or he could define a unique constraint on fields in the data that qualify.

    • @MilanJovanovicTech
      @MilanJovanovicTech  Год назад

      Usually, you'd disable the save button. But this is useful if requests are retried at the gateway level or something similar

    • @olaoluwaolaniran6713
      @olaoluwaolaniran6713 Год назад

      I thought about this too. But simply disabling the save/create button at the client side provides a subtle prevention.

  • @paulkoopmans4620
    @paulkoopmans4620 Год назад +4

    @5:46; while the logic makes sense and in your mind you "create it right away" there is absolutely no guarantee that it will reliably happen.
    I even want to go as far that under load it will fail. Might not be many times... but you will get a race condition where one thread gets to run right past the if check, then paused, and the hosting OS and CLR will give time to another thread which then races past.
    I have done some serious multi-threaded code in the past, for different things, in different companies, but the bugs are almost always of the same type.
    If some code requires a guarantee to be handled in order by a single thread you are going to have to lock it and in case of async methods by using a semaphore.

    • @jeremiahgavin9687
      @jeremiahgavin9687 Год назад +1

      You sure? Wouldn't the database sit as the concurrency handler? Write in a transaction with read consistency and there won't be an issue as one request(write/read) will fail guaranteed.

    • @paulkoopmans4620
      @paulkoopmans4620 Год назад +1

      ​@@jeremiahgavin9687 yeah pretty sure. DBcontext is registered as a scoped service by default. That can be changed... but you really don't want to. The idea of the DBContext is a context per user request; therefore tracking the changes and make sure all the changes either succeed or fail for that user request.
      Only IF two user requests change EXACTLY the same record from a table, you might get some database locking. But here the two requests are inserting brand new unique records and I think that is indeed not subject to any locking on the database side.
      One can test these things out: put a break point on that second statement, fire two requests, one would hit it first of course. Open the threads window and freeze the thread. Then continue debugging... and then the other one will get passed.
      Then finally, as Milan points out in the end of the video, if you have multiple instances of your API running or other reasons you might end up with requirements where you need or want to use something else, like a different DB technology, say redis, or StateServer. Point being here that you simply should not rely on the underlying implementation.
      The whole point of this clip is that the code provides the idempotency behaviour and side effects from certain technologies should not drive it.

    • @MilanJovanovicTech
      @MilanJovanovicTech  Год назад +2

      If I can't rely on my DB guarantees and it working as expected - how can I even write correct code at all?

    • @jeremiahgavin9687
      @jeremiahgavin9687 Год назад +2

      @@paulkoopmans4620 I'm sorry, he is inserting a record with a primary key is has a unique constraint. If he tries to read for it, doesn't get it back, and goes to write it as if it's new the database will throw an exception saying that a unique constraint was going to be violated to the update to the database could not be performed. This is of course only true with a database that support unique constraints on keys/columns.

    • @nikogj9495
      @nikogj9495 Год назад

      @@jeremiahgavin9687 It might not always behaving this way. Yes, the database handles concurrency, but the application code doesn't. It has 2 lines of code (the if condition and the insert) which are not making it an "atomic" statement.
      Thus, a second thread execution could meddle in and will eventually fails one of the 2 running http requests. You'll get a 500 error instead of an expected 200 just because your thread tried to insert the requestId, thinking it was not existing, but by the time it tried, the requestId got inserted by someone else.
      Granted, this is not that big of a deal though. The client could just retry such transient errors blindlessly because... well, there's idempotency :p

  • @kodindoyannick5328
    @kodindoyannick5328 8 месяцев назад

    Thank Milan for your Great content. You're the best in all time!

  • @nandoandrade34
    @nandoandrade34 Год назад +1

    great video man, one question? Why most of you classes are internal sealed? Is there some good benefits performance?

    • @MilanJovanovicTech
      @MilanJovanovicTech  Год назад +1

      internal - for hiding implementation details
      sealed - for small perf boost, but mostly a personal preference

  • @takkerutube
    @takkerutube Год назад +3

    Good video to solve for Idempotency, thank you. This solution will work for most scenarios, however if an API processes high volume of requests, how would you handle duplicates if a request hit the API before saving the request id to the database? Also, the response from the DB could be affecter by several factors like network and load on the DB. What are your thoughts?

    • @Greenthum6
      @Greenthum6 Год назад +1

      Exactly my thoughts. Duplicate handling will be executed if both requests land at request call before calling create.
      Therefore, idempotency is not granted. You need to use a transaction across request and create. Also, error handling code is needed to catch concurrency exceptions.

    • @MilanJovanovicTech
      @MilanJovanovicTech  Год назад +3

      Did no one understand there's a unique constraint in the database, which is thread safe? There will never be two requests that can go to the handle part

    • @grumpydeveloper69
      @grumpydeveloper69 Год назад +5

      @@MilanJovanovicTech Yes the can. Two parallel requests with the same RequestId could both proceed to line 24 in the IdempotencyCommandPipelineBehviour and then be "stopped" Then one can proceed, and then the other. Your explanation on unique constraint merely mens that executing line 24 for the seond request will cause a Database Exception.
      So why not just do that and react to the Exception as an Idempotencey Guard?

    • @takkerutube
      @takkerutube Год назад

      @@MilanJovanovicTech Understood and I missed that point. It makes sense to me!

  • @mostafajawhari3501
    @mostafajawhari3501 Год назад +2

    As far as I know, if a request has already been executed, it's generally better to return the result of the old request rather than an error message.

  • @stickyamp5996
    @stickyamp5996 3 месяца назад

    Awesome video thanks, could you also explain with code how it would work when you additionally want to resend back the same response the original request made? So you dont only return default but the same response despide of the fact that api didnt do any processing. Thanks!

    • @MilanJovanovicTech
      @MilanJovanovicTech  3 месяца назад

      Yeah, I'll do another version of this video and tackle that aspect

  • @precmv
    @precmv Год назад +16

    Decent idea I suppose, but your implementation is highly vulnerable to race conditions. Two simultaneous requests could easily pass the lookup in the database, and prevent this from actually doing what you want. Also, a storage like Redis using TTL (time to live) would be much more fitting, instead of a database table that just keeps filling up.

    • @MilanJovanovicTech
      @MilanJovanovicTech  Год назад +10

      After the check, it's immeditaley stored in the DB, with a unique constraint. Race condition not possible. One of the two requests will fail to insert the request

    • @osamahamdy9970
      @osamahamdy9970 Год назад

      You can put lock on request creation to prevent race condition by allowing just one thread and the second will fail from condition

    • @jeremiahgavin9687
      @jeremiahgavin9687 Год назад

      Using a database with strong consistency would avoid the first issue. I agree TTL is a good idea, Redis or Cassandra(for high write throughput) would be great at this imo. I don't have experience with this in prod though so don't quote me!

    • @clem_grim
      @clem_grim Год назад +2

      ​@@MilanJovanovicTechbut then what happens if you have a transient error before storing the object in the db but after storing the request ID? You will end up with the request ID marked as processed but the request actually failed. I think that you should have a timeout (like a lease), after that time, if the request was not confirmed to have been successful, we can allow requests with the same request ID. Wdyt?

    • @crazyfox55
      @crazyfox55 Год назад

      ​@MilanJovanovicTech yes this does prevent the second request but it throws an exception which is not the same as what the if condition does.

  • @ahmadjaber2940
    @ahmadjaber2940 5 месяцев назад

    Great Video. I solved mine by using memory cache, since I already have developed a bit older application, so that was my only solution, is that a good solution you think?

  • @ayasaber2567
    @ayasaber2567 5 месяцев назад

    Thanks for explanation, would you pls provide me with an example for the last point you talked about multiple instances with multiple databases?

    • @MilanJovanovicTech
      @MilanJovanovicTech  5 месяцев назад

      You'd need a distributed cache to hold the request IDs

  • @AbuBakrSadiqi-b7t
    @AbuBakrSadiqi-b7t 5 месяцев назад

    great video, thanks for sharing it @Millan

  • @JosephRuhundwa
    @JosephRuhundwa Год назад

    Hello Milan. Can Redis be used for this instead of DB. Just thinking from performance point of view? Thank you again for your great video.

  • @june3878
    @june3878 Год назад +1

    Thank you!)

  • @sp11-mp9ti
    @sp11-mp9ti Год назад +1

    I feel this is a bit too much simplified. There are rare cases where this solution fails:
    If it did not get a response to the first request a client may send a request twice. However, although the actual request may have been processed and may have resulted in a failure, the second request will always return a 200-success.
    long:
    - Request A received
    - Request A idempotency ID saved
    - Request A processed and fails due to validation errors
    - Request A fail-response cannot be sent due to unexpected (network?) error
    - Clients re-submits request A
    - Idempotency key found, API returns 200

  • @olaoluwaolaniran6713
    @olaoluwaolaniran6713 Год назад +1

    Though this implementation may come with some limitations but helpful and useful for pedagogical purposes.
    I await your lecture series on clean architecture. Thank you.

    • @MilanJovanovicTech
      @MilanJovanovicTech  Год назад

      What are the limitations you see?

    • @danillomagno1726
      @danillomagno1726 Год назад +2

      One drawback is that you are forcing your clients to add a X-Idempotency-Key. This could be an issue if it is a public API that is already running for some time. This would be a breaking change.

  • @mohammadtoficmohammad3594
    @mohammadtoficmohammad3594 Год назад

    Thank you

  • @pontiacgtx4801
    @pontiacgtx4801 Год назад +1

    I wonder if the person created a Guid on every request how would you tecognize it belongs to the same request? I wonder if it isnt easier to know the same object was sent twice rather counting on the header?

  • @bloggrammer
    @bloggrammer Год назад +1

    How about making one property of the entity is unique and enforce the constraint at the database layer or app layer?

  • @AliÖzbek-y9n
    @AliÖzbek-y9n Год назад +1

    Can we use rate limit for specific endpoints to prevent from duplicate requests? For example: 1 request per second

    • @yegorandrosov6334
      @yegorandrosov6334 Год назад

      1 request per second sound like a high-load system to me. In the real world scenario you would usually see 1 request per hour

    • @jeremiahgavin9687
      @jeremiahgavin9687 Год назад

      If your system never has more than one request per second, sure. But that sounds like a rather caveman solution...

    • @MilanJovanovicTech
      @MilanJovanovicTech  Год назад +2

      Rate limiting will degrade overall performance though

  • @evilroxxx
    @evilroxxx Год назад +2

    What if the client is designed to generate a new uuid (for your header) on each new request whether it’s for the exact same or different api call. Then your solution is sadly not going to work. Instead I’d just follow the standard rules of stateless restful pattern. If the resources is the exact same on the second call check if the resource id already exists in db and return a 409 conflict instead. No need to make unrelated db calls for any requests. Also concurrency token is always helpful in case of updates too. I feel this video should not be treated as a end all solution as it’s misleading. I’d suggest you put a disclaimer stating this is only for educational purposes and not to be used in a real live scenario.

    • @evilroxxx
      @evilroxxx Год назад

      Also enforcing a single db to manage horizontal scale sounds like a single point of failure too

    • @MilanJovanovicTech
      @MilanJovanovicTech  Год назад

      What if a client is an API, with a retry mechanism?

  • @grumpydeveloper69
    @grumpydeveloper69 Год назад +1

    So this is to prevent an API user from accidentally (or on purpose) adding records that are in fact identical. Ie, I want to add a new Customer but somehow manage to call the API endpoint twice for the same new customer. These is no reord, so no Key to validate, and thuis approach seems to ask the user to give a unqiue id for the request. But yu would have to keep this requestId indefinitely correct?
    So would adding an idempotency field to the entities (database table) and adding a unique constraint on that be a way to prevent double inserting as well. Or is there a benefit to having a seperate idempotency store? Well I guess when you upscale and have multiple seperate databases tha might be something. But when you have multiple database how are you going to hadle getting back this (in my case) Customer from all those seperate databases.
    Personally I always wonder. At what point does a system really need to scale beyond 1 database (server). We have systems that have databases that are around the 500-1000GB mark which is big for a lot of applications. We can't all be google's with our storage requirements?

    • @MilanJovanovicTech
      @MilanJovanovicTech  Год назад

      The client app isn't the only one who could be sending the request to your API. What about API-to-API calls? Like calling a payment provider

    • @grumpydeveloper69
      @grumpydeveloper69 Год назад

      @@MilanJovanovicTech I understand, 'user' is not a human here. I put to much in one comment, I was more talking about adding the idempotentkey to the entity as a second unique key instead of using a seperate table to store these. Would that be a problem, I.e. would that be considered some sort of coupling?

  • @timur2887
    @timur2887 2 месяца назад

    instead of generating a request id on client and passing it into the api isn't it better to generate a hash based on request type and parameters inside the api?

  • @wellyngtond2
    @wellyngtond2 Год назад +2

    It's depends where the cliente will generate the GUID, because if this process was in a generic place, it's will generate new Guid for the "same" request and will broke this strategy.

    • @MilanJovanovicTech
      @MilanJovanovicTech  Год назад

      Not necessarily. These are "different" requests as far as the API is concerned

  • @amitkumdixit
    @amitkumdixit Год назад +1

    Better to use some kind of In memory storage Like Redis rather than DB.

  • @nove1398
    @nove1398 Год назад

    @ 6:08 how do you determine the return type for the pipeline if you have an actual return type that is not void?

    • @MilanJovanovicTech
      @MilanJovanovicTech  Год назад +2

      I didn't want to dive into that part - but a bit more boilerplate around idempotent commands would've been enough. Like defining what to return as a default value.

  • @vickytr1692
    @vickytr1692 2 месяца назад

    I have a problem with this approuch of using idempotentcy key . User can easily send duplicate request here with different key but with same parameters. Saying this is the responsibility of the user is same as not handling idempotentcy. You can say the user not to send duplicate request as well.
    Using a hash key created using the parameter body in the backend would be a better solution.

    • @MilanJovanovicTech
      @MilanJovanovicTech  2 месяца назад

      Makes sense. But why is it wrong to expect of your API clients to use the API how you design it? So long as its documented.

    • @vickytr1692
      @vickytr1692 2 месяца назад

      @@MilanJovanovicTech By relying on the client to do the right thing, we are providing a way for them to make mistakes. I personally feel that if there is a room for error it will eventually happen(happen a lot). Better not to give any room for errors.
      Moveover once there is room for error, you need to handle it to make the api resilient.

  • @tonystoynev7969
    @tonystoynev7969 Год назад

    Why do you mark all of the classes as “sealed” ? What are the benefits

    • @MilanJovanovicTech
      @MilanJovanovicTech  Год назад +4

      Slight performance benefits. And I like "sealed by convention" approach.

    • @2engle3
      @2engle3 Год назад

      Instead of persisting the request id and request name in the database; could we have it an static dictionary that is living in memory?

    • @tonystoynev7969
      @tonystoynev7969 Год назад +1

      @@2engle3 memory is not a persistent storage. What will happen with the already processed requests after an application restart

    • @davearkley7014
      @davearkley7014 Год назад

      @@2engle3 In which case this would never work if multiple instances of the API exist. A shared database table allows scaling out as Milan pointed out at the end

  • @unskeptable
    @unskeptable Год назад

    What if only a few values change in the request like timestamps ?

    • @MilanJovanovicTech
      @MilanJovanovicTech  Год назад

      Which request?

    • @olaoluwaolaniran6713
      @olaoluwaolaniran6713 Год назад

      @milan suggested making use of guid to generate the header idempotent key... this will guarantee uniqueness per request

  • @Greenthum6
    @Greenthum6 Год назад

    This is not a good solution. Every request will do a separate select and insert into database table which grows without limit. That will add a lot of latency for every request.
    It is better to use transactions inside application logic to make sure the data is kept intact and handle any concurrency issues. Duplicate calls will automatically be handled without any additional logic or need to decorate requests with extra guids.

    • @MilanJovanovicTech
      @MilanJovanovicTech  Год назад

      Okay - So just use an fast persistence like Redis

    • @Greenthum6
      @Greenthum6 Год назад

      @@MilanJovanovicTech I mean there is no need to store requests in Redis or elsewhere. Just let the application's database do its thing. Duplicate requests will cause the latter call to fail which is an expected situation. Be liberal when accepting requests and conservative when sending requests is a good approach.

  • @MegaMage79
    @MegaMage79 Год назад

    Meh. This is way too simplified. What if you receive duplicate request before you process the first one ? You will return "success" but the first request might still fail. What if client sends the same idempotency key for different commands ? How would you deal with commands that need to return data ?

    • @jeremiahgavin9687
      @jeremiahgavin9687 Год назад

      I don't think idempotency is as important on read commands if the read command is reading from a consistent database. You'll be running duplicate reads but the result will most likely be the same if they are happening at the "same time".
      "You will return "success" but the first request might still fail." - explain this?
      The client having a bug is exactly that, a client side bug.
      Your last question is good. Maybe return a Result type with ? I'm not sure...

    • @MegaMage79
      @MegaMage79 Год назад

      @@jeremiahgavin9687 1. You receive 1st request with the same reqId.
      2. You save it to the idempotency table
      3. You start processing the 1st request
      4. You receive 2nd request with the same reqId
      5. Idempotency table already contains that record, so you return 200/204 status code immediately
      6. Processing of the 1st request fails

    • @MilanJovanovicTech
      @MilanJovanovicTech  Год назад +1

      Case 1: The error will eventually propagate to the client, and the client can decide how to handle it. One of the calls "may succeed" - but we can also throw a `DuplicateReuqestException` in that case
      Case 2: Then that's the clients problem, not yours.
      Case 3: A bit more boilerplate, maybe I'll tackle it in another video

    • @jeremiahgavin9687
      @jeremiahgavin9687 Год назад

      @@MegaMage79 So you are saying the first request fails because the second succeeds? I'm confused. Are you assuming the client will cancel the first request when the second succeeds?

    • @MegaMage79
      @MegaMage79 Год назад

      @@jeremiahgavin9687 I meant that the first request might randomly fail. But the idempotency system essentially marks it as success before it's completed.
      Yes, in ideal case client shouldn't send the same request multiple times in parallel. But in reality, especially when doing microservices, clients use timeouts. So, if server hangs while processing request and client reaches timeout, it can retry it. From client's POV, it's not parallel

  • @techpc5453
    @techpc5453 Год назад +1

  • @marcioduarte2906
    @marcioduarte2906 Год назад +2

    POST might not have been the best example, as per its RFC it was designed to not be idempotent en.wikipedia.org/wiki/POST_%28HTTP%29#Affecting_server_state.
    I find that this approach, using a request id, will have many edge cases to consider and could have its own issues if not implemented correctly (which in my opinion, depending on the context might or might not be worth the effort) - Ex: Consistency - If the operation is not part of a transaction we might end up with inconsistent state between data and the request ids. Security - could an attacker use an existing request id and inject that for all calls made to the service, simulating something like a denial of service? We would also have to periodically clean up that table as it would grow quite considerable depending on the load..

    • @jeremiahgavin9687
      @jeremiahgavin9687 Год назад

      Interesting point.
      I think avoiding the denial of service could be handled by rate limiting or a DNS security function.
      Cleaning up the table could be done using a database that supports TTL.
      Fixing the consistency issue is done as you stated, use a transaction.

    • @MilanJovanovicTech
      @MilanJovanovicTech  Год назад

      We could use a non-persistent storage like Redis. Request IDs are generally short-lived

    • @marcioduarte2906
      @marcioduarte2906 Год назад

      @@jeremiahgavin9687 yes I know all that, but the point is, is that worth all the effort (depends on the context and use cases), also, there are simpler ways to implement idempotency.