stevencodes.swe - September 21, 2025

Dev UX Improvements, book snippet

đź‘‹ Hey friends,

Here’s what I’ve got for you this week:

  • Snippet from Chapter 4 of The Backend Lowdown

  • Weekly Video Highlight: Dev UX Improvements

Let’s get into it 👇

The Backend Lowdown: Chapter 4 Preview

Every newsletter will include a snippet from my book in progress, The Backend Lowdown, available for $1 right now on Gumroad!

Get The Backend Lowdown →

Bust-on-Write (keep cache-aside correct)

A sequence diagram for busting a cache on write

With cache-aside, your cache only updates when there's a miss - it doesn't know when the underlying data changes. To handle updates correctly, your write path needs to delete (or "bust") the affected cache keys, forcing the next read to fetch fresh data and repopulate the cache.

When it helps

  • You want users to see their changes immediately but prefer to keep your read path simple (pure cache-aside)

  • You have derived caches (product lists, search indexes, count aggregates) that become stale when individual items change

  • Your data is read frequently but written occasionally, making invalidation cheaper than constant updates

Here are 3 ways to go about implementing bust-on-write:
Newsletter note: only showing one example here

3. Event-based bust (for multi-service or heavy fan-out)

When cache invalidation gets complex, multiple services need to know about changes, or you have many keys to bust, emit a lightweight "object changed" event and let subscribers handle the invalidation. This keeps your write path fast and simple while allowing sophisticated cache management.

Writer

after_commit do
  ActiveSupport::Notifications.instrument("product.updated",
    product_id: id, category_id: category_id)
  # That's it! The writer doesn't need to know about caching strategies
end

Subscriber

ActiveSupport::Notifications.subscribe("product.updated") do |*args|
  event = ActiveSupport::Notifications::Event.new(*args)
  pid, cid = event.payload.values_at(:product_id, :category_id)
​
  # Combine strategies: bust entity keys + bump collection version
  Rails.cache.delete_multi(%W[v3:product:#{pid}:en v3:product:#{pid}:es])
  bump_category_version!(cid)

  # Could also queue jobs for heavy work:
  # RefreshSearchIndexJob.perform_later(pid)
  # WarmPopularCachesJob.perform_later(cid)
end

Implementation notes

  • Always use after_commit (not after_save) to avoid invalidating cache based on data that might roll back

  • Extract key generation into shared helpers. Mismatched keys between reads and busts mean stale data

  • For heavy invalidation (hundreds of keys), queue a background job and use version bumping to avoid expensive wildcard deletes

  • Keep using TTL + jitter even with bust-on-write. Invalidation handles correctness, TTL provides a safety net for orphaned keys

Tripwires

  • Missing derived caches: You remembered to bust the product but forgot the category page: users see fresh items but stale lists. Either track all affected keys (for small sets) or use version bumping (for large/dynamic sets).

  • Bulk update stampedes: Updating 100 products triggers 100 cache busts, causing a thundering herd of reads to refill simultaneously. Debounce your invalidation events or use request coalescing (single-flight) during refills.

  • Race conditions in versioning: Using timestamps or manual counters for versions can create race conditions where two updates stomp on each other. Always use atomic operations like Redis INCR.

  • Missing cache variants: You bust the English version but forget Spanish, or clear the customer view but not the admin view. Make sure your bust logic covers all the variants your read path creates, or users will see each other's cached data.

Weekly Video Highlight: Dev UX Improvements

I posted a video on TikTok this week about various improvements you can do to improve the experience of working with your app.

The improvements are:

  1. Use a setup script - 1 command that installs dependencies, sets environment variables, sets up the database

Note: A few people asked “What about Docker?” Great question. Docker/Compose is awesome, but in this context it’s an implementation detail behind the one setup command. Your setup still needs to:

  • install app deps

  • create/verify .env

  • run migrations/seeds

  • check tool versions/prereqs

  • print “what to do next”

Pro tip: hide all of that behind make setup so newcomers run one command, regardless of OS or tooling. (Quite a few of you mentioned Makefiles as well which is a solid tip!)

One command to rule them all

  1. Errors that tell you how to fix them

Return a link to some docs to help the poor developer who’s debugging this issue!

  1. Logging a curl request when something blows up to instantly reproduce the issue

Log a curl request to instantly reproduce the issue (be sure to scrub any secrets!)

Here’s the video (will post to IG soon!):

@stevencodes.swe

Three small changes that massively improve your dev experience: one-command setup, errors that tell you how to fix them, and instant copy-... See more

That’s a wrap for this week. If something here made your day smoother, feel free to reply and tell me about it. And if you think a friend or teammate would enjoy this too, I’d be grateful if you shared it with them.

Until next time,
Steven