Rails Scales for the Rest of Us: How Donorbox Runs a Decade-Old Monolith with a Boring Stack

Open on YouTube ↗
Overview

Donorbox helps nonprofits raise money online and in person, and the platform behind it is a Ruby on Rails application that has been evolving for more than ten years. In this episode of On Rails, host Robby Russell talks with Hussain Akram, an engineering manager at Donorbox, and Antonio Carlos de Souza (A.C.), its chief architect. The conversation centers on one question: how far can a team get by staying close to Rails conventions? Both guests argue it gets them a long way. They describe a deliberately simple infrastructure that handles serious traffic, a CI process moved back onto developers' machines, an ongoing effort to pull older code toward Rails defaults, and agent documentation that steers AI tools toward the patterns the team wants.

32 min read
1:17

Why Both Guests Stayed with Rails

Hussain started using Rails in their third or fourth semester of a bachelor's degree, while also taking courses in object-oriented programming, data structures, assembly, and C. Following Rails conventions let them build term projects quickly without getting pulled into infrastructure questions. They describe the appeal as being able to hold a mental model of a hard problem and solve it in readable Ruby, instead of first deciding how to wire everything together. Hussain was selective about their first job after graduating in mid-2018 and looked specifically for companies using Rails, which was around Rails 3 or 4 at the time. Because they already knew the framework, their onboarding training lasted about two weeks before they joined a project.

A.C. started working in 2002 with Java on the web, after earlier frustration with how non-web applications were rolled out. They also worked with Flash and Java Struts. Their main complaint about the Java world was endless debate over how to build the same thing. The most extreme example they recall was a long argument over which logging framework to use. There were three main competitors, and the discussion was pointless, A.C. says, because dependencies required all three anyway. Around 2009 A.C. was looking for a platform without those debates. They looked at TurboGears and Django from the Python world before finding Rails, which had conventions for exactly the things they were tired of arguing about. A.C. also remembers the Java community around 2008 jumping straight to UML diagrams of design patterns instead of asking whether they had the problem a pattern solves.

What Good Rails Looks Like

Robby asked whether the two agree on what good Rails looks like across codebases. A.C. said Rails builds a sense of familiarity. When you move between applications, teams, or companies, you can guess where things live and what shape they should have. That familiarity is A.C.'s test for whether an approach is good or weird. A.C. has worked at companies that constantly fought Rails and changed every decision it makes, to the point of asking why they chose Rails at all. A.C. suspects some of this comes from boredom: Rails has already solved many problems, so people want the feeling that they made the decisions themselves. A.C. adds that this problem only exists because Rails still lets those teams deliver in the end.

Hussain's advice is to stop reinventing the wheel. Before solving a problem, look at how the community has solved it, because in their estimate 99% of the time a simpler solution already exists. The codebases Hussain has struggled with were the ones that reinvented things and became complex as a result. With Rails, Hussain says, the pieces are already in front of you, and the job is to put them in the right place.

11:55

Inside Donorbox: Teams, Kiosks, and Offline Payments

Hussain describes an engineering organization split into five or six teams, each owning one area of the service. Each team has a project manager, a designer, and a QA person. Hussain's team handles in-person fundraising, which covers live kiosks (one stands in the corner of Hussain's home office), ticketed events, text-to-give, and related features. Other teams include online fundraising, which owns the main donation form, and an experience team that builds tools for the other teams. There is also a platform team and an integrations team for Salesforce, HubSpot, and similar systems. A support team rounds this out. Its members are engineers who do high-level debugging, fix and push bugs themselves when they can, and escalate larger issues to the owning team. In total the guests estimate about 50 people.

Robby asked about unreliable internet at events. Hussain said kiosks are used by mosques and churches across the US, and some locations lack a stable connection. Stripe provides a way to process transactions offline, so the kiosk stores transactions in the device's local storage. When the device reconnects, it sends them to the server for processing.

15:04

A Monolith with a Few Support Ships

The core product is a Rails monolith. A few separate repositories exist alongside it: Android and iOS code for the kiosk, and a separate integrations application that talks to the monolith through an internal API. A.C. calls this a mothership with some support ships around it. The integrations app is essentially a background job that maps events in Donorbox to third-party tools. It has no user experience beyond that mapping.

A.C.'s test for whether something can live outside the monolith is whether the whole application could be deleted after a business change without anyone noticing. This rarely happens. A.C. joked that a business change that large would probably mean deleting the team as well.

A.C. also explained how compliance shapes architecture. Donorbox holds PCI, SOC, and other certifications, which add double checks to its processes. A.C.'s example is code review. It is easy to explain to an auditor that every pull request needs approval from someone other than its author. It is much harder to explain a homegrown integration process that runs many checks before anyone else sees the code. Weighing time lost in audits against time lost in daily process, the team concluded that a long audit costs more than asking a colleague to approve a pull request. A.C. described Donorbox as a company that didn't follow the expected startup path of breaking and vanishing. Instead it has kept growing, so old habits have to be adapted to requirements nobody expected.

A Founder Who Learned Rails to Build the Company

A.C., who joined in 2019, shared some company history. The founder built Donorbox personally, learning Rails because it was what the cool kids were using at the time. In A.C.'s words, the founder was not a programmer by trade but became one by necessity, "before Claude," which A.C. considers an important difference. Rails offered a clear learning path for someone who had never built a web application, and the founder followed it. Where the founder couldn't build something alone, they managed tasks contracted through Upwork. When A.C. joined, the founder was still doing production deployments. A.C. credits Rails with letting someone passionate build something that was useful, secure, and manageable by one person.

Shipping Fast and Paying Down the Debt Later

Hussain described the events product as an example of building fast and optimizing later. The founder had heard that many nonprofits wanted to run events for their main fundraisers, so the feature started as an experiment and was built in about three or four months, ahead of a gala season. Speed brought refactoring costs later. Because the product served nonprofits well and generated good revenue, the company could then justify investing in it. Today the in-person team of four or five developers manages events along with other products. Hussain has seen the same pattern in the main application and sees no shame in it.

25:49

Traffic and Running on Heroku

A.C. gave traffic figures measured at the edge. In the last 30 days Donorbox processed 188 million requests, excluding favicons, JavaScript, CSS, and images. (Robby restated the figure as "almost 120 million," and A.C. did not correct it, so the exact number is unclear from the conversation.) In the last 60 days, the peak was 204 donations in a single minute on July 15. A.C. says nobody in the company noticed; it wasn't a special day for anyone. Hussain added that A.C. regularly posts Cloudflare stats in Slack showing spikes that happened while everyone slept.

All of this runs on Heroku, which A.C. says the team chose for the same reason it chose Rails: the conventions already exist and work. A.C. acknowledged Heroku's reputation for being expensive but noted that Donorbox has no dedicated infrastructure team to run its own servers.

Payment Providers and a Global Donor Base

Besides Stripe, which handles far more than credit cards, Donorbox integrates PayPal and The Giving Block, which processes stock and cryptocurrency donations. According to A.C., most donations come from the US, but about 40% come from elsewhere. Europe is the second largest region, followed by the United Kingdom and India. Those regions bring their own payment methods, such as SEPA in Europe and what A.C. recalled as something like "ACSS debit."

30:28

The Life of a Donation Request

A request starts at Cloudflare, which serves the donation form, either embedded on a nonprofit's site or as a hosted page. A.C. notes these served forms were not included in the request numbers above. When the donor submits, the browser asks Stripe or PayPal to tokenize the payment method. Donorbox's infrastructure only ever receives that token. From there, A.C. says, it is essentially Heroku writing a row to Postgres.

The team tries to create a donation as a single insert into a single table, which keeps it fast because fewer locks are taken. Background jobs then extract information from the donation, for example checking whether the donor already exists and creating a record if not.

32:09

Keeping the Hot Path Fast

Robby noted that this looks unusual for a Rails app, since a typical create action might write several related records at once. He asked whether it was designed this way from the start. A.C. said the donation is by far the hottest path in the application. It was originally built around the idea of one individual making one donation to one organization. Over time that idea stretched, for instance when a company donates. Years later the team measured that keeping the operation this simple helped them scale.

A.C. was emphatic that "we're not Shopify." This is scale "for the rest of us." A.C. has been in many discussions where people claimed Rails doesn't scale, and once told someone they had more microservices than customers. A.C.'s view is that Rails scales well in most cases as long as you pay attention.

Donorbox did hit a real scaling problem, and A.C. says it was self-inflicted. The app used touch everywhere to keep counter caches updated, so creating a donation set off updates across much of the database. On a hot path that needs to insert donation after donation, stacked-up updates make the database complain, and the team had a number of outages because of it. With 100 customers you don't notice. With a thousand you see spikes if you watch the logs. A.C. said Donorbox now has more than 30,000, in context apparently customers, on one database for writes.

Robby contrasted this with e-commerce, where inventory must be checked before a sale. A.C. agreed that for donations "the more the better," but noted exceptions. Some campaigns, such as political ones, require information that must be validated before the donation is accepted, and those are the most complicated cases. Hussain added that much validation happens at the organization level, by confirming the organization is a compliant 501(c) nonprofit. Fraud prevention blocks people who are abusing the form. Beyond that, the flow is simple: the client sends JSON parameters, a donation job is enqueued, and a couple of tables are written.

37:28

Sidekiq Today, Solid Queue Tests, and Right-Sizing Dynos

Background jobs currently run on Sidekiq with Redis. The team plans to move to Solid Queue and is testing it in two of the support ships, one for the communication team and one in the integrations team, before trying it in the main app. A.C. cited storage space for jobs as a main reason. Growing Redis, particularly on Heroku, is expensive. A.C. was careful not to bash Redis, but said a relational database makes it much easier to query jobs that have run. Hussain pointed to job continuations. Currently a restart or deploy stops all running jobs, which then rerun. Most jobs finish in a second or two, but some take longer and occasionally get stuck.

Those stuck jobs are usually caught by Zacarias, whom A.C. calls the team's "server shepherd." Zacarias isn't a dedicated DevOps team but enjoys infrastructure work and watches for queries running longer than 30 seconds, queries missing caches, and queries that load half the database into memory. Hussain said Zacarias recently downsized the Sidekiq dyno from 15 GB of RAM to 2.5 GB because the extra capacity was unused.

A.C. reported that the median web dyno count over the past month was two and a half. The baseline is two, because A.C. is "too paranoid" to run just one, and it occasionally goes to three. A.C. attributes most of the bumps to deploys. A production deploy invalidates the cache, Heroku reads the result as a traffic spike and adds a dyno, then removes it about 15 minutes later.

42:40

Review Apps, QA, and a Boring Deploy

Hussain described the deploy process as simple and classic. A developer opens a PR. Each team has its own Heroku review app where the change can be deployed for the team's QA person to test. Hussain says coverage is close to 100%, and some system tests exercise the embedded versions of the donation form across configurations. QA also re-embeds forms in places like WordPress sites to check them. Changes to the donation form are rare and usually tied to major launches, such as a recently relaunched version that required thorough testing across embed variants. After QA comes code review, which any team may join. Then a single command runs all the tests and reports results to GitHub, and another internal shell command, similar to Capistrano, deploys to a staging server used for demos and then to production.

45:54

Moving from CircleCI to Local CI

Donorbox used to run CI on CircleCI. Hussain recalls a recurring problem. When two PRs merged at about the same time, coverage on the main branch dropped by fractions, blocking the next deploy and causing arguments in Slack. Around then DHH posted about moving to local CI, which the team took as validation. They built a rake-based tool that runs linting, Brakeman, and the tests, and reports results to the GitHub PR. Since only one PR can merge at a time, the coverage-drop issue went away.

A.C. said they were the one who pushed for the change and framed it differently. On CircleCI the suite took about an hour. A.C. wanted developers to experience the problems they created and to get back the feedback they had given up by outsourcing CI. A.C. also shared DORA metrics, Google's research-based DevOps measurements, which A.C. considers hard to game because the four metrics depend on each other. Last week Donorbox averaged about 10 deployments a day with 15 developers.

The tool, which A.C. calls "integrated box," has 12 steps. It runs RuboCop with autocorrection, using the full Omakase RuboCop configuration "after a lot of dispute." It runs Brakeman, the tests, and a static analysis that enforces Rails directory conventions, which A.C. credits with keeping 15 developers from drifting into different code designs. It checks that the feature branch is up to date and that the bundle is current, and it tests migrations. It requires the GitHub CLI so it can authenticate and mark the PR as checked, and that mark is a merge requirement. A.C. says it is a plain rake task and grows only when a real requirement appears.

48:56

Testing Migrations and the Case for Fixtures

To test migrations, the tool drops the developer database and rebuilds it by running every migration from scratch, instead of loading the schema, to confirm the migrations still work from earlier versions. The first version did exactly this, and the first complaint was that it wiped people's local data. A.C.'s answer was a pitch for fixtures, which A.C. jokingly introduced as "our lord and savior." People didn't realize fixtures aren't just for tests. They provide a set of known database scenarios, which A.C. argued beats having every developer learn and recreate each scenario by hand. The catch was that this conversation happened about ten years after the app was built, so the team has had to catch up on fixing fixtures.

Hussain admitted early friction. A server running in another terminal could conflict with the tool and sometimes drop the database. Hussain treats such problems as signals that something needs fixing, not reasons to abandon the process, and says they rarely see them now.

50:21

Tests as a Developer Tool and What Coverage Tells You

Running the suite locally also pushed developers to speed it up. It initially took more than 20 minutes on a developer machine. Hussain says developers proactively optimized slow tests and shared fixes, and the suite now takes seven to eight minutes. Seeing deprecation warnings locally also makes it easier to ask a colleague whether a gem needs upgrading. Hussain reports that coverage sat around 86% for a while after they joined, then rose to 100% within six to eight months of the move to local CI, though they note the change came with costs.

Robby recalled the era before hosted CI, when teams assumed everyone ran tests locally and that wasn't always true. A.C. replied that with an hour-long CI run, waiting for checks before review was exactly the problem. The tests had become a checkmark on someone else's checklist instead of a development tool. A.C. compares tests to a code editor: a tool the company not only allows but requires, for the developer's benefit. Tests don't make the PM's or manager's life easier. They spare the developer from waking up to fix a production bug nobody can explain.

On coverage, A.C. said 100% is "more noise than signal." The useful information is the list of code that could change without any automated check catching it. Reaching 100% doesn't guarantee a test meaningfully exercises a line, but it guarantees some test passes through it. If something breaks anyway, that indicates something odd in the tests.

Asked how they got the team on board, Hussain said it was easy because A.C. shared the tool on GitHub, invited opinions, and addressed suggestions, leaving a single command to run. A.C. described the organizational work. They first pitched the idea to engineering managers, arguing that developers had lost track of why they write tests. With an hour-long suite, A.C. admitted, asking for that faith was hard, so they also worked with company management to get time allocated. A.C.'s advice to teams in that position is that it is doable if you commit.

1:01:09

Moving from Slim Back to ERB

Donorbox is gradually converting Slim templates to ERB. Hussain describes it as part of the push toward pure Rails defaults. Slim was causing formatting issues, added complexity for new hires, and in Hussain's view was slower than ERB. Hussain also prefers templates that mirror what the browser renders, with visible structure and indentation, over configuring editors so they don't strip trailing spaces in Slim files. A.C. added that the team isn't committing to Rails defaults rhetorically. This is how they make the commitment tangible, with the eventual goal of removing the Slim gem. The issue isn't Slim itself but that Donorbox didn't need it.

The rule is that before changing a Slim file, you convert it to ERB first. Hussain says automatic conversion is too risky for some delicate parts of the app. Online converters and gems sometimes make mistakes, so converted files are tested, sometimes with QA involved. Hussain has been using Marco Roth's Herb tooling and praised Marco's talk in Amsterdam the previous year.

Keeping a Long-Running Migration on Track

A.C. used the Slim migration partly for team building, aiming for collective ownership without hurting delivery. A.C. asked management to make it a departmental priority announced to the whole company, so developers knew the time they spent on it counted as part of their job. A.C. connects this to Donorbox being remote-first, with people from California to Cambodia and many time zones between, Pakistan being the most represented. The migration was a reminder that they are one team.

The main motivation tool is a weekly post in the developers' channel showing how many Slim files were migrated the previous week and how many were changed without being migrated. A.C. insists it isn't meant as "a knife on people's neck" but as an invitation to ask for time. In the first weeks, some people edited Slim files without converting them, and A.C. reached out to help. After a few weeks that stopped. A.C.'s observation is that once a behavior changes, people forget why they ever did it differently.

Hiring and Onboarding with Rails Conventions

Hussain calls hiring an underrated Rails feature. A candidate with two or three years of experience can be expected to know the conventions, so onboarding mostly means explaining how Donorbox works. The team keeps an onboarding guide that takes six to eight minutes to read. It covers how teams are structured, how the code is organized, the main parts of the app, and tips for adding database columns and doing database-level optimizations. With that, Hussain says, new developers get an overall picture in one or two weeks. Hussain still consults the guide and links relevant sections when new developers ask questions, updating it when something is missing. Hussain contrasts this with stories from Node.js teams, where every project is structured differently and newcomers spend weeks reconstructing the original author's reasoning.

1:12:35

Refactoring Search Filters with ActiveModel

Donorbox needs extensive filtering for donor segmentation, for example filtering donors by number of donations made. Developers wanted to stop users from building impossible filters, such as "more than five donations and less than two," which can happen by accident, for instance by scrolling over an increment control. The original implementation raised exceptions and used flash messages to explain the error, so it was controlling user flow with exceptions.

A.C. shared Rails guides and documentation on ActiveModel without success, so A.C. asked the two developers involved for three hours together. A.C. told them it would feel tedious and they would want to ask why, since the code already worked and was deployed. A.C. stressed that the alternative wasn't necessarily better, that the merge decision was theirs, and that A.C. has never managed to make this kind of point to a large audience, only one on one.

A.C. deleted the filter code from the controllers, which got their attention, and started a plain Ruby class named, as A.C. jokes, SearchDonors. The first filter was a simple date range. When asked how to validate it, the developers suggested raising an error. A.C. instead introduced ActiveModel, which A.C. describes as the nice parts of ActiveRecord without the database, and added a validation. When the errors appeared directly in the form without extra work, A.C. says it "landed different." A.C. went on to implement all the filters in about three hours. Afterward, one developer told A.C. they had spent the whole session wondering why, but came away with the sense that if something takes too long or feels too hard, they might be doing it wrong. After more than 20 years of Rails, they probably aren't the first person with that problem. Hussain added that ActiveModel and ActiveSupport are very underrated.

1:21:41

AI Agents, Agent Docs, and an MCP-Powered Design System

Speaking in August 2026, Hussain said developers use Claude or Cursor depending on preference. A.C. added an AGENTS.md file and related files covering things like how to write comments and commit messages. The experience team built an internal gem of design components backed by an MCP server, so agents can ask how to add a button or a modal. Hussain says this has saved a great deal of time and unified the product, which previously mixed old and new designs. When Hussain's team reviews a Figma design now, they mostly think about the models and associations they need, because the front-end pieces already exist.

On steering agents away from outdated patterns, Hussain wants code to be readable and to follow domain-driven design, which matches most of the existing codebase. The non-negotiables are documented in the markdown files. A quick agent review will flag conflicts and ask whether to proceed, which Hussain calls a soft warning. A.C. said the team documents "the things we don't want to follow anymore."

Asked who decides those rules, A.C. acknowledged that one name keeps showing up in the commit history. Instead of a committee, A.C. reaches out to the people actually working in an area, describes the two approaches, and asks whether agents should help move toward the new one each time they hit the old pattern. A.C. sees LLM agents as amplifiers that magnify both your knowledge and your lack of it. The agent documents are meant to back up one person's missing context.

A.C. regretted not having Claude during the Slim migration. A.C. keeps one central AGENTS.md and, for Claude users, a symlink as CLAUDE.md. A.C. finds it funny that Claude reads the symlink fine but gets confused when trying to edit it. The core file covers what every question about the codebase needs: how to run tests and servers (just rails test and rails server), the tech stack, the Donorbox components gem and how to update it, and the application's directory structure. Specific pieces of the system get their own markdown files.

1:29:48

No Services Folder, and Integrations Wrapped in lib

Hussain said Donorbox has no app/services directory. Business logic classes live in app/models, namespaced where useful, alongside the other default directories.

The lib directory wraps third-party integrations. A.C.'s example is Cloudflare, which caches forms and hosted pages, so the cache must be purged when an organization updates a form. The code reads like a Cloudflare cache purge called with the form's slug, the same identifier used in the hosted-page and embed URLs. The wrapper translates from "Donorbox language to Cloudflare language," so Cloudflare details don't leak into application code and read like any other library. Another example is Plivo, used for text messages. A.C. stressed that Donorbox only sends texts in donor-initiated flows: a donor who has already given through a form can text to request it and give again. Donorbox doesn't send texts soliciting donations.

Hussain added that agents are discouraged from adding new CSS files. The design components should require minimal CSS, kept in the same template rather than in a stylesheet directory. If a component doesn't support what a design needs, that triggers a 15-minute conversation with the designer and the component owners to decide whether to extend the component or drop the requirement.

How Features Get Planned, and How Far to Trust Agents

Hussain outlined the feature workflow. An R&D phase starts with meetings the designer schedules to clarify goals and scope. After design, Hussain brings in a motivated developer, who spends two or three hours checking feasibility, limitations, and the deadline. Then the two hold a short meeting, about 30 minutes, to sketch a development plan, such as the migrations needed. Because the engineers are mid to senior level, an outline is enough.

Asked how close they are to handing a ticket straight to an agent and skipping review, Hussain said they are fortunate that Donorbox developers don't trust their agents too much, at least for now. Hussain uses Claude for code reviews but doesn't pass its comments directly to developers. Hussain reads them, asks why a suggestion was made, and sometimes updates the markdown files so the agent follows the team's conventions next time.

Using AI to Understand Other Teams' Code

Hussain says the biggest benefit so far comes from the codebase's size. Instead of pinging other developers or scheduling long meetings, you can ask AI how payments work, how invoices are charged, or how scheduling is done when your feature touches another team's code.

Robby asked whether this might quietly reduce conversations between colleagues and change how cohesive the team feels. Hussain doesn't think AI can answer everything yet. Many decisions were made under constraints of the time and were discussed rather than written down. For example, only a handful of people know why the events product uses Rails so differently. AI can read comments and PRs but not those discussions, unless the history is fed into the markdown files. A.C. called agents a much-improved Google search. You wouldn't ask a colleague for the exact name of a numericality validation, and you can use AI to do your homework before going to a person to ask what they were trying to prevent, which is information the AI doesn't have.

What They Want to Try Next

A.C. is interested in Lexxy, the recently released rich text editor, as an alternative to the editor Donorbox currently uses. A.C. sees it as part of a move back to "if you're going to write JavaScript, just write JavaScript," away from recreating JavaScript behavior in another language, referring to TypeScript.

Hussain is excited to adopt Solid Queue, especially job continuations. Hussain has also been reading the codebase of 37signals' Fizzy since its release and was struck by its identity model. An identity is essentially an email, and the identity's relationship to a user determines a role. Coming from Devise-style setups where everything lives on the user model, Hussain found this hard to absorb at first. Hussain likens it to a building's security guard who checks your ID without caring who you are, after which different areas of the building are open to you. Hussain thinks it could fit Donorbox, where a person may log in as a fundraiser, on behalf of an organization, or as a donor, and would volunteer to implement it. (Hussain clarified that Donorbox doesn't actually use Devise; it was just an example.)

Robby asked how someone who gives to ten organizations is represented. A.C. said that from Donorbox's perspective a donor is a person's contact record within an organization, separate from the person. Someone giving to ten organizations is ten donors, but they can manage all those donations through their own donor credentials, while each organization sees only its own relationship with them.

Book Recommendations

Hussain recommended Agile Web Development with Rails, which they called the old bible for Rails developers, along with Eloquent Ruby and Sandi Metz's Practical Object-Oriented Design in Ruby. A.C. seconded those and added The Pragmatic Programmer: From Journeyman to Master by Andrew Hunt and David Thomas. A.C. values it because it shows, through many examples, that everything in professional programming involves weighing pros and cons. It treats programming as a tool for achieving something, while acknowledging that sometimes the journey is the goal, and it calls attention to things developers take for granted.