Saying no to scope that would break the project
A client wanted to add real-time collaborative editing to their internal CRM. Three people editing the same customer record simultaneously, with cursors and presence indicators. They asked us to scope it as a change request against the application we'd shipped six months earlier.
The application was a straightforward Rails monolith. PostgreSQL, a few Sidekiq workers, standard CRUD flows. It handled their 40-person sales team well. The collaborative editing request would have meant introducing WebSockets, an Operational Transform or CRDT layer, conflict resolution logic, and a fundamentally different state model. Roughly three weeks of work to prototype, another four to make production-ready, plus ongoing maintenance complexity that would touch every entity in the system.
We said no.
The conversation took about forty minutes. It went something like this: we explained what the feature actually required under the hood, which was more than the client expected. We asked what problem they were trying to solve. It turned out two sales reps had edited the same record on the same afternoon and the second save overwrote the first. The data loss was minor — a phone number update — but it felt alarming to the team lead.
The actual problem was concurrent overwrites, not collaboration. We added optimistic locking. Rails has `lock_version` built in; it took half a day. When two people edit the same record, the second save gets a stale-object error and we show them a diff screen. No cursors, no presence, no WebSocket infrastructure.
This is how most of these conversations go. The client asks for a specific implementation. We ask what triggered the request. The trigger is usually a real problem, but the proposed solution is often the heaviest possible fix for it.
We don't say no to everything. Last month a client asked us to add SSO via their Azure AD tenant instead of password auth. That was a reasonable request — their IT team wanted centralized access control, and the implementation was a standard OmniAuth flow. We scoped it, shipped it in a week, and it removed a class of support tickets. The scope fit the system.
The ones we decline share a pattern: they add a cross-cutting concern that re-architects how the application handles state, permissions, or data flow, and the payoff is disproportionate to the cost. Real-time collaboration on a CRUD app. A custom workflow engine when the client has three approval paths. Multi-tenant isolation retrofitted onto a single-tenant schema because one new customer asked for it.
The hardest conversations are when the client has already promised the feature to their team. We've had calls where the operations head told their CEO they'd have multi-currency support by quarter-end, and then called us to figure out how to build it. In that case the currency work was legitimate — they were expanding into UAE offices — so we did it. But we pushed back on the timeline. Multi-currency in an invoicing system touches tax calculation, reporting, payment reconciliation, and historical data. It took six weeks, not the two they'd promised.
When we decline, we try to come with an alternative. The collaborative editing request became optimistic locking. A request to build a custom notification system with per-user rules became a simpler model: event types that users can toggle on or off, no complex rule builder. A request to support arbitrary custom fields on every entity became a fixed set of extension columns on the two entities that actually needed them.
Sometimes the alternative is just naming the cost. We had a client ask for offline-first mobile sync. We wrote up what it would take: a local SQLite store, a sync engine, conflict resolution, and roughly 40% more maintenance burden on every future feature that touched synced data. They read the estimate and decided the field team could work with periodic online sync instead. That was their call, with real information.
The point is that declining scope is not about being conservative. It's about protecting the system we're going to maintain for the next two years. Every cross-cutting feature we add makes the next feature harder to build and harder to reason about. A three-person team can maintain a well-scoped application indefinitely. A three-person team maintaining an application with three layers of architectural debt cannot ship anything new without something else breaking.