Skip to content
Back to Ideas 3 min read

Safe to Run Twice

Safe to Run Twice

Here is a question worth sitting with. What happens if you connect the same repository twice? Maybe the user clicks "Connect" twice because the first click felt slow. Maybe the network hiccuped and the request was retried. Maybe a webhook redelivered an event GitHub already sent. In a naive system, the second run duplicates every pull request and doubles everyone's XP. In ours, nothing bad happens: run it again and the result is the same. That property has a name, idempotency, and the importer earns it with two different techniques for two different kinds of work.

The write: upsert

When the importer processes a pull request, it does not blindly create a row. It upserts. Upsert is a portmanteau of update and insert: "if a row like this already exists, update it in place; otherwise create it." The trick is choosing the right key to decide "a row like this". Ours is the pair (repoId, githubId), the repo plus GitHub's own PR number, which is unique and stable. So on a reimport, PR #42 does not become a second PR #42; it finds the existing row and refreshes it.

await prisma.pullRequest.upsert({
  where: { repoId_githubId: { repoId, githubId: pull.number } },
  update: { title, state, additions, deletions /* ... */ },
  create: { /* the full row */ },
});

That one word, upsert, is why reimporting is safe for the data. The rows converge to the same state no matter how many times you run it. This is the first lesson: give your records a natural key and write through it, so a repeated write changes nothing rather than duplicating.

The side effect: a guard

But data is not the only thing an import does. When a pull request is merged, we award XP. That is a side effect, and side effects do not get idempotency for free. The upsert protects the PR row, but if the merge handler ran twice it would happily award the merge bonus twice, and someone's score would drift upward every time an event redelivered.

So awarding XP needs its own guard. Before paying the merge bonus, the importer asks the ledger a question: has this pull request already earned its merge XP?

const alreadyAwarded = await prisma.xPTransaction.findFirst({
  where: { pullRequestId: prRow.id, reason: 'PR_MERGED' },
});
if (!alreadyAwarded) {
  await awardXP({ /* PR_MERGED, and the right sized bonus */ });
}

The ledger row is not just a record of the payment; it is the guard. Its existence means "this already happened, do not do it again." This is exactly once from at least once machinery: the event may arrive many times, but the effect lands once.

The lesson

The two halves are the whole point. A robust operation separates the work into:

  • Idempotent writes, made safe by a natural key and an upsert. Run them a hundred times, same result.
  • Side effects, made safe by an explicit guard that records "done" and is checked before acting.

The mindset to carry into your own code: assume it will run twice. Networks retry, users double click, queues redeliver, cron overlaps. If your operation would misbehave on a second run, it has a bug that has not happened yet. Make the writes idempotent, guard the side effects, and you can hand someone a "Connect" button without fear of what a nervous second click will do.

More from Ideas

·8 min read

Inversion of Control Doesn't Need a DI Container

We started designing a dependency scanner, and its needs led us to Inversion of Control. It turned out we were already doing it, without a DI container.

A
Anastasios Orfanidis
·7 min read

Unknown Is an Answer

A stub that holds no documents could not say whether a change still matched a query. Letting it answer unknown named a case the real database has too.

·7 min read

The Inbox That Delivers Nowhere

Signing in by emailed code puts a mailbox on the critical path. How we use Mailpit on Studomia, how our tests read the code, and what one question taught us.