Same Request Twice Should Mean Nothing Changes
Why the vehicle registration Lambda needs to survive being run twice for the same request, and how an idempotency key turns a dangerous retry into a harmless no-op.
The circuit breaker from the last post assumes retries are expected and plans around them, a message that fails gets tried again later, deliberately. That assumption has a consequence worth stating directly: if retries are expected, the same step can run more than once for the same request.
That's not a hypothetical edge case to defend against just in case. It's a documented guarantee of the service actually being used here.
The guarantee, not the exception
Inside the Step Functions Express Workflow, a Lambda function writes the confirmed vehicle details into a vehicle table once the RMV check succeeds. Step Functions Express Workflows guarantee at-least-once delivery to their targets, which means AWS itself is telling you: this step might run more than once for a single logical request.
A system is idempotent if running the same request multiple times produces the same result as running it once. Given that guarantee, idempotency here isn't a nice-to-have, it's the only way the retry behavior the whole design relies on doesn't turn into duplicate data.
The easy case: inserting a new record
Writing the vehicle details for the first time is the simple version of this problem. DynamoDB supports conditional writes, and the specific condition that matters here is attribute_not_exists.
Tell DynamoDB to insert this vehicle record only if it doesn't already exist, and a duplicate insert attempt fails cleanly instead of creating a second copy. One record, no matter how many times that step actually runs.
ExpandA retried request checks an idempotency table first. Finding a match returns the recorded result instead of repeating the action
The hard case: an action with a side effect
Inserting a row is easy because "does this record already exist" is a question the database can answer directly. Not every operation has that shape.
Picture the same problem in an e-commerce checkout instead: a Lambda function charging a customer's card. There's no natural "does this charge already exist" check built into a payment call the way there is with a database insert, and a duplicate charge is a real problem a customer will notice immediately, not an abstract correctness issue.
The fix: an idempotency key and a lookup table
The pattern here is structurally the same one the circuit breaker post used, a table that remembers something so the next attempt can check before acting.
- Attach an idempotency key to the request, a unique identifier for this specific logical operation, not for each individual retry attempt.
- Before performing the action, check an idempotency table for that key. Found it? Skip the action and return the previously recorded result. This request has already happened, whether this is attempt one or attempt three.
- Not found? Perform the action, then record the key and its result in the table, so the next retry, if there is one, finds it.
The second delivery of a duplicate request finds a record sitting there, not an opportunity to charge the card again.
Don't hand-roll this if you don't have to
Building an idempotency table by hand for one operation is manageable. Building it correctly for every Lambda function that needs it is not something worth reinventing per function, including the race conditions this pattern can introduce: two retries of the same request arriving close enough together that both check the table, find nothing yet, and both proceed to act, before either one has recorded its key.
Lambda Powertools, available for Python, Java, and TypeScript among others, ships an idempotency utility built for exactly this. It handles the key lookup, the record write, and the concurrency edge cases, wrapped around a handler instead of written from scratch each time.
Know your service's delivery guarantee before you assume anything
The broader habit worth keeping from all of this: check what a managed service actually promises before designing around an assumption. Step Functions Express Workflows commit to at-least-once delivery, so idempotency here isn't optional. Other AWS services commit to exactly-once processing for specific operations, and building unnecessary idempotency logic around one of those is wasted effort in the other direction.
Read the guarantee. Design for the guarantee that's actually documented, not the one that seems intuitive.
The Essentials
- At-least-once delivery means a step can run more than once for the same request. That's a guarantee, not an unlikely edge case.
- DynamoDB's
attribute_not_existscondition makes inserts idempotent for free. A duplicate insert fails cleanly instead of creating a second record. - Side-effecting actions, like charging a card, need an explicit idempotency key and a lookup table. There's no natural "already exists" check built into the action itself.
- Check before acting, record after acting. The second delivery of a duplicate request should find a result, not repeat the work.
- Use a library like Lambda Powertools instead of hand-rolling idempotency per function. The race conditions in a homemade version are easy to get wrong.
- Read a managed service's actual delivery guarantee before designing around it. At-least-once and exactly-once call for different amounts of defensive code.
Further Reading and Watching
Keep reading