Guía de Clean Architecture: Capas y Regla de Dependencia
Guía práctica de Clean Architecture de Uncle Bob: organiza el código en capas para que frameworks, UI y bases de datos sean detalles, no dependencias.
Descripción General
Heredas una base de código donde las reglas de negocio viven dentro de controladores, modelos del ORM y un par de procedimientos almacenados. Actualizar el framework web significa reescribir media aplicación, y cada test unitario necesita una base de datos corriendo para pasar. Clean Architecture, descrita por Robert C. Martin (Uncle Bob) en su libro de 2017 y en sus posts anteriores del blog Clean Coder, existe para evitar exactamente esa situación: organiza el código en capas concéntricas y aplica una sola regla — la Regla de Dependencia — para que frameworks, bases de datos e UI sean detalles reemplazables en lugar de dependencias estructurales.
La regla se enuncia fácil: las dependencias del código fuente solo pueden apuntar hacia adentro. Nada en una capa interna puede saber nada de una capa externa. Tu dominio nunca importa tu ORM, tus casos de uso nunca importan Express y tus entidades no saben que JSON existe. Todo lo que sí apunta hacia adentro — controladores hacia casos de uso, repositorios hacia interfaces del dominio — es lo que evita que la arquitectura colapse de nuevo en un enredo.
Las Cuatro Capas
Martin describe la arquitectura como cuatro anillos concéntricos, desde las preocupaciones más volátiles afuera hasta las políticas más estables adentro:
┌──────────────────────────────────────┐
│ Frameworks y Drivers │
│ (Web, UI, APIs externas, BD) │
├──────────────────────────────────────┤
│ Adaptadores de Interfaz │
│ (Controladores, Presenters, Puertas) │
├──────────────────────────────────────┤
│ Reglas de Negocio de Aplicación │
│ (Casos de Uso, Servicios de App) │
├──────────────────────────────────────┤
│ Reglas de Negocio de Empresa │
│ (Entidades, Lógica de Dominio) │
└──────────────────────────────────────┘
El número cuatro no es sagrado — las capas no son un dogma. Los propios escritos de Martin admiten más o menos capas — lo que importa es que la Regla de Dependencia se cumpla en cada frontera.
Entidades (Más interna)
Reglas de negocio de toda la empresa. Una entidad es un objeto (o un pequeño conjunto de funciones y estructuras de datos) que codifica reglas que serían ciertas en cualquier aplicación del negocio: “un usuario empieza pendiente y debe activarse”, “el total de un pedido es la suma de sus líneas”. Son la capa más general y reutilizable, y la que menos cambia.
export class User {
private constructor(
private readonly id: UserId,
private email: Email,
private status: UserStatus
) {}
static create(email: Email): User {
return new User(UserId.generate(), email, UserStatus.PENDING);
}
activate(): void {
this.status = UserStatus.ACTIVE;
}
isActive(): boolean {
return this.status === UserStatus.ACTIVE;
}
}
Fíjate en lo que falta: nada de decoradores @Entity(), nada de extends BaseModel, nada de atributos de serialización JSON. Esas preocupaciones pertenecen a capas externas. Si tu archivo de entidad importa algo del ORM, la dependencia apunta en la dirección equivocada.
Casos de Uso
Reglas de negocio específicas de la aplicación. Cada caso de uso orquesta entidades y define una operación que la aplicación soporta — “registrar un usuario”, “hacer un pedido”, “cancelar una suscripción”. Esta capa captura qué hace tu aplicación, en contraposición a qué es el negocio.
export class RegisterUserUseCase {
constructor(
private userRepository: UserRepository,
private emailService: EmailService
) {}
async execute(command: RegisterUserCommand): Promise<Result<User>> {
const existing = await this.userRepository.findByEmail(command.email);
if (existing) {
return Result.failure('Email ya registrado');
}
const user = User.create(Email.create(command.email));
await this.userRepository.save(user);
await this.emailService.sendWelcome(user.email);
return Result.success(user);
}
}
El caso de uso recibe entrada simple (RegisterUserCommand) y devuelve salida simple (Result<User>). No ve la petición HTTP, el DTO de request ni el objeto de respuesta — eso es trabajo de los adaptadores.
Adaptadores de Interfaz
Esta capa convierte datos en ambas direcciones: del formato más conveniente para casos de uso y entidades, al formato más conveniente para frameworks y drivers. Controladores, presenters, view models e implementaciones de repositorio viven aquí.
@RestController()
export class UserController {
constructor(private registerUser: RegisterUserUseCase) {}
@Post('/users')
async register(@Body() dto: RegisterUserDto): Promise<UserResponse> {
const result = await this.registerUser.execute(dto.toCommand());
return result.isSuccess()
? UserResponse.from(result.value)
: UserResponse.error(result.error);
}
}
El controlador es delgado a propósito: traduce RegisterUserDto a un comando del dominio, delega, y traduce el resultado de vuelta a UserResponse. Cualquier lógica más pesada que una traducción indica una regla de negocio que se filtró a la capa equivocada.
Frameworks y Drivers
La capa más externa — frameworks web, bases de datos, UI, dispositivos externos. Esta capa contiene código mínimo: registro de rutas, configuración de conexiones, configuración del ORM y la composition root donde las implementaciones reales se conectan a los puertos. Todo aquí debería poder cambiarse sin tocar una capa interna, porque para eso existe el ejercicio.
La Regla de Dependencia
Las dependencias del código fuente deben apuntar solo hacia adentro, hacia políticas de mayor nivel.
Al principio parece imposible: el caso de uso obviamente llama a la base de datos en runtime. El mecanismo que resuelve la paradoja es la inversión de dependencias. La capa de aplicación declara un puerto — una interfaz que describe lo que necesita (UserRepository, EmailService) — y una capa externa provee la implementación. La flecha del código fuente va de PostgresUserRepository hacia UserRepository, es decir hacia adentro, aunque la llamada en runtime fluye hacia afuera.
En concreto, esto significa:
- El framework web llama al controlador, no al revés
- El adaptador de base de datos implementa una interfaz de repositorio propiedad de la capa interna, no al revés
- La UI renderiza un view model construido por el presenter, no al revés
Las capas de mayor nivel ni siquiera nombran a las de menor nivel. Si un caso de uso menciona “PostgreSQL” o “HTTP”, eso es un bug arquitectónico aunque el código compile. La misma idea aparece en los estilos hermanos — ver Arquitectura Hexagonal — Puertos, Adaptadores y Testabilidad para la formulación de puertos y adaptadores de la que esto toma prestado.
Cruzando Fronteras
En cada frontera de capa, los datos cruzan como estructuras simples (DTOs) para evitar filtrar detalles de implementación. La capa interna también es dueña de la interfaz: UserRepository se declara en la capa de aplicación porque el caso de uso es el consumidor, no porque Postgres lo implemente.
// Capa de dominio — no sabe nada de HTTP ni SQL
interface UserRepository {
findById(id: UserId): Promise<User | null>;
save(user: User): Promise<void>;
}
// Capa de infraestructura — implementa el puerto
class PostgresUserRepository implements UserRepository {
constructor(private db: Knex) {}
async findById(id: UserId): Promise<User | null> {
const row = await this.db('users').where('id', id.value).first();
return row ? this.toDomain(row) : null;
}
async save(user: User): Promise<void> {
await this.db('users').insert(this.toRow(user));
}
}
Dos detalles importan aquí. Primero, la interfaz devuelve User, el tipo de dominio — nunca la fila del ORM. Si el puerto devolviera UserRow, el esquema de la base de datos se filtraría a cada llamador y la frontera existiría solo en el papel. Segundo, el mapeo (toDomain, toRow) vive dentro del adaptador, así que cuando la tabla cambia, el dominio no se mueve.
El flujo de control y el flujo de dependencias van en direcciones opuestas en fronteras como esta. El caso de uso llama a userRepository.findById, pero la dependencia del código fuente va de PostgresUserRepository hacia adentro, a UserRepository. Sentirse cómodo con esa inversión es la mayor parte de lo que “hacer Clean Architecture” significa en el día a día.
Una Request a Través de las Capas
Ayuda ver una petición HTTP cruzar cada frontera. POST /users en la app de registro:
- Express resuelve la ruta y llama a
UserController.register(dto)— capa de frameworks. - El controlador convierte
RegisterUserDtoenRegisterUserCommandy llama aregisterUser.execute(command)— del adaptador a la capa de aplicación. RegisterUserUseCasepregunta auserRepository.findByEmail(...)a través del puerto — una llamada hacia afuera que resuelve, vía DI, aPostgresUserRepository.- El adaptador consulta Postgres, mapea la fila a un
Usery lo devuelve — del adaptador a frameworks, el tipo de dominio de vuelta hacia adentro. - El caso de uso crea la entidad, llama a
save, luego aemailService.sendWelcome— de nuevo a través de puertos. - De vuelta en el controlador,
UserResponse.from(result.value)convierte el resultado de dominio en un HTTP 201 con un body JSON — trabajo de presenter en la capa de adaptadores.
Cada flecha de control apunta hacia afuera en los pasos 3-4 y cada flecha de dependencia apunta hacia adentro. Esa asimetría es lo que te permite borrar PostgresUserRepository, enchufar MongoUserRepository y no cambiar nada entre la interfaz y el dominio.
Estrategia de Testing
Las capas se pagan a la hora de testear. Las internas no tienen I/O, así que son rápidas; las externas reciben menos tests y más pesados:
| Capa | Enfoque de Test | Velocidad |
|---|---|---|
| Entidades | Tests unitarios puros | < 10ms |
| Casos de Uso | Tests unitarios con repos en memoria | < 50ms |
| Adaptadores | Tests de integración con BD real | < 500ms |
| E2E | Tests de stack completo | segundos |
test('el registro falla cuando el email está tomado', async () => {
const repo = new InMemoryUserRepository();
const mailer = new SpyEmailService();
const useCase = new RegisterUserUseCase(repo, mailer);
repo.add(User.create(Email.create('taken@example.com')));
const result = await useCase.execute(new RegisterUserCommand('taken@example.com'));
expect(result.isFailure()).toBe(true);
expect(mailer.sentEmails).toHaveLength(0);
});
Toda la suite de reglas de negocio corre en milisegundos porque InMemoryUserRepository es un map detrás de una interfaz. Reserva los tests lentos — Postgres en un contenedor, el stack HTTP real — para la capa de adaptadores, donde de verdad verifican algo.
Clean vs Hexagonal vs Onion vs Layered
Estas cuatro arquitecturas responden al mismo problema — mantener las reglas de negocio independientes de los detalles de entrega — con vocabulario distinto. Componen bien entre sí; no hay que elegir religión.
| Estilo | Metáfora central | Qué aporta |
|---|---|---|
| Layered tradicional | UI → Negocio → Datos | Modelo mental simple; las dependencias apuntan hacia abajo, así que la lógica de negocio tiende a conocer la base de datos |
| Hexagonal (Cockburn) | Puertos y adaptadores | ”Dentro vs fuera” en lugar de capas; cada dependencia externa recibe un puerto con nombre |
| Onion (Palermo) | Modelo de dominio en el centro | Añade el anillo de servicios de aplicación e infraestructura explícita alrededor del núcleo |
| Clean (Martin) | Capas concéntricas + Regla de Dependencia | Unifica las demás, nombra entidades/casos de uso explícitamente y formaliza la regla de dependencia hacia adentro |
Si ya manejas puertos y adaptadores bien, Clean Architecture se lee como la misma idea con anillos. Las diferencias que importan en la práctica: Clean separa entidades (reglas de empresa) de casos de uso (reglas de aplicación), y trata la Regla de Dependencia como un invariante que se refuerza mecánicamente — con tests de arquitectura — en lugar de una convención para recordar.
Para un tratamiento más profundo de cada una, ver Arquitectura Hexagonal, Arquitectura Onion y Arquitectura en Capas. Para la alternativa de cortar por feature en lugar de por capa, ver Arquitectura Vertical Slice.
Adopción Incremental
Reescribir una app existente a Clean Architecture de una pasada falla casi siempre — la migración dura más que la paciencia del equipo y la tolerancia del producto a congelar features. Lo que funciona es la extracción, un corte vertical de comportamiento a la vez:
- Elige un corte acotado. Una feature, caso de uso o módulo — idealmente uno que cambie seguido, porque ahí es donde la testabilidad se paga más rápido.
- Escribe el caso de uso primero. Extrae “registrar usuario” a una clase cuya firma toma entrada simple y devuelve salida simple. Empuja las reglas de negocio hacia él desde el controlador.
- Introduce el puerto. Define la interfaz de repositorio en la capa de aplicación, haz que el código de acceso a datos existente la implemente e inyéctala. No moviste la base de datos — moviste la dependencia.
- Mueve el código de framework hacia afuera. Los controladores se quedan con routing y traducción de DTOs; nada más. Las anotaciones del ORM migran a una entidad de persistencia separada de la entidad de dominio, con un mapper entre ambas.
- Protege la frontera en CI.
dependency-cruiseroeslint-plugin-boundariesen JavaScript,ArchUniten Java,NetArchTesten .NET — cualquiera puede romper el build cuando un caso de uso importaexpresso una entidad importa el ORM. Las reglas aplicadas por tests sobreviven a las reglas aplicadas por code review.
Mide el progreso por lo que se vuelve fácil: ¿puedes correr la suite de dominio sin base de datos? ¿Puedes cambiar el servicio de correo por un stub en una línea? Cuando la respuesta es sí en los puntos críticos, deja de extraer y deja que el resto pague su camino después.
Mejores Prácticas
- Refuerza la dirección mecánicamente. La Regla de Dependencia se erosiona en silencio; un
importen el lugar equivocado compila y sale a producción igual. Los tests de arquitectura en CI son la única protección duradera. - Que el consumidor sea dueño del puerto.
UserRepositoryvive en la capa de aplicación porque el caso de uso lo necesita, no porque Postgres lo implemente. Acerca la interfaz a quien la llama. - Mantén una sola composition root. Todo el cableado — “qué implementación de repositorio”, “qué mailer” — pertenece al punto de entrada (
main,app.ts), nunca repartido por las capas internas. La guía del patrón de Inyección de Dependencias cubre la mecánica del cableado. - Cruza fronteras con datos tontos. Commands, queries, DTOs y view models llevan campos, no comportamiento. En el momento en que un objeto de frontera ejecuta lógica de negocio, deja de ser una frontera.
- Dales comportamiento a las entidades. Si
Usersolo tiene getters y setters, tus reglas están sin casa — derivarán a casos de uso, controladores y procedimientos almacenados. Pon los invariantes del dominio en la entidad y deja que los casos de uso orquesten.
Errores Comunes
- Acoplamiento al framework — importar Spring o Express dentro de casos de uso. Un
import expressdentro deRegisterUserUseCasey la capa deja de existir; construiste un monolito con carpetas extra. - Abstracciones con fugas — pasar objetos de request HTTP, entidades del ORM o
HttpContextal dominio. El puerto devuelveUser | null, noUserRow | null; el caso de uso toma unCommand, no unRequest. - Modelos anémicos — tratar las entidades como bolsas de datos sin comportamiento. Las reglas terminan desperdigadas por los casos de uso hasta que dos de ellos no se ponen de acuerdo en qué significa “usuario activo”.
- Sobre-abstracción — agregar interfaces para cosas que nunca cambian. Un puerto se gana su lugar cuando hay dos implementaciones (real + fake, o dos reales) o cuando la frontera protege una regla de negocio.
IStringHelperno protege nada. - Casos de uso pasarela — un
GetUserUseCaseque solo llama arepo.findByIdagrega un archivo y un test por cero regla. Colapsa los pasarela o deja que el controlador llame al puerto directamente hasta que aparezca lógica real.
Cuándo Usar
- Para alternativas, ver Arquitectura Hexagonal — Puertos, Adaptadores y Testabilidad.
- Aplicaciones medianas a grandes con larga vida útil — el overhead se amortiza en años de cambios.
- Aplicaciones donde la lógica de dominio pesa más que el acceso a datos — validación, pricing, workflows, invariantes.
- Equipos que valoran la testabilidad y el despliegue independiente — suites unitarias rápidas sin base de datos.
- Proyectos donde el cambio de framework es probable — esperas sobrevivir al framework web o al ORM actual.
Cuándo NO Usar
- CRUD simple sin reglas de negocio — puertos y capas agregan archivos sin comprar aislamiento.
- Scripts, prototipos o MVPs donde la velocidad importa más que la estructura — puedes extraer la arquitectura después si la cosa sobrevive.
- Equipos sin la disciplina para mantener las fronteras — Clean Architecture aplicada solo por convención decae en un monolito en capas peor en un año.
- Servicios muy pequeños — una lambda de 200 líneas o un worker de propósito único rara vez tiene suficiente dominio para justificar cuatro capas.
Solución de Problemas
- Las fronteras se cruzan solas: un
import expresso un tipo del ORM se cuela en un caso de uso después de cada deadline. Agrega un test de arquitectura (dependency-cruiser, ArchUnit, NetArchTest) que rompa el build ante un import que apunta hacia afuera — es la diferencia entre una regla y un deseo. - Cada cambio toca cinco archivos: un campo nuevo significa editar la entidad, dos DTOs, dos mappers y un caso de uso. Tienes demasiadas capas de mapeo; fusiona los DTOs estructuralmente idénticos y deja el tipo de dominio como la única forma que importa.
- Los casos de uso se vuelven god classes:
UserServiceacumula register, activate, deactivate, resetPassword, exportCsv. Divide por operación — una clase por caso de uso — porque cada operación tiene sus propias reglas y sus propias razones para cambiar. - Los tests siguen levantando la base de datos: tu puerto de repositorio filtra tipos del ORM o el fake es demasiado fiel. El puerto debería devolver solo tipos de dominio, y el fake en memoria debería ser un map, no un cliente SQL simulado.
- El equipo bordea las capas: los desarrolladores llaman a
PostgresUserRepositorydirectamente “solo esta vez”. Haz que lo ilegal sea difícil — pon los adaptadores concretos en un paquete que las capas internas no puedan importar — en lugar de depender de comentarios de review.
Temas Avanzados
Escenario Detallado: App de Registro de Usuarios con Clean Architecture
Un servicio de registro y autenticación (TypeScript + Node.js) repartido en las cuatro capas. La estructura de archivos pone cada capa en su propio directorio. Todos los archivos de abajo, más los tests, son ejecutables desde el repositorio companion:
src/
domain/
entities/User.ts
valueobjects/Email.ts
valueobjects/UserId.ts
valueobjects/UserStatus.ts
repositories/UserRepository.ts # puerto (interfaz)
services/EmailService.ts # puerto (interfaz)
errors/DomainError.ts
application/
usecases/RegisterUserUseCase.ts
usecases/AuthenticateUserUseCase.ts
dto/RegisterUserCommand.ts
dto/UserResponse.ts
results/Result.ts
infrastructure/
persistence/PostgresUserRepository.ts # implementa UserRepository
email/SendGridEmailService.ts # implementa EmailService
database/KnexConnection.ts
presentation/
controllers/UserController.ts
presenters/UserPresenter.ts
routes/userRoutes.ts
app.ts # composition root (Express)
El flujo de POST /users sigue el recorrido de request de Una Request a Través de las Capas: Express entrega el body a UserController, que lo mapea a RegisterUserCommand, llama al caso de uso y convierte el Result devuelto en un 201 o un 400 vía el presenter.
Los tests viven junto a lo que cubren. Los tests de entidad son puros:
// domain/entities/User.test.ts
describe('User', () => {
test('create pone el status en PENDING', () => {
const user = User.create(Email.create('test@example.com'));
expect(user.isActive()).toBe(false);
});
test('activate pone el status en ACTIVE', () => {
const user = User.create(Email.create('test@example.com'));
user.activate();
expect(user.isActive()).toBe(true);
});
});
Los tests de casos de uso cambian los puertos por fakes en memoria — sin base de datos, sin red, sin contenedor:
// application/usecases/RegisterUserUseCase.test.ts
describe('RegisterUserUseCase', () => {
let repo: InMemoryUserRepository;
let mailer: SpyEmailService;
let useCase: RegisterUserUseCase;
beforeEach(() => {
repo = new InMemoryUserRepository();
mailer = new SpyEmailService();
useCase = new RegisterUserUseCase(repo, mailer);
});
test('registra un usuario nuevo y envía el email de bienvenida', async () => {
const result = await useCase.execute(new RegisterUserCommand('test@example.com'));
expect(result.isSuccess()).toBe(true);
expect(mailer.sentEmails).toHaveLength(1);
});
test('rechaza un email duplicado', async () => {
repo.add(User.create(Email.create('test@example.com')));
const result = await useCase.execute(new RegisterUserCommand('test@example.com'));
expect(result.isFailure()).toBe(true);
expect(mailer.sentEmails).toHaveLength(0);
});
});
Los tests de dominio corren en milisegundos de un dígito, los de casos de uso se quedan bajo ~50ms, y los únicos tests lentos son los de adaptadores que pegan a Postgres real — donde lento es aceptable porque están verificando la integración, no las reglas.
Manejar datos entre capas sin filtrar detalles
Usa DTOs en cada frontera. El controlador recibe un RequestDTO y lo convierte en un Command del dominio. El caso de uso devuelve un Result con la entidad de dominio. El presenter convierte la entidad en un ResponseDTO. Nunca pases el Request de Express al dominio y nunca devuelvas la entidad del ORM al controlador — cada capa habla su propio idioma y los DTOs son la capa de traducción.
El único lugar donde esto duele es en superficies CRUD pequeñas donde command, entidad y response son estructuralmente idénticas. Tres tipos iguales y dos mappers por frontera es peor que un solo tipo; colapsa los DTOs cuando no llevan significado distinto, y conserva el mapeo solo donde las formas realmente difieren.
Lecturas Adicionales
- Companion ejecutable — el ejemplo de registro como archivos TypeScript independientes con tests.
- The Clean Architecture — el post original de Uncle Bob de 2012 con el diagrama de círculos concéntricos.
- Clean Architecture: A Craftsman’s Guide to Software Structure and Design (Robert C. Martin, 2017) — el tratamiento completo, con capítulos sobre fronteras, presenters y el componente principal.
- Arquitectura Hexagonal — Puertos, Adaptadores y Testabilidad — el estilo hermano del que esta guía toma la mecánica de fronteras.
- Guía de Principios SOLID — la inversión de dependencias es el mecanismo que hace posible la Regla de Dependencia.
Preguntas frecuentes
¿Clean Architecture es lo mismo que Hexagonal?
Comparten el mismo objetivo — aislar el dominio de los detalles de entrega — con metáforas distintas. Hexagonal habla de puertos y adaptadores; Clean habla de capas concéntricas y la Regla de Dependencia, y separa reglas de aplicación (casos de uso) de reglas de empresa (entidades). Una base de código hexagonal suele cumplir la Regla de Dependencia automáticamente.
¿Cómo manejo transacciones entre casos de uso?
Mantén la frontera de la transacción a nivel del caso de uso con un puerto Unit of Work en la capa de aplicación, o envuelve el caso de uso en un decorador de transacción que viva en los adaptadores. Lo que no funciona: que el controlador abra la transacción, porque entonces las reglas de rollback viven fuera de la capa dueña del flujo de negocio.
¿Puedo usar ORMs en la capa de entidades?
No sin romper la Regla de Dependencia — las anotaciones del ORM son preocupaciones de persistencia. El patrón habitual es una entidad de persistencia separada (o schema) más un mapper que convierte filas en entidades de dominio. Si tu ORM soporta mapeo sin anotaciones (p. ej., fluent config de EF Core, entity schemas de TypeORM), a veces puedes mantener una sola clase y mapearla externamente.
¿Dónde va la validación de entrada?
Divídela por tipo. "¿Es una request bien formada?" — campos requeridos, tipos, longitudes — pertenece al adaptador, donde vive el DTO. "¿Es un estado de negocio válido?" — el usuario debe estar activo, el pedido no puede enviarse dos veces — pertenece a la entidad o al caso de uso, donde la regla se aplica sin importar el punto de entrada.
¿Cómo estructuro las carpetas?
Primero-por-capa (domain/, application/, infrastructure/, presentation/) funciona cuando la app es un solo bounded context. Cuando tienes varios, inviértelo — primero-por-feature (users/, billing/), cada una con sus propias subcarpetas de capa — así borrar una feature borra un directorio. Las capas siguen importando; solo están anidadas dentro del corte.
¿Vale la pena Clean Architecture para una app pequeña?
Normalmente no. Si todo el dominio es "guarda este formulario", las capas cuestan archivos e indirección sin comprar aislamiento. La señal de que llegó el momento: las reglas de negocio empiezan a aparecer en más de un lugar, o no puedes testear una regla sin levantar infraestructura. Extrae ahí, incrementalmente — ver Adopción Incremental arriba.
Recursos Relacionados
Arquitectura Hexagonal — Puertos, Adaptadores y Testabilidad
Referencia Detallada de Arquitectura Hexagonal (Puertos y Adaptadores): estructura aplicaciones para aislar la lógica de dominio de frameworks, bases de datos y servicios externos.
GuideGuía de Arquitectura Onion: Diseño Centrado en el Dominio
Guía práctica de Arquitectura Onion: organiza código alrededor del dominio, fuerza dependencias hacia adentro y aísla infraestructura. Incluye ejemplos en C#.
GuideArquitectura por Capas — N-Tier Explicado
Guía práctica de Arquitectura por Capas (N-Tier): separar presentación, lógica de negocio y capa de datos con responsabilidades claras y reglas de dependencia.
GuidePrincipios SOLID Explicados con Ejemplos
Aprende los cinco principios SOLID con ejemplos de código malo vs bueno y un refactor completo antes/después de un servicio de pagos en TypeScript.
PatternPatrón Dependency Injection
Suministra dependencias desde fuera en lugar de crearlas internamente. Un patrón arquitectural para código desacoplado y testeable.
GuideSlices Verticales: Organización por Feature
Guía práctica de Slices Verticales: organizar código por feature en lugar de capa técnica, reduciendo navegación cruzada y mejorando cohesión.