# Rails sostenible (VIII): controladores, jobs y clases frontera

*Octava entrega de la serie **[Rails sostenible](/search?tag=rails-sostenible)**, sobre el libro de David Bryant Copeland. Tiempo de lectura estimado: 13 minutos.*

Hay un patrón que se repite en todo el libro y que aquí se hace explícito: controladores, jobs, mailers y rake tasks son todos **clases frontera** (*boundary classes*). Viven en el lado de Rails del seam, conectan el framework con el mundo exterior, y su trabajo no es contener lógica de negocio sino **traducir y delegar**. Una vez interiorizas esto, las tres categorías de este post se vuelven la misma idea aplicada tres veces.

## El código del controlador es configuración

La afirmación más provocadora del capítulo de controladores es esta: **el código de un controlador debería leerse casi como configuración**. Declarativo, uniforme, aburrido. Un controlador no decide reglas de negocio; recibe una petición, extrae lo que necesita, llama a una operación del dominio y decide qué renderizar. Nada más.

```ruby
class CancellationsController < ApplicationController
  def create
    order  = Order.find(params[:order_id])
    result = Orders.new.cancel(order, reason: params[:reason])

    if result.cancelled?
      redirect_to order_path(order), notice: "Pedido cancelado"
    else
      redirect_to order_path(order), alert: result.error_message
    end
  end
end
```

Si todos tus controladores siguen este molde —buscar, delegar al seam, renderizar según el resultado— cualquiera que abra uno sabe qué esperar. Esa uniformidad es sostenibilidad: reduce la sorpresa a cero. El controlador gordo, con cálculos y reglas metidas entre `params`, es justo lo contrario.

## No abuses de los callbacks

Rails ofrece `before_action`, y es útil con moderación (autenticar, cargar un recurso común). Pero Copeland advierte de lo mismo que con los callbacks de los modelos: **esconden el flujo de control**. Un `before_action` que hace tres cosas y aborta la petición bajo ciertas condiciones convierte una acción aparentemente simple en algo cuyo comportamiento real está repartido por toda la clase. Pocos, evidentes, y nunca con lógica de negocio dentro.

## Convierte los parámetros a tipos ricos

Aquí hay un consejo afinado y muy valioso. Los `params` llegan **como strings** (o hashes de strings): `"2026-06-13"`, `"42"`, `"true"`. La tentación es pasarlos crudos a la lógica de negocio, que entonces tiene que lidiar con parsing y validación de formato. Copeland propone que el controlador, que está en la frontera, **convierta los parámetros a tipos ricos** antes de cruzar el seam:

```ruby
def index
  start_date = Date.parse(params[:start_date])   # de String a Date
  page       = Integer(params[:page] || 1)        # de String a Integer
  @report    = Reports.new.sales_between(start_date, page: page)
end
```

¿Por qué importa? Porque mantiene la **frontera como el único sitio donde el mundo exterior es "sucio"**. Una vez cruzado el seam, tu lógica de negocio trabaja con `Date`, `Integer`, value objects... tipos limpios y fiables, sin tener que desconfiar del formato. La conversión (y el fallo si el formato es inválido) ocurre en un único lugar predecible.

## No sobre-testees los controladores

Coherente con [el post de testing](/post/rails-sostenible-vii-testing-y-ejemplo-end-to-end): si el controlador es configuración que delega, testearlo en aislamiento con tests de controlador pesados aporta poco. La cobertura real viene de dos sitios: los **system tests** que recorren el flujo de usuario, y los **tests unitarios** de la lógica de negocio detrás del seam. Escribir tests exhaustivos de controlador es invertir esfuerzo donde menos riesgo hay.

![Resaltado de sintaxis de Ruby](fig-01.webp)

## Jobs: diferir ejecución y tolerar fallos

Los jobs en segundo plano sirven para dos cosas, dice Copeland: **diferir trabajo** que no tiene que ocurrir dentro del request (mandar un email, generar un informe) y **aumentar la tolerancia a fallos** (reintentar una integración externa que puede caerse). Y, como clase frontera que son, su regla es la misma que la del controlador: **encolar el job directamente y que el job delegue en la lógica de negocio**.

```ruby
class CancelOrderJob < ApplicationJob
  queue_as :default

  def perform(order_id, reason:)
    order = Order.find_by(id: order_id)
    return unless order   # ya no existe: nada que hacer

    Orders.new.cancel(order, reason: reason)   # delega en el seam
  end
end
```

El job no contiene la lógica de cancelar; la *invoca*. Así el mismo código de negocio se ejecuta igual desde un controlador, una consola o un job. El seam vuelve a pagar dividendos.

### Entiende cómo funciona tu backend de jobs

Copeland insiste en algo que mucha gente ignora hasta que explota: **entiende cómo funciona tu backend**. ¿Dónde guarda los jobs? ¿Qué pasa si el proceso muere a mitad? ¿Cómo reintenta? ¿Qué garantías de entrega da? No saberlo es operar a ciegas.

### Sidekiq es la mejor opción para la mayoría

Su recomendación es clara: **Sidekiq** es el mejor backend de jobs para la mayoría de equipos. Maduro, rápido, bien documentado, con un ecosistema enorme y reintentos robustos. Y dedica un apartado a **evaluar con cuidado Solid Queue** —la opción que respalda Rails moderno, sin Redis, sobre la base de datos—: prometedora y atractiva por su simplicidad operativa, pero a sopesar según las garantías y la carga que necesites. Es un buen ejemplo de su pragmatismo: no se casa con la novedad ni la rechaza, te pide que decidas con los ojos abiertos.

### Los jobs se reintentan: deben ser idempotentes

Este es el punto crítico y la fuente de los bugs más desagradables en producción. **Los jobs se reintentan.** Si un job se cae a mitad —timeout, deploy, error transitorio— el backend lo volverá a ejecutar. Por tanto, un job **debe ser idempotente**: ejecutarlo dos veces no puede tener un efecto distinto a ejecutarlo una vez. Si tu job cobra una tarjeta y no es idempotente, un reintento cobra dos veces.

```ruby
def perform(order_id)
  order = Order.find(order_id)
  return if order.charged?   # guardia de idempotencia
  Payments.new.charge(order)
end
```

Diseñar para el reintento —guardias de "ya hecho", operaciones que se pueden repetir sin daño— no es opcional: es la diferencia entre un sistema fiable y uno que corrompe datos bajo carga.

## Otras clases frontera: mailers, rake tasks y compañía

El último capítulo de esta tanda recoge el resto de clases frontera bajo la misma regla. **Mailers**: deciden formato y envío, pero el *qué* y el *cuándo* vienen de la lógica de negocio. **Rake tasks**: estupendas para tareas operativas, pero deben ser finas y delegar —nada de meter 200 líneas de lógica en un `.rake` imposible de testear—. Y lo mismo para **mailboxes** (correo entrante), **Action Cable** (websockets) y **Active Storage** (ficheros): todas son fronteras que traducen entre Rails y el exterior, y todas delegan el negocio al seam.

```ruby
# Una rake task fina que delega
namespace :orders do
  desc "Cancela pedidos pendientes de pago con más de 7 días"
  task cancel_stale: :environment do
    Orders.new.cancel_stale_unpaid(older_than: 7.days.ago)
  end
end
```

El patrón es tan consistente que casi se vuelve un mantra: **la clase frontera traduce y delega; el negocio vive en el seam.**

## Mi versión

Lo de "el controlador es configuración" es una vara de medir que uso a diario. Cuando reviso un PR y veo un controlador con un `if` anidado calculando algo, salta la alarma: eso es negocio disfrazado de controlador, y va al seam. El objetivo es que todos los controladores se parezcan tanto que sean casi tediosos de leer. Tedioso, otra vez, es el cumplido.

La idempotencia de los jobs me la enseñó la vida antes que el libro, y a base de sustos. El día que un deploy reinició los workers a mitad y media docena de jobs se reejecutaron, aprendí que "esto no va a pasar dos veces" es una ilusión peligrosa. Desde entonces, todo job lleva su guardia de "¿ya está hecho?". Copeland lo eleva a principio, y con razón.

Sobre Sidekiq vs Solid Queue, estoy justo en la duda que él describe. Sidekiq es la apuesta segura y lo que pondría en producción seria hoy. Solid Queue me tienta por quitarme Redis de encima en proyectos pequeños. Su consejo —evaluarlo con cuidado según garantías y carga— es exactamente el marco correcto: no es fe, es análisis de carrying cost.

## Lo que viene

Hemos cubierto las fronteras internas. En el **próximo post** salimos al exterior, a "más allá de Rails": **autenticación y APIs**. Veremos por qué Copeland recomienda Devise u OmniAuth en la duda, cómo plantear la autorización y los controles de acceso por rol, y un puñado de decisiones muy concretas sobre APIs —poner la versión en la URL, usar `to_json`, elegir la autenticación más simple posible— que ahorran muchísimo carrying cost.
