Back to Ideas 3 min read

Safe to Run Twice

E
EkoHacks Team
·

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.

E

Written by

EkoHacks Team

More from Ideas

·6 min read

The Gate Went Red, and It Was Right

We added the missing check on our API specification, pushed it, and it failed on its first run. Not because anyone had forgotten to regenerate the file, but because the file was never a function of the source. It depended on whose environment last produced it, and only a machine with no environment could ever have noticed.

E
EkoHacks Team
·6 min read

Ready to Ship, Not Ready to Release

Most teams have a hidden delay between saying done and being able to ship. The goal is to be technologically ready to release at any moment, even when the feature is half built. Why our deploy workflow ships the commit that was tested rather than the newest one.

E
EkoHacks Team
·6 min read

Never Break the Build

If the software worked five minutes ago, only five minutes of change can be to blame, and debugging turns into reading. That is what a green trunk buys. It is bought by agreement, not by tooling, and one of our own checks turns out to be incapable of failing.

E
EkoHacks Team
EkoHacks

EkoHacks is a coding institute teaching employable software engineering through real projects. We build civic technology and train developers, born in Nigeria, launching in rural Greece. Global tech, local impact.

Stay Tuned

Be the first to hear about new dojos, insights, and opportunities from EkoHacks.