Popularity Depends on How You Define It: How 700 RSS Bot Posts Took Over DEV's Community Hub 🤖💥
A few days ago, I opened the Forem mobile app and navigated over to the Community Hub (/community) 📱.
Naturally, you expect the "Popular Community Members" section to feature the people driving great technical discussions—authors writing insightful deep-dives, answering tough engineering questions, and earning genuine community appreciation 💬✨.
Instead, sitting proudly at the top of the leaderboard were accounts like:
- 🤖 Account A: 779 posts this month. Reactions: ~0.
- 🤖 Account B: 415 posts this month. Reactions: ~0.
Unless there is an engineer on Earth with 16 arms who writes a blog post every 40 minutes while sleeping 😴, we had a classic platform bug: Automated RSS syndication scrapers were being crowned the most "popular" developers in the community.
!Jim Carrey typing furiously
The RSS bot publishing 779 posts a month while the rest of us spend 3 days refining a single paragraph.
Here is the story of how that happened, why popularity metrics break when you measure volume instead of engagement, and how fixing it led to a wild deep-dive into PostgreSQL query planner costs with Ben Halpern 🔍📉.
---
🕵️♂️ The Crime Scene: What Did "Popular" Actually Mean?
If you don't explicitly define what "popular" means in code, PostgreSQL will define it for you—and usually in the most chaotic way possible 😅.
In CommunityController#index, the @top_authors query looked something like this:
# The old query (simplified)
@top_authors = User.where(id: recent_articles.select(:user_id))
.order("total_score DESC, recent_articles_count DESC")
.limit(8)
Look closely at what was missing:
1. 🚫 No filter on published_from_feed: Forem has an official feature that lets creators syndicate their personal blogs via RSS feeds. But the query never checked published_from_feed: false. If a feed bot syndicated 700 markdown drafts, every single one counted toward their author standing.
2. 📊 Post Count as a Tie-Breaker: By ordering with recent_articles_count DESC, sheer output volume trumped community feedback.
3. 💔 Reactions Were Ignored: A creator who spent 20 hours crafting an engineering masterclass with 100 reactions was mathematically outranked by a script dumping weather logs.
!Screenshot before bug fix
(📸 Before: Automated feed bots dominating the Popular Community Members section with hundreds of 0-reaction posts)
---
🛠️ Round 1: The "Simple" Fix
The first solution felt completely intuitive:
1. 🧹 Exclude feed-imported posts: .where(published_from_feed: false)
2. ❤️ Rank primarily by real human engagement: SUM(articles.public_reactions_count) DESC
3. 🛡️ Exclude suspended/spam users: .without_role(:suspended).without_role(:spam)
I opened PR #24324, added request specs, and pushed 🚀.
!Leonardo DiCaprio as Jordan Belfort nodding in approval
Me pushing the PR thinking I solved open-source with 3 lines of ActiveRecord.
Then, @ben Sir's automated AI review agent (open hands) ran an EXPLAIN query on the production schema...
And the query planner cost completely exploded. 💥🤯
---
⚠️ The Database Trap: Rolify's Hidden Sequential Scans
Here’s the baseline cost of the query on origin/main:
- 📊 Baseline Planner Cost: 216,129.56 (Bitmap index scan on articles + nested loop index scan on users_pkey).
With my chained .without_role(:suspended).without_role(:spam), look at what happened:
- 🚨 Planner cost jumped 4.4x to 953,586.49!
Why? 🤔
Forem uses the rolify gem for role management. Under the hood, without_role(:role_name) generates a NOT IN subquery:
WHERE users.id NOT IN (
SELECT users_roles.user_id FROM users_roles ...
)
When you chain .without_role(:suspended).without_role(:spam), ActiveRecord nests the first NOT IN subquery inside the second NOT IN subquery.
PostgreSQL looked at that nested monstrosity across nearly 1 million users and essentially threw its hands in the air:
1. 🛑 It abandoned the fast index scan on users_pkey.
2. 🔄 It triggered a Parallel Seq Scan on users across 6 workers (~348,000 rows per worker).
3. 🐢 It ran an outer full-table Seq Scan across 988,000 rows.
4. 💣 To make matters worse, /community calls <% if @top_authors.any? %> and <% @top_authors.each %>, executing that 950k-cost query twice per uncached render!
!Mr. Bean waiting and checking his watch
Waiting for PostgreSQL to run two full-table scans across 1,000,000 user rows.
!Troy from Community walking in with pizza to find the room on fire
My simple bug fix accidentally generating a database denial-of-service vector.
---
⚡ Round 2: The Correlated NOT EXISTS Anti-Join
To fix the query regression without dropping role protection, we replaced the nested NOT IN calls with a correlated NOT EXISTS anti-join:
# app/controllers/community_controller.rb
.where.not(
UserRole.joins(:role)
.where("users_roles.user_id = users.id")
.where(roles: { name: %w[suspended spam], resource_type: nil, resource_id: nil })
.select(1)
.arel.exists
)
🧠 Why this changes everything for PostgreSQL:
Instead of dumping nearly a million IDs into memory for a sluggish NOT IN array, PostgreSQL executes a clean Nested Loop Anti-Join using:
- 🎯 An Index-Only Scan on index_users_roles_on_user_id_and_role_id
- 🎯 An Index Scan on roles_pkey
🏆 The Scoreboard:
- 📉 Old baseline on main: 216,129
- 💥 My initial attempt: 953,586 (4.4x regression!)
- ⚡ With NOT EXISTS anti-join: 179,613 (Faster than main!)
!Keanu Reeves Neo dodging bullets in The Matrix
Zero sequential scans on the users table. Pure index scans.
---
🧪 Bonus: Slashing Test Suite Overhead
While profiling the specs in spec/requests/community_spec.rb, we noticed the test suite was creating 35 database records using 25.times and 10.times loops just to verify author ordering ⏱️.
By tightening the fixtures down to just 3 targeted articles (e.g., Author A with 1 post & 50 reactions vs. Author B with 2 posts & 0 reactions), we tested the exact same ordering branches while cutting down factory churn across CI shards ⚡🏎️.
---
🏁 The Aftermath
!Screenshot of community page after PR
(📸 After: Popular Community Members now showcases actual community writers based on verified engagement and public reactions)
The PR (#24324) was approved and merged into forem:main by @mikeydorje! 🎉🤝
!Leonardo DiCaprio Great Gatsby raising a champagne toast
💡 Key Lessons for Platform Builders:
1. 📢 Activity ≠ Engagement: If your algorithm rewards sheer publishing volume without weighting community response, bots will always game the system.
2. ⚠️ Beware of without_role chains: Convenience helpers in ORMs can hide expensive nested NOT IN subqueries that degrade exponentially as your tables scale.
3. 🔬 Always check EXPLAIN: An index scan vs. a sequential scan is the difference between a snappy 50ms render and taking down production during peak traffic.
Have you ever encountered a metric or leaderboard on your platform that got completely hijacked by automation? Let's chat in the comments! 👇💬
17 reactions · 7 comments
Something I wanted to see on DEV.to organizations is now a reality. 🚀
!Image description
I submitted a PR to Forem to add article pinning for organizations, and it has now been merged!
Organizations can now pin up to 5 posts, keeping their most important articles visible above the regular feed. This makes it easier to highlight announcements, useful resources, and content they want visitors to see first.
How to pin a post as an organization admin:
1. Go to your dashboard.
2. Find post you want to pin
3. Click Manage.
4. Click the Pin to Organization button.
That’s it! Your post will be pinned to your organization’s page.
A big thank you to Ben Halpern @ben for the review and guidance, and to everyone involved in getting this merged.
It feels great to contribute to a platform I actively use and see an idea turn into a real feature.
PR: forem#24058
50 reactions
Forem: open source software for building communities, the platform that powers dev.to (23k stars)
What it does
All text below is quoted directly from the project README.
>For empowering community
What is Forem?:
- Forem is open source software for building communities.
- Communities for your peers, customers, fanbases, families, friends, and any other time and space where people need to come together to be part of a collective.
- dev.to (or just DEV) is hosted by Forem.
- It is a community of software developers who write articles, take part in discussions, and build their professional profiles.
- Forem is equipped with Kamal 2 to allow you to easily deploy the application to any cloud provider or bare metal server.
- We run on a Rails backend, and we are currently transitioning to a Preact-first frontend.
>bundle exec kamal setup
Repository
forem/forem
Repository stats
|Metric|Value|
|:-|:-|
|Stars|22,812|
|Forks|4.2k|
|Last release|None|
|Languages|Ruby 62.1%, JavaScript 16.4%, HTML 11.5%|
|Contributors|756|
|AI contributor accounts (Claude / Codex / Cursor / Copilot)|@claude (58 commits), @Copilot (70 commits)|
|AI co-authored commits|Claude: 60, Copilot: 70|
Main contributor
|Field|Value|
|:-|:-|
|Name|Ben Halpern|
|GitHub|@benhalpern|
|Commits to this repo|3,203 (top contributor)|
|Bio|Founder of dev.to|
|Company / Location|Forem / New York|
|Followers|2.1k|
──────────────────────────────
📌 How many great GitHub repos have you discovered and completely forgotten about?
Don't let another useful tool disappear into your Reddit feed. Get the community's best repos, organized by category, plus key takeaways from real developer discussions, delivered to your inbox twice a week.
📬 Never lose track of great repos again. Subscribe free.
5 points
I Don’t Write Ruby. A Fix I Sent Is Now in Forem.
On Oct 22, I read the word "Merged" twice. It was my first contribution to Forem (the platform that hosts dev.to). I don't write Ruby. A coding assistant wrote the code, and it still took 12 weeks.
To explain why that word mattered, I have to start in 2019.
I kinda discovered dev.to in around April 2019 and at the time I didn't really get into it. It was just one of the many resources I'd search when I wanted to find solutions to bugs or to create features. One time or two it'd show up in my Google search and then I'd click off it. In April 2021 I finally signed up for an account on dev.to.
I heard being social is an important aspect to make in this industry. I knew at the time blogging was the easiest for me. I'd never really built a network in the industry, I'd just started following people on LinkedIn and Twitter and my projects were all unknown. I wasn't really the kind of person who'd self-promote and ask for a job. It just wasn't right for me.
I'm also an introvert. I wasn't really comfortable meeting new people and I was also very conscious of what they think of me. The Nigerian dev communities didn't help either. They were just hostile to newbies like me. I didn't even have a mentor or friend to learn with.
I only ever used the platform to read articles, and I never took the time to write any posts or comments. I didn't even remember my account existed.
In November 2025, I created another account to boost my online visibility. I'd noticed that searching my name in Google only resulted in my LinkedIn and Facebook profiles. So here I am, with 62,275 followers and 109 posts. dev.to is the second most used app on my phone (after X), and I refresh it frequently. dev.to is how I met developers outside Nigeria.
I had been wanting to dig into Forem for months, but because Forem is built on Rails, and I don't write Ruby, I held off. However, when DEV announced the Summer Bug Smash, and Jess Lee's post came out, that was the final push I needed.
The bug fix was small. It took 12 weeks to merge, most of it waiting. Here is how it went.
Looking at the code was not the easiest thing to do. I could not tell which issues I could or couldn't fix, so I used an AI coding assistant to help me figure it out.
Back in July, the repo had 34 open issues. Some of them were not actually bugs. One was a question the maintainers had already answered. One was an account problem that went to support. And none of the issues were labeled "good first issue".
So I chose #23687, which was a small bug: the Posts count on the dashboard was different than the Posts tab. Since the issue description was short, I could follow it.
The coding assistant identified the issue and told me how to fix it: the badge was including the count of statuses and archived posts, while the tab was not. The coding assistant wrote the code fix and the tests, and explained what was going on. I recognized it as a typical JavaScript issue: the count didn't match the list after filtering.
Here is the fix that was merged. First, the sidebar action sets the count:
def sidebar
@user = current_user
@organizations = @user.admin_organizations
@action = params[:state]
+ @posts_count = posts_count_for(@user)
end
Then a helper counts the same posts the tab lists:
# The "Posts" nav item always links to the default (non-archived, full posts
# only) view of the user's own dashboard, so its indicator should reflect
# that same scope rather than the user's raw articles_count, which also
# includes statuses and archived posts that never show up in that list.
def posts_count_for(user)
user.articles.from_subforem.full_posts.where(archived: false).count
end
And the badge reads that number, not articles_count:
- <span class="c-indicator"><%= @user.articles_count %></span>
+ <span class="c-indicator"><%= @posts_count %></span>
The mobile menu got the same one-line change. The rest of the PR is the test.
Since I did not have Ruby installed on my machine, I couldn't run the tests. I created a PR and let the Forem CI run the tests. I was a bit weirded out that I submitted a PR to code I couldn't read line by line.
The CI failed the first time, but the issue was in the test, not the code. The GitHub status also showed "Merging is blocked", and I assumed a maintainer would have to merge it. That was correct. I then signed the CLA, and waited.
After a while, I forgot about it, until I got a notification. Then a reviewer approved the PR on Oct 20 and noticed I had added a line I should not have. So I asked the maintainers if they wanted me to remove it, and two days later Mikey (a maintainer) removed it and merged.
First, I felt relieved, then proud. That evening, DEV sent me the DEV Contributor badge. It was sent to my old account. I remembered about my old account because of the badge email.
Small fixes are still fixes, and now, someone using the dashboard will see the correct post count.
I entered the fix in the Summer Bug Smash on Aug 3 while it was still awaiting review. And about 11 weeks later, it got merged. I did not win the Bug Smash, but this counts as a win to me.
I learned most of what I know about software development from reading open source projects: the code, documentation, and GitHub issues. This is my first contribution to Forem, and nobody asked me where I was from or what my job title was. They just reviewed my change. That is part of why I love open source.
Forem was special, because it was an open source project powering a site I used on a daily basis. The AI coding assistant transformed my side of things. It helped me submit a fix that got merged by a maintainer. And AI changed the other side of things as well. The Forem README welcomes AI-assisted pull requests, and will reject "slop". The repo has an AGENTS.md that outlines how coding assistants should work.
The review on my PR says it was written by an AI agent, and the line I should not have added passed the CI. It only got caught by the reviewer. Both sides now have AI, but it is ultimately up to a human to make a decision. That's where the trust comes in. People still question the utility of AI. After the line that slipped past CI, I understand why. I still could not have sent this fix without it.
AI might take the ability to write code out of the equation entirely. For example, I know when building an API, I need to spin up a server, create a router, set up route paths to point to handlers, serialize and deserialize the body. When learning a new language, I am trying to figure out how to accomplish these tasks, rather than trying to understand what needs to be accomplished. Removing this limitation also means I can develop with the language more appropriate for the system. AI could change the mindset about hiring, from devs who understand their language to devs who understand systems.
If you want to contribute to Forem:
- Don't wait for a "good first issue". There were none available in July.
- Any issue labeled "bug" and "always open for contribution" is up for grabs.
- Verify the issue is a real bug. Some were not.
- Check if the issue has an assignee. Some were already taken.
- Sign the CLA. That's one comment on your PR.
- Add a test. The README says all PRs should include tests.
- If you're unable to run the tests locally, state it in your PR, and let CI run the tests. I did, and the failing tests in my first run turned out to be a good thing.
- Be patient. My PR took 12 weeks.
Special thanks to Jess for posting about the Bug Smash, and for encouraging me to finally dig into the repo; Ben for creating DEV and Forem; Mikey for reviewing my PR, pushing the cleanup, and merging it; and everyone who answered my comments.
{% github forem#23690 %}
23 reactions · 11 comments
I'm Not on the DEV Team. I Still Debugged Their Missing Badges From Public Code. (And My First Test Was Wrong.)
I’m not on the DEV team. I don’t have an admin panel, production database access, or internal Slack. Everything I know about how DEV works comes from reading the public forem/forem repository. 🕵️♂️
Today, that was enough to diagnose why nobody on DEV was receiving multi-commit contributor badges, ship the service fix upstream, and get a "Thank you for this" and merge from @benhalpern Sir himself. 🚀
!4x DEV Contributor Club badge
The 4x DEV Contributor Club badge: "Awarded for 4+ merged commits to Forem — Thank you so much for your contributions!"
It also taught me a humbling lesson about tests that pass for the wrong reason. 🧪
---
1. The Itch: 4 Merged PRs, 1 Badge 🤔
Over the past few days, I went on a contribution sprint in forem/forem and merged four pull requests:
- #24003 — Docs: fix broken Kamal TOC anchor and index missing guides
- #24056 — A11y: remove duplicate aria-describedby attributes in admin forms
- #24094 — A11y: remove dangling aria-describedby references in creator settings
- #24095 — Views/Liquids: quote src and add missing alt in user_subscription tag
My 1x (dev-contributor) badge showed up right away after PR #1. ✅
When PR #4 merged, I knew from config/schedule.yml that BadgeAchievements::BadgeAwardWorker runs every hour at :20 past the hour (cron: "20 * * * *"). I checked GitHub’s Contributors API (/repos/forem/forem/contributors) and confirmed my account was sitting on page 2 with contributions: 4. Because Forem sets Octokit.auto_paginate = true, the worker was definitely reaching page 2.
One hourly cron passed. Then two. Then three. Still no 4x-commit-club badge. 💀
!Mr. Bean waiting and checking his watch
Me at :20 past the hour, every hour, refreshing my profile like it owes me money.
Instead of refreshing my profile for another 24 hours, I cracked open app/services/badges/award_contributor_from_github.rb. 🔍
---
2. Reading the Code Like a Detective 🕵️♂️
Here is how Badges::AwardContributorFromGithub looked on main:
BADGE_SLUGS = {
"dev-contributor": 1,
"4x-commit-club": 4,
"8x-commit-club": 8,
"16x-commit-club": 16,
"32x-commit-club": 32
}.freeze
When I compared award_single_commit_contributors (which awarded my 1x badge) against award_multi_commit_contributors (which handles 4x, 8x, 16x, and 32x), three bugs jumped right off the screen:
🐛 Problem 1: Silent Slug Drops
Each key in BADGE_SLUGS is resolved via Badge.id_for_slug(slug). If a slug in the production badges table doesn't match the hardcoded constant, id_for_slug returns nil. The service called .compact on the resulting hash and moved on. No error. No log line. Just a badge tier that quietly vanishes into the void. 👻
Because Forem only exposes resources :badges, only: [:index] without public /badges/:slug show routes, you can't query individual badge slugs directly. However, inspecting the public dev.to/badges catalog gave me a massive clue:
> Badge #423 on /badges:
> - Title: 4x DEV Contributor Club
> - Description: 4x-commit-club
Wait—why was 4x-commit-club sitting in the description field while the title was 4x DEV Contributor Club? Because Badge#generate_slug parameterizes the title when no explicit slug is set (4x-dev-contributor-club), the stored slug column in production didn't match the hardcoded "4x-commit-club" constant at all! 💡
!Charlie Day conspiracy board
Connecting Badge#generate_slug to DOM element IDs on /badges at 9 AM with zero database access.
💌 Problem 2: Missing Context Messages
Single-commit badges were awarded with:
BadgeAchievement
.where(user_id: i.user_id, badge_id: dev_contributor_badge_id)
.first_or_create(rewarding_context_message_markdown: msg)
Multi-commit badges, however, were awarded with a bare .create:
BadgeAchievement.create(user_id: user_id, badge_id: slug_id)
Even when a multi-commit badge was created, it omitted rewarding_context_message_markdown: msg ("Thank you so much for your contributions!"). Four times the commits, zero times the thank-you note. 😭
🔁 Problem 3: Missing Idempotency Guard
create_badges_for_user called BadgeAchievement.create(...) on every hourly run instead of .where(...).first_or_create(...). Every hour, it re-attempted already-earned badges for every contributor on the list, relying on ActiveRecord's uniqueness validation to fail silently. Worse, if a badge ever had allow_multiple_awards: true, bare .create would insert a duplicate achievement every hour—firing after_create_commit :notify_recipient and send_email_notification (flooding your inbox from yo@dev.to) each time. 📬💥
---
3. Filing the Issue & Shipping PR #24113 🛠️
I opened Issue #24114 laying out the slug mismatch on badge IDs 423–426, and paired it with PR #24113 to fix the service itself:
1. ✅ Attached rewarding_context_message_markdown: msg to multi-commit badge awards.
2. 🔒 Replaced bare BadgeAchievement.create with .where(...).first_or_create(...).
3. 📊 Deferred slug lookup behind the GitHub auth provider check and added logging whenever configured slugs are missing from the database.
---
4. The Plot Twist in Code Review 🎬
The initial deep-dive review on @benhalpern Sir’s account was posted by Forem's automated review agent (OpenHands), and it did something awesome—it ran a read-only lookup against the badges table and confirmed my exact diagnosis from Issue #24114:
> "I ran a read-only lookup on the badges table for the five BADGE_SLUGS keys. Only one of the five slugs resolves. The four Nx-commit-club slugs have no matching row, so create_badges_for_user skips them and nobody can earn them today. The issue reporter's guess is correct."
Bingo. 🎯
Even better, the review caught two subtle flaws in my PR that taught me more than the bug itself:
📢 Lesson A: My Rails.logger.warn Was Log Spam
I originally logged a Rails.logger.warn inside the loop for each missing slug. Why was that bad?
1. Until the production slugs were aligned, DEV would log 4 warn lines every hour.
2. Forem powers many independent communities. Most self-hosted Forems enable GitHub auth without seeding DEV-specific contributor badges. Five warn lines an hour for expected configuration turns warn into permanent background noise.
The Fix (5e36999): Collect all missing slugs and emit a single Rails.logger.info line per run. 🧹
🤡 Lesson B: My Regression Test Passed on main (For the Wrong Reason!)
To test idempotency, I wrote a spec that ran described_class.call twice and asserted that the user still had only 1 badge achievement for 4x-commit-club.
It passed! Green dots everywhere. 🟢
Except the review pointed out something brutal: that test also passed on origin/main without my fix.
!Surprised Pikachu
When your shiny new regression test passes on main without your bugfix.
Why? The badge factory defaults to allow_multiple_awards: false. On main, the second BadgeAchievement.create hit validates :badge_id, uniqueness: { scope: :user_id, if: :single_award_badge? }, failed validation silently, and left the count at 1. My test wasn't testing first_or_create at all—it was testing ActiveRecord's uniqueness validator! 🤦♂️
The Fix (5e36999): I updated the spec to create the badge with allow_multiple_awards: true (where bare .create genuinely inserts duplicates on main) and asserted expect { described_class.call }.not_to change(BadgeAchievement, :count) on the second run.
After I pushed 5e36999 and all 34 checks went green, @benhalpern Sir stepped in personally, dropped a comment:
!Ben Halpern approving PR #24113 with "Thank you for this"
That "Thank you for this" from @benhalpern Sir genuinely made my entire day! 🙌 And right after that, he merged #24113 into forem:main as d1b962e (my 5th merged PR!). 🎉
!5 merged pull requests in forem/forem
Shortly after, the badge email from yo@dev.to hit my inbox and the 4x DEV Contributor Club badge landed on my profile—complete with the "Thank you so much for your contributions!" context message my PR wired up. The code is cleaner, the PR is merged, and the green 4x circuit board is finally home. 💚
---
💡 3 Takeaways I’d Tell Past Me
1. 🔴 A regression test isn't real until you watch it fail on main. Before you celebrate a green spec, git stash your change and run the spec against main. If it still passes, you haven't written a regression test—you've written a test that agrees with you.
2. 🪵 Log levels are an operator contract. warn means someone should act. If a platform supports optional data seeds, missing optional rows are info, not warn.
3. 🔓 You don't need production access to debug production. You just need the public source code, a falsifiable hypothesis, and the curiosity to follow the trail.
Your turn: What’s the sneakiest time one of your tests passed for the completely wrong reason? Drop it in the comments 👇
---
40 reactions · 23 comments
今週のRuby Trendingから、コミュニティ運営を支える基盤を選んだ。
Foremはopen-source community softwareで、dev.xn--to-g73a1a4dn3g7itg9126a.xn--discussionmember-102oka4469lo52k profileを軸に、開発者以外のcommunityにも使える設計です。
forem/forem
2 likes · 79 views
Design for Churn
The biggest design shift of the AI era isn’t about who writes the code. It’s about how long the code is expected to live.
Churn has always been a dirty word in software. Microsoft researchers showed years ago that relative code churn predicts defect density. Adam Tornhill wrote a whole book called Your Code as a Crime Scene, where frequently changed, complex files are the first suspects. GitClear’s research projected that AI assistants would double the share of code rewritten within two weeks of being written, and framed that as downward pressure on quality. The instinct runs deep: code that keeps changing is code that was wrong.
But that instinct comes from a world where writing code was expensive. Durability was how you got your money’s worth. When a feature costs an afternoon instead of a sprint, the math flips. Rewriting something is often cheaper than carefully preserving it, and that’s only going to become more true.
Codebases are gardens now
We’ve always described software in building terms: architecture, foundations, scaffolding, load-bearing walls. Buildings are supposed to stand still.
I think codebases are becoming more like gardens. Specifically, gardens full of annuals. A gardener replants annuals every season, and nobody calls a tomato plant technical debt because it dies in October. The gardener’s real investment goes into what doesn’t turn over: the soil, the beds, the irrigation, the oak tree in the corner. Good structure is exactly what makes replanting cheap.
Code that churns a lot is essentially disposable, and disposable doesn’t have to mean sloppy. It can still be well made. It just isn’t made to last. Quality for an annual means it does its job, it’s tested at its edges, and it can come out without tearing up everything around it.
The problem is that we don’t have a picture of what high-quality churn looks like yet. Our mental model of quality is tangled up with longevity. We brag about code that has run untouched for six years. So churn happens anyway, but implicitly, and it lands wherever is most convenient.
Where churn actually lands
I looked at the last six months of Forem: 563 commits. The most-churned file is app/models/article.rb, touched in 37 of them.
Our spam handler is up there too, at 13. That one I’m happy about. Spam is a seasonal crop. Spammers change tactics constantly, and our code should churn right along with them.
article.rb is the oak tree. When the oak gets dug up every week, it means new features are rooting themselves in the core model because that was the easiest place to plant them. That’s the structural compromise. Churn isn’t the problem. Unplanned churn in load-bearing places is.
Designing for it
A few ideas for making churn explicit instead of accidental:
Name the layers. Say out loud what’s an annual and what’s a perennial. Our AGENTS.md already does a crude version of this by telling AI assistants to tread carefully around core auth, feed sorting, and cache pipelines. That should be more than a warning label. It should be the shape of the codebase.
Put contracts at the edges of the beds. Interfaces, schemas, API specs, and tests that pin behavior rather than implementation are the perennials. Whatever sits behind them can be rewritten freely, by a person or an agent.
Make removal cheap. The test of a good annual is whether you can pull it out without disturbing the bed. If something is meant to churn, it shouldn’t have tendrils in everything.
Measure churn by layer. High churn in the fast layer is a sign of health. Churn in the slow layer is the alarm.
Churn is a design input
Churn is going to happen either way. AI has made it the default rate of change for most of what we build. We can keep treating it as a symptom, or we can treat it as a design input and build the structure that lets us replant with confidence.
I will acknowledge that I have always been more of the perspective that churn is more acceptable than conventional wisdom of software design has implied. I’ve never seen the avoidance of churn as being a good thing in my practical experience. I used to see both sides of the argument, now I am staunchly in the camp that churn is positively associated with the quality and rigidity of our codebases and our systems.
I’d rather tend the garden than pretend it’s a building.
52 reactions · 8 comments
Mentions
Popularity Depends on How You Define It: How 700 RSS Bot Posts Took Over DEV's Community Hub 🤖💥 A few days ago, I opened the Forem mobile app and navigated over to the Community Hub (/community) 📱. Naturally, you expect the "Popular Community Members" section to feature the people driving great technical discussions—authors writing insightful deep-dives, answering tough engineering questions, and earning genuine community appreciation 💬✨. Instead, sitting proudly at the top of the leaderboard were accounts like: - 🤖 Account A: 779 posts this month. Reactions: ~0. - 🤖 Account B: 415 posts this month. Reactions: ~0. Unless there is an engineer on Earth with 16 arms who writes a blog post every 40 minutes while sleeping 😴, we had a classic platform bug: Automated RSS syndication scrapers were being crowned the most "popular" developers in the community. !Jim Carrey typing furiously The RSS bot publishing 779 posts a month while the rest of us spend 3 days refining a single paragraph. Here is the story of how that happened, why popularity metrics break when you measure volume instead of engagement, and how fixing it led to a wild deep-dive into PostgreSQL query planner costs with Ben Halpern 🔍📉. --- 🕵️♂️ The Crime Scene: What Did "Popular" Actually Mean? If you don't explicitly define what "popular" means in code, PostgreSQL will define it for you—and usually in the most chaotic way possible 😅. In CommunityController#index, the @top_authors query looked something like this: # The old query (simplified) @top_authors = User.where(id: recent_articles.select(:user_id)) .order("total_score DESC, recent_articles_count DESC") .limit(8) Look closely at what was missing: 1. 🚫 No filter on published_from_feed: Forem has an official feature that lets creators syndicate their personal blogs via RSS feeds. But the query never checked published_from_feed: false. If a feed bot syndicated 700 markdown drafts, every single one counted toward their author standing. 2. 📊 Post Count as a Tie-Breaker: By ordering with recent_articles_count DESC, sheer output volume trumped community feedback. 3. 💔 Reactions Were Ignored: A creator who spent 20 hours crafting an engineering masterclass with 100 reactions was mathematically outranked by a script dumping weather logs. !Screenshot before bug fix (📸 Before: Automated feed bots dominating the Popular Community Members section with hundreds of 0-reaction posts) --- 🛠️ Round 1: The "Simple" Fix The first solution felt completely intuitive: 1. 🧹 Exclude feed-imported posts: .where(published_from_feed: false) 2. ❤️ Rank primarily by real human engagement: SUM(articles.public_reactions_count) DESC 3. 🛡️ Exclude suspended/spam users: .without_role(:suspended).without_role(:spam) I opened PR #24324, added request specs, and pushed 🚀. !Leonardo DiCaprio as Jordan Belfort nodding in approval Me pushing the PR thinking I solved open-source with 3 lines of ActiveRecord. Then, @ben Sir's automated AI review agent (open hands) ran an EXPLAIN query on the production schema... And the query planner cost completely exploded. 💥🤯 --- ⚠️ The Database Trap: Rolify's Hidden Sequential Scans Here’s the baseline cost of the query on origin/main: - 📊 Baseline Planner Cost: 216,129.56 (Bitmap index scan on articles + nested loop index scan on users_pkey). With my chained .without_role(:suspended).without_role(:spam), look at what happened: - 🚨 Planner cost jumped 4.4x to 953,586.49! Why? 🤔 Forem uses the rolify gem for role management. Under the hood, without_role(:role_name) generates a NOT IN subquery: WHERE users.id NOT IN ( SELECT users_roles.user_id FROM users_roles ... ) When you chain .without_role(:suspended).without_role(:spam), ActiveRecord nests the first NOT IN subquery inside the second NOT IN subquery. PostgreSQL looked at that nested monstrosity across nearly 1 million users and essentially threw its hands in the air: 1. 🛑 It abandoned the fast index scan on users_pkey. 2. 🔄 It triggered a Parallel Seq Scan on users across 6 workers (~348,000 rows per worker). 3. 🐢 It ran an outer full-table Seq Scan across 988,000 rows. 4. 💣 To make matters worse, /community calls <% if @top_authors.any? %> and <% @top_authors.each %>, executing that 950k-cost query twice per uncached render! !Mr. Bean waiting and checking his watch Waiting for PostgreSQL to run two full-table scans across 1,000,000 user rows. !Troy from Community walking in with pizza to find the room on fire My simple bug fix accidentally generating a database denial-of-service vector. --- ⚡ Round 2: The Correlated NOT EXISTS Anti-Join To fix the query regression without dropping role protection, we replaced the nested NOT IN calls with a correlated NOT EXISTS anti-join: # app/controllers/community_controller.rb .where.not( UserRole.joins(:role) .where("users_roles.user_id = users.id") .where(roles: { name: %w[suspended spam], resource_type: nil, resource_id: nil }) .select(1) .arel.exists ) 🧠 Why this changes everything for PostgreSQL: Instead of dumping nearly a million IDs into memory for a sluggish NOT IN array, PostgreSQL executes a clean Nested Loop Anti-Join using: - 🎯 An Index-Only Scan on index_users_roles_on_user_id_and_role_id - 🎯 An Index Scan on roles_pkey 🏆 The Scoreboard: - 📉 Old baseline on main: 216,129 - 💥 My initial attempt: 953,586 (4.4x regression!) - ⚡ With NOT EXISTS anti-join: 179,613 (Faster than main!) !Keanu Reeves Neo dodging bullets in The Matrix Zero sequential scans on the users table. Pure index scans. --- 🧪 Bonus: Slashing Test Suite Overhead While profiling the specs in spec/requests/community_spec.rb, we noticed the test suite was creating 35 database records using 25.times and 10.times loops just to verify author ordering ⏱️. By tightening the fixtures down to just 3 targeted articles (e.g., Author A with 1 post & 50 reactions vs. Author B with 2 posts & 0 reactions), we tested the exact same ordering branches while cutting down factory churn across CI shards ⚡🏎️. --- 🏁 The Aftermath !Screenshot of community page after PR (📸 After: Popular Community Members now showcases actual community writers based on verified engagement and public reactions) The PR (#24324) was approved and merged into forem:main by @mikeydorje! 🎉🤝 !Leonardo DiCaprio Great Gatsby raising a champagne toast 💡 Key Lessons for Platform Builders: 1. 📢 Activity ≠ Engagement: If your algorithm rewards sheer publishing volume without weighting community response, bots will always game the system. 2. ⚠️ Beware of without_role chains: Convenience helpers in ORMs can hide expensive nested NOT IN subqueries that degrade exponentially as your tables scale. 3. 🔬 Always check EXPLAIN: An index scan vs. a sequential scan is the difference between a snappy 50ms render and taking down production during peak traffic. Have you ever encountered a metric or leaderboard on your platform that got completely hijacked by automation? Let's chat in the comments! 👇💬
Something I wanted to see on DEV.to organizations is now a reality. 🚀 !Image description I submitted a PR to Forem to add article pinning for organizations, and it has now been merged! Organizations can now pin up to 5 posts, keeping their most important articles visible above the regular feed. This makes it easier to highlight announcements, useful resources, and content they want visitors to see first. How to pin a post as an organization admin: 1. Go to your dashboard. 2. Find post you want to pin 3. Click Manage. 4. Click the Pin to Organization button. That’s it! Your post will be pinned to your organization’s page. A big thank you to Ben Halpern @ben for the review and guidance, and to everyone involved in getting this merged. It feels great to contribute to a platform I actively use and see an idea turn into a real feature. PR: forem#24058
Forem: open source software for building communities, the platform that powers dev.to (23k stars) What it does All text below is quoted directly from the project README. >For empowering community What is Forem?: - Forem is open source software for building communities. - Communities for your peers, customers, fanbases, families, friends, and any other time and space where people need to come together to be part of a collective. - dev.to (or just DEV) is hosted by Forem. - It is a community of software developers who write articles, take part in discussions, and build their professional profiles. - Forem is equipped with Kamal 2 to allow you to easily deploy the application to any cloud provider or bare metal server. - We run on a Rails backend, and we are currently transitioning to a Preact-first frontend. >bundle exec kamal setup Repository forem/forem Repository stats |Metric|Value| |:-|:-| |Stars|22,812| |Forks|4.2k| |Last release|None| |Languages|Ruby 62.1%, JavaScript 16.4%, HTML 11.5%| |Contributors|756| |AI contributor accounts (Claude / Codex / Cursor / Copilot)|@claude (58 commits), @Copilot (70 commits)| |AI co-authored commits|Claude: 60, Copilot: 70| Main contributor |Field|Value| |:-|:-| |Name|Ben Halpern| |GitHub|@benhalpern| |Commits to this repo|3,203 (top contributor)| |Bio|Founder of dev.to| |Company / Location|Forem / New York| |Followers|2.1k| ────────────────────────────── 📌 How many great GitHub repos have you discovered and completely forgotten about? Don't let another useful tool disappear into your Reddit feed. Get the community's best repos, organized by category, plus key takeaways from real developer discussions, delivered to your inbox twice a week. 📬 Never lose track of great repos again. Subscribe free.
I Don’t Write Ruby. A Fix I Sent Is Now in Forem. On Oct 22, I read the word "Merged" twice. It was my first contribution to Forem (the platform that hosts dev.to). I don't write Ruby. A coding assistant wrote the code, and it still took 12 weeks. To explain why that word mattered, I have to start in 2019. I kinda discovered dev.to in around April 2019 and at the time I didn't really get into it. It was just one of the many resources I'd search when I wanted to find solutions to bugs or to create features. One time or two it'd show up in my Google search and then I'd click off it. In April 2021 I finally signed up for an account on dev.to. I heard being social is an important aspect to make in this industry. I knew at the time blogging was the easiest for me. I'd never really built a network in the industry, I'd just started following people on LinkedIn and Twitter and my projects were all unknown. I wasn't really the kind of person who'd self-promote and ask for a job. It just wasn't right for me. I'm also an introvert. I wasn't really comfortable meeting new people and I was also very conscious of what they think of me. The Nigerian dev communities didn't help either. They were just hostile to newbies like me. I didn't even have a mentor or friend to learn with. I only ever used the platform to read articles, and I never took the time to write any posts or comments. I didn't even remember my account existed. In November 2025, I created another account to boost my online visibility. I'd noticed that searching my name in Google only resulted in my LinkedIn and Facebook profiles. So here I am, with 62,275 followers and 109 posts. dev.to is the second most used app on my phone (after X), and I refresh it frequently. dev.to is how I met developers outside Nigeria. I had been wanting to dig into Forem for months, but because Forem is built on Rails, and I don't write Ruby, I held off. However, when DEV announced the Summer Bug Smash, and Jess Lee's post came out, that was the final push I needed. The bug fix was small. It took 12 weeks to merge, most of it waiting. Here is how it went. Looking at the code was not the easiest thing to do. I could not tell which issues I could or couldn't fix, so I used an AI coding assistant to help me figure it out. Back in July, the repo had 34 open issues. Some of them were not actually bugs. One was a question the maintainers had already answered. One was an account problem that went to support. And none of the issues were labeled "good first issue". So I chose #23687, which was a small bug: the Posts count on the dashboard was different than the Posts tab. Since the issue description was short, I could follow it. The coding assistant identified the issue and told me how to fix it: the badge was including the count of statuses and archived posts, while the tab was not. The coding assistant wrote the code fix and the tests, and explained what was going on. I recognized it as a typical JavaScript issue: the count didn't match the list after filtering. Here is the fix that was merged. First, the sidebar action sets the count: def sidebar @user = current_user @organizations = @user.admin_organizations @action = params[:state] + @posts_count = posts_count_for(@user) end Then a helper counts the same posts the tab lists: # The "Posts" nav item always links to the default (non-archived, full posts # only) view of the user's own dashboard, so its indicator should reflect # that same scope rather than the user's raw articles_count, which also # includes statuses and archived posts that never show up in that list. def posts_count_for(user) user.articles.from_subforem.full_posts.where(archived: false).count end And the badge reads that number, not articles_count: - <span class="c-indicator"><%= @user.articles_count %></span> + <span class="c-indicator"><%= @posts_count %></span> The mobile menu got the same one-line change. The rest of the PR is the test. Since I did not have Ruby installed on my machine, I couldn't run the tests. I created a PR and let the Forem CI run the tests. I was a bit weirded out that I submitted a PR to code I couldn't read line by line. The CI failed the first time, but the issue was in the test, not the code. The GitHub status also showed "Merging is blocked", and I assumed a maintainer would have to merge it. That was correct. I then signed the CLA, and waited. After a while, I forgot about it, until I got a notification. Then a reviewer approved the PR on Oct 20 and noticed I had added a line I should not have. So I asked the maintainers if they wanted me to remove it, and two days later Mikey (a maintainer) removed it and merged. First, I felt relieved, then proud. That evening, DEV sent me the DEV Contributor badge. It was sent to my old account. I remembered about my old account because of the badge email. Small fixes are still fixes, and now, someone using the dashboard will see the correct post count. I entered the fix in the Summer Bug Smash on Aug 3 while it was still awaiting review. And about 11 weeks later, it got merged. I did not win the Bug Smash, but this counts as a win to me. I learned most of what I know about software development from reading open source projects: the code, documentation, and GitHub issues. This is my first contribution to Forem, and nobody asked me where I was from or what my job title was. They just reviewed my change. That is part of why I love open source. Forem was special, because it was an open source project powering a site I used on a daily basis. The AI coding assistant transformed my side of things. It helped me submit a fix that got merged by a maintainer. And AI changed the other side of things as well. The Forem README welcomes AI-assisted pull requests, and will reject "slop". The repo has an AGENTS.md that outlines how coding assistants should work. The review on my PR says it was written by an AI agent, and the line I should not have added passed the CI. It only got caught by the reviewer. Both sides now have AI, but it is ultimately up to a human to make a decision. That's where the trust comes in. People still question the utility of AI. After the line that slipped past CI, I understand why. I still could not have sent this fix without it. AI might take the ability to write code out of the equation entirely. For example, I know when building an API, I need to spin up a server, create a router, set up route paths to point to handlers, serialize and deserialize the body. When learning a new language, I am trying to figure out how to accomplish these tasks, rather than trying to understand what needs to be accomplished. Removing this limitation also means I can develop with the language more appropriate for the system. AI could change the mindset about hiring, from devs who understand their language to devs who understand systems. If you want to contribute to Forem: - Don't wait for a "good first issue". There were none available in July. - Any issue labeled "bug" and "always open for contribution" is up for grabs. - Verify the issue is a real bug. Some were not. - Check if the issue has an assignee. Some were already taken. - Sign the CLA. That's one comment on your PR. - Add a test. The README says all PRs should include tests. - If you're unable to run the tests locally, state it in your PR, and let CI run the tests. I did, and the failing tests in my first run turned out to be a good thing. - Be patient. My PR took 12 weeks. Special thanks to Jess for posting about the Bug Smash, and for encouraging me to finally dig into the repo; Ben for creating DEV and Forem; Mikey for reviewing my PR, pushing the cleanup, and merging it; and everyone who answered my comments. {% github forem#23690 %}
I'm Not on the DEV Team. I Still Debugged Their Missing Badges From Public Code. (And My First Test Was Wrong.) I’m not on the DEV team. I don’t have an admin panel, production database access, or internal Slack. Everything I know about how DEV works comes from reading the public forem/forem repository. 🕵️♂️ Today, that was enough to diagnose why nobody on DEV was receiving multi-commit contributor badges, ship the service fix upstream, and get a "Thank you for this" and merge from @benhalpern Sir himself. 🚀 !4x DEV Contributor Club badge The 4x DEV Contributor Club badge: "Awarded for 4+ merged commits to Forem — Thank you so much for your contributions!" It also taught me a humbling lesson about tests that pass for the wrong reason. 🧪 --- 1. The Itch: 4 Merged PRs, 1 Badge 🤔 Over the past few days, I went on a contribution sprint in forem/forem and merged four pull requests: - #24003 — Docs: fix broken Kamal TOC anchor and index missing guides - #24056 — A11y: remove duplicate aria-describedby attributes in admin forms - #24094 — A11y: remove dangling aria-describedby references in creator settings - #24095 — Views/Liquids: quote src and add missing alt in user_subscription tag My 1x (dev-contributor) badge showed up right away after PR #1. ✅ When PR #4 merged, I knew from config/schedule.yml that BadgeAchievements::BadgeAwardWorker runs every hour at :20 past the hour (cron: "20 * * * *"). I checked GitHub’s Contributors API (/repos/forem/forem/contributors) and confirmed my account was sitting on page 2 with contributions: 4. Because Forem sets Octokit.auto_paginate = true, the worker was definitely reaching page 2. One hourly cron passed. Then two. Then three. Still no 4x-commit-club badge. 💀 !Mr. Bean waiting and checking his watch Me at :20 past the hour, every hour, refreshing my profile like it owes me money. Instead of refreshing my profile for another 24 hours, I cracked open app/services/badges/award_contributor_from_github.rb. 🔍 --- 2. Reading the Code Like a Detective 🕵️♂️ Here is how Badges::AwardContributorFromGithub looked on main: BADGE_SLUGS = { "dev-contributor": 1, "4x-commit-club": 4, "8x-commit-club": 8, "16x-commit-club": 16, "32x-commit-club": 32 }.freeze When I compared award_single_commit_contributors (which awarded my 1x badge) against award_multi_commit_contributors (which handles 4x, 8x, 16x, and 32x), three bugs jumped right off the screen: 🐛 Problem 1: Silent Slug Drops Each key in BADGE_SLUGS is resolved via Badge.id_for_slug(slug). If a slug in the production badges table doesn't match the hardcoded constant, id_for_slug returns nil. The service called .compact on the resulting hash and moved on. No error. No log line. Just a badge tier that quietly vanishes into the void. 👻 Because Forem only exposes resources :badges, only: [:index] without public /badges/:slug show routes, you can't query individual badge slugs directly. However, inspecting the public dev.to/badges catalog gave me a massive clue: > Badge #423 on /badges: > - Title: 4x DEV Contributor Club > - Description: 4x-commit-club Wait—why was 4x-commit-club sitting in the description field while the title was 4x DEV Contributor Club? Because Badge#generate_slug parameterizes the title when no explicit slug is set (4x-dev-contributor-club), the stored slug column in production didn't match the hardcoded "4x-commit-club" constant at all! 💡 !Charlie Day conspiracy board Connecting Badge#generate_slug to DOM element IDs on /badges at 9 AM with zero database access. 💌 Problem 2: Missing Context Messages Single-commit badges were awarded with: BadgeAchievement .where(user_id: i.user_id, badge_id: dev_contributor_badge_id) .first_or_create(rewarding_context_message_markdown: msg) Multi-commit badges, however, were awarded with a bare .create: BadgeAchievement.create(user_id: user_id, badge_id: slug_id) Even when a multi-commit badge was created, it omitted rewarding_context_message_markdown: msg ("Thank you so much for your contributions!"). Four times the commits, zero times the thank-you note. 😭 🔁 Problem 3: Missing Idempotency Guard create_badges_for_user called BadgeAchievement.create(...) on every hourly run instead of .where(...).first_or_create(...). Every hour, it re-attempted already-earned badges for every contributor on the list, relying on ActiveRecord's uniqueness validation to fail silently. Worse, if a badge ever had allow_multiple_awards: true, bare .create would insert a duplicate achievement every hour—firing after_create_commit :notify_recipient and send_email_notification (flooding your inbox from yo@dev.to) each time. 📬💥 --- 3. Filing the Issue & Shipping PR #24113 🛠️ I opened Issue #24114 laying out the slug mismatch on badge IDs 423–426, and paired it with PR #24113 to fix the service itself: 1. ✅ Attached rewarding_context_message_markdown: msg to multi-commit badge awards. 2. 🔒 Replaced bare BadgeAchievement.create with .where(...).first_or_create(...). 3. 📊 Deferred slug lookup behind the GitHub auth provider check and added logging whenever configured slugs are missing from the database. --- 4. The Plot Twist in Code Review 🎬 The initial deep-dive review on @benhalpern Sir’s account was posted by Forem's automated review agent (OpenHands), and it did something awesome—it ran a read-only lookup against the badges table and confirmed my exact diagnosis from Issue #24114: > "I ran a read-only lookup on the badges table for the five BADGE_SLUGS keys. Only one of the five slugs resolves. The four Nx-commit-club slugs have no matching row, so create_badges_for_user skips them and nobody can earn them today. The issue reporter's guess is correct." Bingo. 🎯 Even better, the review caught two subtle flaws in my PR that taught me more than the bug itself: 📢 Lesson A: My Rails.logger.warn Was Log Spam I originally logged a Rails.logger.warn inside the loop for each missing slug. Why was that bad? 1. Until the production slugs were aligned, DEV would log 4 warn lines every hour. 2. Forem powers many independent communities. Most self-hosted Forems enable GitHub auth without seeding DEV-specific contributor badges. Five warn lines an hour for expected configuration turns warn into permanent background noise. The Fix (5e36999): Collect all missing slugs and emit a single Rails.logger.info line per run. 🧹 🤡 Lesson B: My Regression Test Passed on main (For the Wrong Reason!) To test idempotency, I wrote a spec that ran described_class.call twice and asserted that the user still had only 1 badge achievement for 4x-commit-club. It passed! Green dots everywhere. 🟢 Except the review pointed out something brutal: that test also passed on origin/main without my fix. !Surprised Pikachu When your shiny new regression test passes on main without your bugfix. Why? The badge factory defaults to allow_multiple_awards: false. On main, the second BadgeAchievement.create hit validates :badge_id, uniqueness: { scope: :user_id, if: :single_award_badge? }, failed validation silently, and left the count at 1. My test wasn't testing first_or_create at all—it was testing ActiveRecord's uniqueness validator! 🤦♂️ The Fix (5e36999): I updated the spec to create the badge with allow_multiple_awards: true (where bare .create genuinely inserts duplicates on main) and asserted expect { described_class.call }.not_to change(BadgeAchievement, :count) on the second run. After I pushed 5e36999 and all 34 checks went green, @benhalpern Sir stepped in personally, dropped a comment: !Ben Halpern approving PR #24113 with "Thank you for this" That "Thank you for this" from @benhalpern Sir genuinely made my entire day! 🙌 And right after that, he merged #24113 into forem:main as d1b962e (my 5th merged PR!). 🎉 !5 merged pull requests in forem/forem Shortly after, the badge email from yo@dev.to hit my inbox and the 4x DEV Contributor Club badge landed on my profile—complete with the "Thank you so much for your contributions!" context message my PR wired up. The code is cleaner, the PR is merged, and the green 4x circuit board is finally home. 💚 --- 💡 3 Takeaways I’d Tell Past Me 1. 🔴 A regression test isn't real until you watch it fail on main. Before you celebrate a green spec, git stash your change and run the spec against main. If it still passes, you haven't written a regression test—you've written a test that agrees with you. 2. 🪵 Log levels are an operator contract. warn means someone should act. If a platform supports optional data seeds, missing optional rows are info, not warn. 3. 🔓 You don't need production access to debug production. You just need the public source code, a falsifiable hypothesis, and the curiosity to follow the trail. Your turn: What’s the sneakiest time one of your tests passed for the completely wrong reason? Drop it in the comments 👇 ---
今週のRuby Trendingから、コミュニティ運営を支える基盤を選んだ。 Foremはopen-source community softwareで、dev.xn--to-g73a1a4dn3g7itg9126a.xn--discussionmember-102oka4469lo52k profileを軸に、開発者以外のcommunityにも使える設計です。 forem/forem
Design for Churn The biggest design shift of the AI era isn’t about who writes the code. It’s about how long the code is expected to live. Churn has always been a dirty word in software. Microsoft researchers showed years ago that relative code churn predicts defect density. Adam Tornhill wrote a whole book called Your Code as a Crime Scene, where frequently changed, complex files are the first suspects. GitClear’s research projected that AI assistants would double the share of code rewritten within two weeks of being written, and framed that as downward pressure on quality. The instinct runs deep: code that keeps changing is code that was wrong. But that instinct comes from a world where writing code was expensive. Durability was how you got your money’s worth. When a feature costs an afternoon instead of a sprint, the math flips. Rewriting something is often cheaper than carefully preserving it, and that’s only going to become more true. Codebases are gardens now We’ve always described software in building terms: architecture, foundations, scaffolding, load-bearing walls. Buildings are supposed to stand still. I think codebases are becoming more like gardens. Specifically, gardens full of annuals. A gardener replants annuals every season, and nobody calls a tomato plant technical debt because it dies in October. The gardener’s real investment goes into what doesn’t turn over: the soil, the beds, the irrigation, the oak tree in the corner. Good structure is exactly what makes replanting cheap. Code that churns a lot is essentially disposable, and disposable doesn’t have to mean sloppy. It can still be well made. It just isn’t made to last. Quality for an annual means it does its job, it’s tested at its edges, and it can come out without tearing up everything around it. The problem is that we don’t have a picture of what high-quality churn looks like yet. Our mental model of quality is tangled up with longevity. We brag about code that has run untouched for six years. So churn happens anyway, but implicitly, and it lands wherever is most convenient. Where churn actually lands I looked at the last six months of Forem: 563 commits. The most-churned file is app/models/article.rb, touched in 37 of them. Our spam handler is up there too, at 13. That one I’m happy about. Spam is a seasonal crop. Spammers change tactics constantly, and our code should churn right along with them. article.rb is the oak tree. When the oak gets dug up every week, it means new features are rooting themselves in the core model because that was the easiest place to plant them. That’s the structural compromise. Churn isn’t the problem. Unplanned churn in load-bearing places is. Designing for it A few ideas for making churn explicit instead of accidental: Name the layers. Say out loud what’s an annual and what’s a perennial. Our AGENTS.md already does a crude version of this by telling AI assistants to tread carefully around core auth, feed sorting, and cache pipelines. That should be more than a warning label. It should be the shape of the codebase. Put contracts at the edges of the beds. Interfaces, schemas, API specs, and tests that pin behavior rather than implementation are the perennials. Whatever sits behind them can be rewritten freely, by a person or an agent. Make removal cheap. The test of a good annual is whether you can pull it out without disturbing the bed. If something is meant to churn, it shouldn’t have tendrils in everything. Measure churn by layer. High churn in the fast layer is a sign of health. Churn in the slow layer is the alarm. Churn is a design input Churn is going to happen either way. AI has made it the default rate of change for most of what we build. We can keep treating it as a symptom, or we can treat it as a design input and build the structure that lets us replant with confidence. I will acknowledge that I have always been more of the perspective that churn is more acceptable than conventional wisdom of software design has implied. I’ve never seen the avoidance of churn as being a good thing in my practical experience. I used to see both sides of the argument, now I am staunchly in the camp that churn is positively associated with the quality and rigidity of our codebases and our systems. I’d rather tend the garden than pretend it’s a building.