Skip to main content
Counters are everywhere — likes, view counts, inventory, rate-limit buckets. The naïve implementation is read, add one, save — and it’s wrong the moment two requests land at the same time. This recipe shows the right way: use Esix’s increment and decrement, which translate to MongoDB’s $inc operator and run atomically on the server. Esix features used: increment, decrement on QueryBuilder.

What You’ll Build

Each endpoint runs in a single round trip and is safe under arbitrary concurrency.

The Post Model

Two counters on the document — no userId lists, no per-action collection. If you also need to know who liked a post, model that with a separate Like collection alongside; for the count itself, a number is enough.

Why Not Read-Modify-Write?

This pattern looks fine and is wrong:
If two requests run this at the same time, both read the same viewCount, both write viewCount + 1, and one of the increments vanishes. The fix is to push the increment all the way down to MongoDB:
increment returns the number of documents modified (0 when nothing matched), which is enough for the route handler to tell “found and updated” from “not found”.

View Counter

A view endpoint is the simplest case — single field, single increment:

Like and Unlike

Likes are symmetric: one route increments, the mirror route decrements. The optional second argument lets you bump by something other than 1:
Note: decrement accepts any positive number — decrement('likeCount', 5) would subtract five — and it doesn’t clamp at zero. If you need a floor, guard the call site or use a MongoDB conditional update.

Bumping by More Than One

Both helpers take an optional second argument. Some everyday examples:

Bulk Increments

increment runs against the whole where clause, so you can bump many documents at once. This is great for batch reconciliation:
updated is the count of documents $inc actually touched — useful when you want to log “processed N records” or feed a progress UI.

Pattern Notes

  • Counters belong on the parent. Don’t store viewCount in a sidecar collection — that just turns a one-document write into a join.
  • One increment per request. Avoid calling increment inside a loop; if you need to bump by N, pass N as the second argument.
  • Distinguish “missing” from “no-op”. increment returns 0 for both an empty where and a non-existent id — your route should return 404 in either case.

What’s Next