Ingeniería
Capas base, core y marts: cómo organizo un proyecto dbt
Cómo estructurar un proyecto dbt en capas base, core y marts, con convenciones de nombres, materializaciones, tests por capa y errores frecuentes.
Autor: Rubén Solano CeaPublicado: 4 min de lectura
Cuando un proyecto dbt crece sin una estructura clara, ocurre siempre lo mismo: la misma lógica se repite en cinco modelos, nadie sabe cuál es «la tabla buena» y un cambio en una fuente rompe informes en sitios inesperados. La solución no es una herramienta nueva, sino ordenar el trabajo en capas con responsabilidades distintas.
Esta es la organización que uso, con tres capas: base, core y marts. Es una variante de la guía oficial de dbt, que habla de staging, intermediate y marts (How we structure our dbt projects); lo importante no son los nombres, sino respetar la separación.
La idea en una frase por capa
| Capa | Responsabilidad | Qué NO debe hacer |
|---|---|---|
| base | Una tabla por fuente, casi sin transformar: nombres, tipos y poco más | Unir fuentes ni aplicar reglas de negocio |
| core | Entidades del negocio (clientes, transacciones…) con claves y reglas bien definidas | Preparar datos para un informe concreto |
| marts | Tablas listas para un uso: un cuadro de mando, un modelo, una exportación | Ser la fuente de otras capas anteriores |
La dirección de dependencias es siempre la misma: base → core → marts. Un modelo de core nunca depende de un mart.
Capa base: la frontera con el exterior
Es la única capa que lee de source(). Su trabajo es dejar los datos «limpios de forma» para que el resto del proyecto no tenga que preocuparse de cómo vienen:
# models/base/_base__sources.yml
version: 2
sources:
- name: crm
description: Datos exportados del CRM.
tables:
- name: customers
- name: transactions
-- models/base/base_crm__transactions.sql
select
cast(transaction_id as string) as transaction_id,
cast(customer_id as string) as customer_id,
cast(amount as numeric) as amount,
upper(currency) as currency,
cast(created_at as timestamp) as created_at
from {{ source('crm', 'transactions') }}
(Ejemplo ilustrativo con nombres genéricos, no pertenece a ningún proyecto real.)
Reglas que me funcionan: un modelo por tabla de origen, convención base_<fuente>__<entidad>, ningún join y materialización como vista, porque no hay motivo para almacenar una copia casi idéntica de la fuente.
Capa core: donde viven las reglas del negocio
Aquí se decide qué es un cliente, qué cuenta como transacción válida, cómo se deduplican registros y cuál es la clave de cada entidad. Es donde más conviene invertir en claridad, porque todo lo demás se apoya en ella.
-- models/core/core_transactions.sql
select
t.transaction_id,
t.customer_id,
t.amount,
t.currency,
t.created_at,
c.country_code
from {{ ref('base_crm__transactions') }} as t
left join {{ ref('base_crm__customers') }} as c using (customer_id)
Los modelos core suelen materializarse como tablas (o incrementales cuando el volumen lo pide), porque se consultan mucho y conviene que el cálculo ya esté hecho.
Capa marts: una tabla por uso
Un mart responde a una pregunta concreta, por ejemplo «volumen mensual por país y tipo de cliente». Puede agregar, desnormalizar y renombrar columnas pensando en quien lo va a consumir (una herramienta de BI, un modelo, una hoja). Si dos marts repiten la misma lógica, esa lógica probablemente pertenece a core.
Tests por capa
La estructura también ordena las pruebas:
# models/core/_core__models.yml
version: 2
models:
- name: core_transactions
columns:
- name: transaction_id
data_tests:
- unique
- not_null
- name: customer_id
data_tests:
- not_null
- relationships:
to: ref('core_customers')
field: customer_id
- name: currency
data_tests:
- accepted_values:
values: ['EUR', 'USD', 'GBP']
- En base: pruebas de frescura de las fuentes (
source freshness) y comprobaciones mínimas de tipo y no nulos en las claves. - En core: unicidad de claves, relaciones entre entidades y reglas de negocio (por ejemplo, importes no negativos salvo devoluciones).
- En marts: que las métricas cuadren con el total de
corey que no haya duplicados en la granularidad declarada.
Las pruebas fallan cerca de donde nace el problema, y eso reduce muchísimo el tiempo de diagnóstico.
Configuración por carpeta
dbt permite fijar la materialización por capa en dbt_project.yml, de modo que no hay que repetirla en cada modelo:
# dbt_project.yml (fragmento)
models:
mi_proyecto:
base:
+materialized: view
core:
+materialized: table
marts:
+materialized: table
Errores frecuentes
- Saltarse capas: un mart que lee directamente de
source()rompe el aislamiento y duplica limpieza. - Reglas de negocio en base: acaban dispersas y es difícil saber dónde se decidió algo.
- Marts que alimentan otros modelos: crea dependencias circulares de intención y cambios con efectos inesperados.
- Sin documentación de la granularidad: «una fila por transacción» o «una fila por cliente y mes» debe estar escrito en el modelo.
- Tests solo al final: cuando llegan a los marts, el origen del fallo ya se ha diluido.
Cuándo no hace falta tanto
Con un proyecto pequeño (pocas tablas, un solo consumidor), tres capas pueden ser demasiado. Una versión mínima —base y un único mart— ya aporta orden. Lo importante es que, si el proyecto crece, la estructura permita añadir core sin reescribirlo todo.
Dónde lo he aplicado
Uso esta organización en mi laboratorio de analytics engineering, un proyecto personal con datos sintéticos, y la he aplicado en mi trabajo con SQL, dbt y BigQuery en un equipo de analítica de una fintech (descrito a alto nivel). Si estás ordenando o creando un almacén de datos y quieres una segunda opinión, mira el servicio de ingeniería de datos.