Camadas do app Rails (modules/backend)
TLDR: o
app/segue o layout padrão do Rails —models,controllers,jobs,serializers,use_caseseservices— como define ocommons:rails2.1. Controllers e jobs chamam um use case para toda ação de negócio, e controllers podem consultar models direto (Customer.find,Customer.search); use cases são ações de negócio que devolvemSuccess/Failurecom objetos de negócio; models são ricos;services/é só integração externa.
Contexto
Este serviço orquestra Asaas, Apolo, Kommo e Slack, e a dor central do produto é o vocabulário desses sistemas ter vazado para dentro da operação. A arquitetura do back existe para manter o negócio escrito no nosso vocabulário e os terceiros confinados a uma camada de tradução.
O modelo é o layout padrão do Rails mais duas pastas, use_cases/ e services/, com um papel
fixo para cada camada:
- Entradas (
controllers/,jobs/) recebem a requisição ou o agendamento, chamam um use case e reagem ao resultado. Não decidem nada. Controllers podem consultar models direto (find,search, escopos) para ler e listar. - Use cases (
use_cases/) são as ações de negócio, nomeadas como o negócio fala (Debit::Renegotiate). Decidem quando algo acontece e o que mais acontece, e devolvemSuccess/Failurecom objetos de negócio. - Models (
models/) são ricos: definem como o próprio estado muda — cálculos, transições de status, invariantes — sem sair dos próprios dados e objetos diretos. - Services (
services/) são tradutores: um módulo por terceiro, que recebe e devolve o nosso vocabulário e nunca decide negócio. - Serializers (
serializers/) só dão forma à resposta.
O conceito central é use case + result. u-case (Micro::Case) é a implementação atual;
trocá-la não muda a arquitetura. Este documento aplica o commons:rails; em caso de conflito, ele
vence.
Decisão
Layout padrão do Rails:
app/
controllers/ → entrada HTTP: autentica, extrai params, consulta models ou
chama um use case, renderiza — zero ifs de negócio
jobs/ → entrada de background: mesma forma dos controllers
models/ → ricos: mexem só nos próprios dados e objetos diretos
use_cases/ → ações de negócio: <Contexto no singular>::<Verbo> (ex: Charge::Recalculate)
services/ → só integrações externas: um módulo por terceiro
(Asaas, Apolo, Kommo, Slack), nunca decide negócio
serializers/ → forma da resposta; não decide nada
mailers/ → e-mail de saída; só o use case dispara (deliver_later)
Controllers de API ficam sob controllers/api/v1/, com Api::V1::BaseController cuidando de
autenticação JWT e do formato de erro.
mermaid
graph LR
HTTP["HTTP / cron"] --> PO["controllers · jobs"]
PO --> UC["use_cases"]
UC --> MO["models"]
PO -.->|consulta| MO
UC --> SV["services/<br/>Asaas · Apolo · Kommo · Slack"]
UC -.->|perform_later| JO["jobs"]
SV --> EXT["APIs externas"]
PO --> SE["serializers"]
style UC fill:#1f2937,color:#fff
style SV fill:#374151,color:#fff
A direção da seta é única: controllers/jobs chamam use_cases, use_cases chamam models e
services e podem enfileirar jobs; controllers também leem models. Nada volta. A regra é convencional — a pasta não impede, a
revisão (commons:review) cobra.
Controllers
Controllers podem consultar models direto quando a action só lê: buscar um registro
(Customer.find(params[:id])), listar ou filtrar (Customer.search(scope, params), escopos,
includes, paginação) e passar o resultado ao serializer. Um use case que só repassaria a consulta
não traz nada.
O limite é a escrita e a decisão:
- Toda action que muda estado (
create,update,destroy, transições de status) chama um use case e renderiza pelo result - A regra de busca mora no model (
Customer.search, escopos), nunca montada emwheres no controller - Escolher o escopo pelo perfil do usuário (
current_user.admin? ? Contract.all : ...) é autorização e pode ficar no controller; regra de negócio não - Controller nunca chama service
```ruby # GOOD — leitura direta, escrita por use case class CustomersController < BaseController def index render json: Customer.search(Customer.all, params), each_serializer: CustomerSerializer end
def show render json: Customer.find(params[:id]), serializer: CustomerSerializer end
def update Customer::Update.call(customer: Customer.find(params[:id]), attributes: customer_params) .on_success { |result| render json: result[:customer], serializer: CustomerSerializer } .on_failure(:invalid) { |result| render_error(:unprocessable_entity, “invalid”, “Dados inválidos”, result[:errors]) } end end ```
Use cases
- Uma classe = uma ação de negócio, com contexto no singular:
Charge::Recalculate,Debit::Renegotiate,Dashboard::Summarize. O contexto pode ou não ser um model - Sempre declarada na forma compacta
class Debit::Renegotiate < Micro::Case. Quando o contexto é um model,module Debitquebra comTypeError: Debit is not a module; a forma compacta funciona nos dois casos (o Zeitwerk usa a classe do model ou cria o módulo a partir da pasta) Successcarrega objetos de negócio (models, value objects), nunca JSON- Todo
Failuretem tipo de negócio (:already_listed,:charge_rejected) - Todo erro de service vira
Failure, capturando só a hierarquia do terceiro (rescue Asaas::Error); indisponibilidade viraFailure(:asaas_unavailable)e o job fazretry_job
Models
Ricos, desde que mexam só nos próprios dados e nos objetos diretos (has_many/has_one;
belongs_to só leitura). O model define como o próprio estado muda (debit.recalculate!); o use
case decide quando e o que mais acontece. Model nunca chama service, job, mailer ou use case.
Services
O primeiro service é o Apolo (app/services/apolo.rb e app/services/apolo/): Apolo.find_student(email:)
devolve Apolo::Student/Apolo::Enrollment (Data.define), falha com Apolo::Unavailable/Apolo::Rejected,
e o Faraday só aparece em Apolo::Client. As próximas integrações seguem este formato.
Módulo Ruby por terceiro com operações, como um SDK: Asaas.create_payment(...). Recebe e devolve
o nosso vocabulário (value objects com Data.define); o vocabulário do terceiro (billingType,
dueDate) só aparece dentro de services/<terceiro>/. Falha levanta a hierarquia do terceiro
(Asaas::Unavailable, Asaas::Rejected); erro da gem HTTP nunca sai de services/.
Autoload
Nenhuma configuração. Todas as pastas são filhas diretas de app/, então o Zeitwerk já as trata
como autoload roots por padrão — inclusive use_cases/ e services/. config/application.rb fica
só com config.autoload_lib(ignore: %w[assets tasks]).
app/models/user.rb define User, como em qualquer app Rails.
Testes
test/ espelha app/: test/models/, test/controllers/, test/use_cases/, test/services/,
test/jobs/.
Consequências
| Consequência | Efeito |
|---|---|
| Zero configuração de autoload | Pasta nova sob app/ funciona sem tocar em config/application.rb |
| Ferramentas funcionam por convenção | annotate_rb, generators e ruby-lsp acham os arquivos sozinhos |
| Fronteiras são convencionais | Nenhuma pasta impede um if de negócio em services/ ou num controller; quem barra é a revisão (commons:review) |
services/ tem um papel só |
Só integrações externas, por terceiro; transporte assíncrono (eventos, filas) ainda não tem definição e não mora em services/ |
Desvios do código atual
Levantamento de 2026-09-28. O código foi escrito antes do commons:rails 2.0 e ainda não foi
ajustado; cada item é pendência separada.
| Desvio | Onde | Regra do commons:rails |
|---|---|---|
Use cases declarados com module aninhado |
todos os 13 em app/use_cases/ |
forma compacta class Contexto::Verbo < Micro::Case |
Contexto no plural para contornar o TypeError com o model |
Debits::TransferPortfolio, Contracts::AvailableActions |
contexto no singular (Debit::, Contract::) com forma compacta |
| Nome que não é ação de negócio | Contracts::AvailableActions, Dashboard::Attendants, Dashboard::Evolution; Authenticate::Login usa verbo como contexto |
<Contexto>::<Verbo> (ex: Dashboard::Summarize, Authentication::Login) |
| Serializer chamando use case | ContractSerializer#actions chama Contracts::AvailableActions |
serializer só formata; regra que depende só do status do contrato vai para o model (contract.available_actions) |
| Mixin de use case fora de pasta padrão | app/concerns/dashboard/period_filterable.rb (Dashboard::PeriodFilterable) |
o commons:rails não prevê app/concerns/; decidir entre step privado ou método de model/escopo |
| Use case sem chamador | Debits::TransferPortfolio só é chamado pelos testes |
apagar arquivo sem uso ou ligá-lo ao fluxo que precisa dele |
| Use cases sem teste | Contracts::AvailableActions, Dashboard::Evolution, Profile::Update |
test/ espelha app/ |
Referências
commons:rails— a arquitetura Rails do commons que este documento aplica- Camadas do app React — a contraparte no front
- Spec: scaffold de fundação do back-end Rails
- USER-001 — Fundação do serviço
modules/backend/config/application.rb