Introducción: El Dilema del Dev solitario
Hace 22 años me tocó elegir entre dos caminos.
Opción A: Arquitectura “perfecta” — patrones SOLID, tests exhaustivos, separación de concerns quirúrgica, capa de servicios, capa de presentación, inyección de dependencias. Hermoso. Mantenible. Paralizante para uno solo.
Opción B: “Write a feature, ship it, learn from users” — Un caos controlado donde puedo iterar a velocidad de startup, pero que no explote en mis manos en el mes 6.
Elegí opción B con reglas. Y después de construir desde sistemas de cotización complejos (Alugandia) hasta plataformas fitness (ChuletonApp), he encontrado la fórmula que funciona. No es perfecta. Es pragmática.
Este post es lo que aprenderé haciendo esto 3 veces más.
El Problema Real: No es el código, es tu tiempo
Cuando eres solo tú, la ecuación es diferente que en un equipo:
| Métrica | Equipo de 5 devs | Dev solitario |
|---|---|---|
| Tiempo leyendo código | 60% | 5% |
| Tiempo escribiendo código | 40% | 65% |
| Costo de refactor tonto | Bajo (otro lo ve) | MUY ALTO (estás solo) |
| ROI de test unitario al day 1 | Medio | Negativo |
| ROI de documentación | Alto | Negativo (cambias todo) |
El resultado: Si optimizas para “equipo perfecto”, mueres de hambre. Si optimizas para “ir rápido sin cuidado”, heredas deuda que te frena en mes 4.
La respuesta: Optimizar para velocidad inicial + decisiones inteligentes sobre qué mantener.
Los 4 Pilares de una Arquitectura Rails Solo-Dev
He condensado 22 años de errores en 4 pilares. No son revolucionarios. Pero funcionan.
1️⃣ Modelos Obesos, Controllers Esqueléticos
Rails promete MVC. Lamento decirte que MVC es mentira para código único.
Lo que veo en código “limpio”:
# app/controllers/quotes_controller.rb - "Limpio"
class QuotesController < ApplicationController
def create
quote = Quote.new(quote_params)
quote.calculate_total
quote.apply_discounts
quote.validate_materials
if quote.save
QuoteMailer.send_confirmation(quote).deliver_later
redirect_to quote
end
end
end
Lo que funciona en realidad (ChuletonApp + Alugandia):
# app/models/quote.rb - "Obeso pero legible"
class Quote < ApplicationRecord
belongs_to :user
has_many :line_items
validates :user, presence: true
enum status: { draft: 0, sent: 1, accepted: 2, rejected: 3 }
# TODO: Métodos que viven en Quote
def calculate_total
line_items.sum(&:subtotal)
end
def apply_discounts
return unless customer_discount
self.total = total * (1 - customer_discount / 100.0)
end
def confirm_and_send
self.status = :sent
save!
QuoteMailer.send_confirmation(self).deliver_later
ActivityLog.record(user, "quote_sent", id)
end
private
def customer_discount
case user.tier
when :gold then 15
when :silver then 10
else 0
end
end
end
# app/controllers/quotes_controller.rb - Esquelético
class QuotesController < ApplicationController
def create
quote = Quote.new(quote_params)
quote.confirm_and_send ? (redirect_to quote) : (render :new)
end
end
¿Por qué funciona?
- Controllers: 3-5 líneas. Si entiendes el modelo, entiendes el flujo.
- Cambios futuros: Edito un modelo, no 3 clases diferentes.
- Testing: Test el modelo una vez, controllers casi no fallan.
- Mantenimiento solo: Todo en un lugar que conoces.
⚠️ Límite de tolerancia: Cuando el modelo toca >500 líneas → extraer a app/services/ (pero esto lleva 6+ meses en solo-dev).
2️⃣ Servicios Simples (No inyección de dependencias)
Una verdad incómoda: Dependency Injection es una solución a un problema de equipo.
Cuando trabajas solo, tu “inyector de dependencias” eres tú. Tu cerebro.
Lo que NO hagas:
# DON'T: Sobrecomplejidad prematura
class StravaApiService
def initialize(http_client = StravaHttpClient.new, logger = Logger.new)
@http_client = http_client
@logger = logger
end
def fetch_activities(user)
@logger.info "Fetching activities for #{user.id}"
@http_client.get("/api/activities", token: user.strava_token)
end
end
Lo que SÍ hagas:
# DO: Servicios aburridos que funcionan
class StravaSync
def self.sync_user(user)
new(user).sync
end
def initialize(user)
@user = user
end
def sync
activities = fetch_from_strava
store_locally(activities)
update_leagues(@user, activities)
end
private
def fetch_from_strava
# llamada directa a API
conn = Faraday.new(url: 'https://www.strava.com/api/v3')
conn.authorization :Bearer, @user.strava_token
conn.get('/athlete/activities').body
end
end
# En un job:
class SyncStravaJob < ApplicationJob
def perform(user_id)
user = User.find(user_id)
StravaSync.sync_user(user)
rescue => e
ErrorTracker.notify(e, user: user_id)
end
end
¿Por qué?
- 1-2 métodos públicos claros
- Testing: mock lo mínimo (Strava API)
- Debugging: stack trace te muestra exactamente dónde falló
- Reutilización: un job lo llama, un admin rake lo llama, easy
Cuándo extraer a servicio: Cuando la misma lógica aparece en 2+ controllers.
3️⃣ Testing Pragmático (No purista)
El mito: “100% coverage”. La realidad: pierdes 4 semanas escribiendo tests para edge cases que nunca pasan.
Mi regla del 80/20 para solo-dev:
# Sí testea ESTO (lógica de negocio):
RSpec.describe Quote, type: :model do
describe '#calculate_total_with_discounts' do
it 'applies customer tier discount correctly' do
user = create(:user, tier: :gold)
quote = create(:quote, user: user, subtotal: 100)
expect(quote.total_after_discount).to eq(85) # 15% gold discount
end
end
end
# No testees ESTO (Rails magic):
RSpec.describe User, type: :model do
it 'validates presence of email' # Rails lo valida
it 'creates a timestamp' # Rails lo hace
it 'encrypts password' # Devise ya lo testea
end
# Sistema de tests real en ChuletonApp:
# ✅ Punto scoring: probado 100% (es el core)
# ✅ Validación Garmin OAuth: probado (falla costoso)
# ✅ Cálculo ranking: probado 100%
# ❌ Vistas: probado con screenshot solo
# ❌ Emails: probado manual (son templates)
# ❌ Job scheduling: probado manual (es n8n, no código)
Cobertura real ChuletonApp: 45% por herramienta, pero 100% de rutas críticas.
Ahorro de tiempo: 20 horas de tests que no escribo = 5 features más.
4️⃣ Estructura de Carpetas: Simple y Predecible
Evita la “carpeta de servicios del caos” donde viven 47 archivos sin relación.
Mi estructura (después de 22 años):
app/
├── models/ # Lógica de negocio + validaciones
│ ├── user.rb
│ ├── quote.rb
│ └── line_item.rb
├── controllers/ # Routing → modelo → render
│ └── quotes_controller.rb
├── jobs/ # Background tasks
│ ├── sync_strava_job.rb
│ └── send_digest_email_job.rb
├── services/ # Solo si aparecen 2+ veces
│ └── strava_sync.rb
├── mailers/ # Emails
├── views/
├── lib/ # Utilidades que no son Rails
│ └── point_calculator.rb
└── concerns/ # Mixins reutilizables (raro)
config/
├── routes.rb
└── initializers/
└── sidekiq.rb
Regla de oro: Si no sabes dónde poner algo, va en models/ primero. Cuando crezca (6+ meses), la mueves.
Qué NO hago:
- ❌
app/presenters/(overkill para HTML) - ❌
app/decorators/(lógica de vista = en modelo) - ❌
app/repositories/(ActiveRecord YA es repository) - ❌
app/serializers/(jBuilder o JSON:API lo hace todo)
Patrones Concretos que Funcionan
Patrón: Polymorphic Jobs para múltiples contextos
En Alugandia, necesitaba mandar notificaciones por WhatsApp, Email, y Slack.
# app/models/notification.rb
class Notification < ApplicationRecord
belongs_to :notifiable, polymorphic: true
enum channel: { email: 0, whatsapp: 1, slack: 2 }
after_create :queue_delivery
private
def queue_delivery
case channel
when 'email'
NotificationMailer.send_notification(self).deliver_later
when 'whatsapp'
SendWhatsappJob.perform_later(id)
when 'slack'
SendSlackJob.perform_later(id)
end
end
end
# Usage:
quote = Quote.last
Notification.create!(
notifiable: quote,
channel: :whatsapp,
recipient_phone: user.phone
)
Ventaja: Un modelo, múltiples comportamientos. Un cambio = en un lugar.
Patrón: Concern para lógica reutilizada (raro, pero efectivo)
En ChuletonApp y Alugandia, ambos necesitan “registrar cambios”.
# app/models/concerns/auditable.rb
module Auditable
extend ActiveSupport::Concern
included do
has_many :audit_logs, as: :auditable
after_update :log_changes
end
private
def log_changes
changes.each do |field, (old_val, new_val)|
AuditLog.create!(
auditable: self,
field: field,
old_value: old_val,
new_value: new_val,
changed_by: Current.user
)
end
end
end
# app/models/quote.rb
class Quote < ApplicationRecord
include Auditable
end
# app/models/user.rb
class User < ApplicationRecord
include Auditable
end
Cuándo usar: Cuando la lógica aparece en 2+ modelos Y es compleja. Si es simple, no.
Métricas de Verdad: Cómo midió esto
Llevo datos desde 2006. Aquí está la realidad:
| Proyecto | Años activo | Tiempo dev/semana | Líneas modelo | Tests % | Deuda técnica |
|---|---|---|---|---|---|
| Alugandia CRM | 3 años | 5h | ~2,500 | 25% | Media |
| ChuletonApp | 4 meses | 10h | ~1,200 | 45% | Baja |
| erisso WP themes | 8 años | 2h | ~800 | 15% | Alta |
Hallazgo clave: Deuda técnica correlaciona con tests, NO con líneas de código.
Hallazgo 2: Los proyectos con >1,500 líneas en un modelo → empecé a extraer servicios, la velocidad mejoró.
Hallazgo 3: Refactorizar “código limpio” es como reescribir un libro. Refactorizar “código pragmático” es editar párrafos.
❌ Errores que cometí (y aprendí)
Error 1: “Voy a usar Sidekiq para todo”
Creé 47 jobs. Algunos disparados cada 5 minutos.
El problema: Debugging distribuido = infierno solitario. Un job falla silenciosamente, un cliente espera 2 horas.
La solución: Ahora:
- ✅ Jobs para lo que tarda >5s (sync API, reportes)
- ❌ Jobs para lo que tarda <1s (notificaciones triviales → inline)
- 📊 Monitoreo básico (alertas si job falla)
Código arreglado:
# app/models/quote.rb
def confirm_and_send
self.status = :sent
save!
# Esto es fast, hazlo síncrono
QuoteMailer.send_confirmation(self).deliver_later
# Esto es lento, hazlo async
NotifySlackJob.perform_later(id)
# Esto es crítico, hazlo ahora o falla visiblemente
update_league_rankings!
end
Tiempo ahorrado: 40 horas de debugging de jobs fantasma.
Error 2: “Voy a seguir TDD al pie de la letra”
Escribí tests ANTES de código. Perfecto. Ingeniero.
El problema: Refactoricé mi código 5 veces hasta que “se sintiera bien”. Los tests se rompieron 5 veces. Pasé más tiempo haciendo pasar tests que construyendo features.
La solución: Ahora:
- ✅ TDD para lógica crítica (cálculos, scoring)
- ❌ TDD para exploración temprana (“¿cómo se vería esta feature?”)
- 🔄 Red → Green → Refactor, pero no obsesionarse con coverage
Tiempo ahorrado: 15 horas/mes de frustración con tests.
Error 3: “Voy a documentar todo impecablemente”
Escribí README hermosos, comentarios quirúrgicos, docstrings.
El problema: 3 meses después el código cambió. La documentación se quedó atrás. Ahora sirve de engaño.
La solución: Ahora:
- ❌ README extenso (nadie lo lee)
- ✅ README con: instrucciones setup (copy-paste), 1 diagrama del flujo (5 min)
- ✅ Comentarios SOLO en lógica WTF (por qué, no qué)
- ✅ Código lo suficientemente claro para no necesitar comentarios
Tiempo ahorrado: 10 horas/mes de documentación muerta.
🎯 Checklist: ¿Tu arquitectura Rails está lista para solo-dev?
MODELOS & LÓGICA
☐ Lógica de negocio = en modelos
☐ Validaciones = en modelos
☐ Controllers < 50 líneas
☐ Métodos en modelo < 30 líneas (indicio refactor)
SERVICIOS & JOBS
☐ Servicios solo si lógica aparece 2+ veces
☐ Jobs solo para tareas > 5 segundos
☐ Cada job tiene logging básico
☐ Jobs tienen reintentos automáticos
TESTING
☐ Lógica crítica = testada (80%+)
☐ Validaciones = testeadas
☐ Vistas = screenshot manuales OK
☐ No test obsession: 40-50% coverage está bien
ESTRUCTURA
☐ app/ folder sigue patrón consistente
☐ No hay carpetas "misc" o "utils"
☐ lib/ tiene código NO-Rails
☐ config/initializers tiene 1-2 archivos máximo
DEPLOYMENT & MONITORING
☐ Error tracking (Sentry/Rollbar/similar)
☐ Basic health checks (monitoring)
☐ Logs centralizados (al menos locales)
☐ Database backups automáticos
🚀 Conclusión: La arquitectura real
Después de 22 años, esto es lo que aprendí:
1. Perfecta es enemiga de Shipped
Un modelo “obeso” pero que funciona hoy es mejor que una arquitectura hermosa que será refactorizada mañana.
2. Tu tiempo es la unidad de medida
No líneas de código. No test coverage. Tu tiempo.
Si una práctica te ahorra 5 horas/semana: hazla. Si te ahorra 30 minutos pero toma 2 horas mantenerla: no la hagas.
3. La arquitectura es una decisión temporal
Hoy (mes 1-6) optimizo para velocidad. Mañana (mes 6-12) optimizo para mantenibilidad. Después (año 2+) puedo permitirme refactores.
4. Tú eres el único code reviewer
Las prácticas que importan son las que tú vas a recordar y mantener. No las que dice Twitter que son “best practices”.
📚 Para profundizar
- Growing Rails Applications (graceful degradation)
- POODR de Sandi Metz (diseño pragmático)
- The Rails Way de Obie Fernandez (si quieres ortodoxia)
Pero honestamente: lee tu propio código de hace 6 meses. Eso te enseña más.
¿Tienes una arquitectura diferente que te funciona?
Este post es lo que a mí me funciona con 10-20h/semana. Probablemente tú tienes otro sistema.
Tech Stack: Rails 7+, PostgreSQL, Sidekiq, Docker
Tiempo de lectura: 12 minutos
Aplicable a: Solopreneurs, pequeños equipos (2-3 devs)
Próximo post: “Testing Rails en 30 minutos: Lo que necesitas”