Limitless Wealth
Article

How we build Rails applications

This is the standard we apply to every project. We publish it for three reasons: it is the honest answer when a client asks how we work, it saves an argument at the start of every engagement, and a contractor's real deliverable is a codebase someone else can still work in two years later.

The failure mode we are designing against

Contract work has a characteristic ending. The engagement finishes, the contractor leaves, and eighteen months later the codebase is something the team works around rather than in. Nobody did anything wrong on any given day. The accumulation is the problem.

Almost everything below is aimed at that, and the common thread is that conventions have to be enforced by machinery rather than by discipline. Discipline leaves when the contractor does.

One command to a running application

Every project ships with a single command that resets the database, runs migrations, loads fixtures and prints working login credentials for each role.

The measure that matters is how long a new engineer takes to get to a running application with usable data. If that is measured in days and involves asking someone, the project has a documentation problem it will never solve, because setup instructions rot faster than any other kind. A script that everyone runs weekly cannot rot - it breaks loudly and gets fixed.

Fixtures, not factories

This is the most contested decision we make and we hold it firmly.

Factories generate data per test. That sounds like isolation and in practice it means every test pays a setup cost, suites get slow, slow suites stop being run locally, and the ones that survive are full of setup code obscuring what is actually being tested.

Fixtures load once. The suite is dramatically faster, tests read as behaviour rather than construction, and the data is the same every run - so a failure is a real failure rather than a race in setup.

The objection is that fixtures become a shared dependency nobody dares change. That is true if they are treated as an afterthought. Treated as a designed dataset - every role, every state, every tenant, deliberately curated - they become the best documentation of the domain in the repository.

Coverage per layer, not in aggregate

A single coverage number is a comfortable lie. Ninety percent overall frequently means models are exhaustively tested and controllers barely are, because models are the easy thing to test.

We measure and enforce per layer - models, controllers, jobs, mailers, helpers, lib - each with its own gate. A gap in one cannot be concealed by strength in another, and the number stops being something to optimise and starts being something that tells you where the risk is.

System tests, parallelised, split by driver

System tests are the ones people delete because they are slow. Two things keep them alive.

Split by what they need. Most tests do not need a real browser. Run those on a fast rack driver with high parallelism, and reserve the browser driver for the genuinely JavaScript-dependent ones at lower parallelism. The ratio is usually around three to one.

Fix flakes as failures. A retried test is a broken test with the alarm disconnected. Once a suite has a known-flaky list, people stop reading the results, and then the suite is decoration.

Migrations checked before they merge

Schema changes are the most common way to take production down. Two gates run in CI on every project.

Adding a non-null column without a default, renaming in one step, adding an index without concurrency: all known-dangerous patterns, all blocked automatically. A machine should catch these, not a reviewer on a Friday afternoon.

The second gate matters more than it sounds. A migration that passes in isolation can still fail against schema changes merged in parallel, and that failure lands during a deploy rather than before one.

Lint, and custom cops for the local rules

Standard linting is table stakes. The part that pays is writing custom rules for the conventions specific to a project - a pattern this team has agreed not to use, a helper that must be used instead of a raw call, a tenancy scope that must not be bypassed.

Every convention that exists only in someone's head is one review comment away from being lost. Encoded as a cop, it survives turnover.

If a convention is not enforced by a machine, it is not a convention. It is a preference held by whoever is currently paying attention.

Codebases that agents can work in

A newer requirement, and it turns out to be the same requirement. Coding agents need what a new engineer needs, only more explicitly: a single command to a working environment, a fast deterministic test suite to verify against, conventions expressed as enforceable rules rather than folklore, and a written statement of how the project works.

We keep that written statement in the repository. It is genuinely the same document you would want for onboarding, and the projects that were already disciplined needed almost no changes.

What this costs

A few days at the start of a project, and a standing tax on adding new code paths without tests.

What it buys is the ability to change things later. Every item above exists so that a change six months from now is a contained piece of work rather than an archaeological exercise. On a contract engagement that is not a nicety - it is the difference between leaving behind an asset and leaving behind a liability.

This is how we work on every engagement. If you would like these standards applied to an existing codebase, or want a view on where yours currently stands, get in touch.

contact@limitlesswealth.xyz