No history yet

Handling Duplicates

Transcript

Beau

Okay, Jo, I've been thinking about that outbox pattern we talked about. It makes sense, you know, write to the database, then a separate thing sends the message. Guarantees it goes out. But... what if the thing sending the message... hiccups?

Jo

Hiccups in what way? Like it crashes?

Beau

Yeah, exactly. So... it reads the outbox, sends the 'charge customer' message, but then crashes before it can mark the message as 'sent' in our database. When it restarts, it's going to... send it again, right?

Jo

You've just perfectly described one of the fundamental realities of distributed systems. We call that 'at-least-once' delivery. Because of the Two Generals' problem—that deep uncertainty—we can never be 100% sure the other side got the message. So, to be safe, we'd rather send it twice than not at all.

Beau

But... in our ERP, that means we could charge the customer twice. Or ship two of the same item. That's not just a technical problem, that's a 'we're losing money and customers' problem.

Jo

Exactly. So the sender's guarantee is 'I'll get it to you at least once'. This means the responsibility for handling duplicates shifts. It's now on the receiver.

Beau

The receiver? So the... the Payments service in our ERP needs to be smart enough to know it's already seen a specific charge request?

Jo

Precisely. The receiver needs to be what we call 'idempotent'.

Beau

Idem-potent. Sounds fancy. Break that down.

Jo

It just means you can perform the same action multiple times, but the result is the same as if you'd only done it once. Think about an elevator button. You press it, it lights up. You press it again... and again... and again. The elevator is still only called once. The action is idempotent.

Beau

Okay, that clicks. So we need to make our 'charge customer' action into an elevator button. How do we do that? How does the system know it's the *same* request and not a brand new one for a different order?

Jo

We give it a unique identifier. We call it an 'idempotency key'. When the Order service first decides to charge the customer, it generates a unique ID for that specific action, like a UUID. Let's call it 'charge_request_123'. It then sends the charge command *with* that key attached.

Beau

So it's like putting a tracking number on the message itself.

Jo

Perfect analogy. Now, on the receiving end—the Payments service—the first thing we do is check if we've ever seen that tracking number before.

Beau

And how do we check? Just... query the whole payments table?

Jo

You could, but we can be more efficient. The standard practice is to have a small, separate table. Let's call it `processed_messages`. It might only have two columns: `idempotency_key` and `processed_at`.

Beau

A deduplication table.

Jo

Exactly. So when the message with 'charge_request_123' comes in, we start a PostgreSQL transaction. The very first thing we do is try to insert 'charge_request_123' into that `processed_messages` table.

Beau

But what if it's already in there? From the first time the message arrived.

Jo

This is where we let the database do the heavy lifting for us. We put a `UNIQUE` constraint on the `idempotency_key` column in PostgreSQL. If we try to insert a key that's already there, the database will throw a unique violation error. Instantly.

Beau

Oh, that's clever. So our code just catches that specific error. If we get the error, we know 'Ah, this is a duplicate.' We just ignore it and send back a 'success' message, because the original action already succeeded.

Jo

You got it. That's the core of safe retry logic. If the insert into the deduplication table succeeds, we know it's a new message. We proceed with the actual business logic—charging the customer's card, updating the ledger—and then commit the whole transaction. The key and the charge are saved atomically.

Beau

So the whole 'at-least-once' delivery thing isn't something to *fear*, it's just a rule of the road we have to design for. The sender is a bit paranoid, sending messages multiple times, and the receiver is the calm one, just checking a list before doing any work.

Jo

That's the perfect way to look at it. We accept the unreliability of the network and build simple, robust guards right into our database logic. It's a shift from trying to prevent errors to simply handling them gracefully when they inevitably happen.