- stevencodes.swe
- Posts
- stevencodes.swe - September 21, 2025
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
endSubscriber
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)
endImplementation notes
Always use
after_commit(notafter_save) to avoid invalidating cache based on data that might roll backExtract 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:
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
.envrun 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
Errors that tell you how to fix them

Return a link to some docs to help the poor developer who’s debugging this issue!
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