Arquitectura de Software: Del Caos al Sistema Escalable
En 2021, heredé un sistema bancario que procesaba $2M USD diarios. El problema: cada deploy era una ruleta rusa. La solución: arquitectura bien diseñada.
La arquitectura no es dibujar cajas y flechas. Es tomar decisiones técnicas que el negocio pagará (o sufrirá) durante años.
Por Qué la Arquitectura Importa Más que el Código
Historia Real: El Monolito que Costó $500K
Escenario: Fintech latinoamericana, 50K usuarios activos.
Problema:
Arquitectura existente:
├── app.js (15,000 líneas)
├── database.js (todas las queries)
└── utils.js (todo lo demás)
Síntomas:
- 🐌 Deploy: 45 minutos de downtime
- 💥 Escalabilidad: Un módulo caído = todo caído
- 🔥 Bugs: 1 cambio = 10 regresiones
- 👨💻 Team: Nadie quiere tocar el código
Costo real:
- 3 meses de refactoring
- 2 desarrolladores a tiempo completo
- $500K USD entre costos de desarrollo y oportunidad perdida
Los 4 Pilares de Arquitectura Profesional
1. Separación de Responsabilidades
Cada capa debe tener un propósito claro.
// ❌ ANTES: Todo mezclado
app.post('/transfer', async (req, res) => {
const { from, to, amount } = req.body;
// Validación mezclada con lógica
if (!from || !to || amount <= 0) {
return res.status(400).json({ error: 'Invalid' });
}
// Acceso directo a DB
const accountFrom = await db.query('SELECT * FROM accounts WHERE id = ?', [from]);
// Lógica de negocio en el controller
if (accountFrom.balance < amount) {
return res.status(400).json({ error: 'Insufficient funds' });
}
// 50 líneas más...
});
// ✅ DESPUÉS: Arquitectura en capas
// 1. PRESENTATION LAYER (API)
@Controller('transfers')
export class TransferController {
constructor(private transferUseCase: TransferUseCase) {}
@Post()
@UseGuards(AuthGuard)
async transfer(@Body() dto: TransferRequestDto): Promise<TransferResponseDto> {
return this.transferUseCase.execute(dto);
}
}
// 2. APPLICATION LAYER (Use Cases)
@Injectable()
export class TransferUseCase {
constructor(
private accountRepo: AccountRepository,
private transferRepo: TransferRepository,
private eventBus: EventBus
) {}
async execute(request: TransferRequestDto): Promise<TransferResponseDto> {
// Orquestación sin lógica de negocio
const fromAccount = await this.accountRepo.findById(request.fromAccountId);
const toAccount = await this.accountRepo.findById(request.toAccountId);
// Domain hace la validación y lógica
const transfer = fromAccount.transferTo(toAccount, request.amount);
await this.transferRepo.save(transfer);
await this.eventBus.publish(new TransferCompletedEvent(transfer));
return TransferResponseDto.fromDomain(transfer);
}
}
// 3. DOMAIN LAYER (Lógica de Negocio)
export class Account {
constructor(
public readonly id: AccountId,
private balance: Money,
private status: AccountStatus
) {}
transferTo(destination: Account, amount: Money): Transfer {
this.validateTransfer(amount, destination);
this.balance = this.balance.subtract(amount);
destination.balance = destination.balance.add(amount);
return Transfer.create(this, destination, amount);
}
private validateTransfer(amount: Money, destination: Account): void {
if (!this.isActive()) {
throw new InactiveAccountError(this.id);
}
if (!destination.isActive()) {
throw new InactiveAccountError(destination.id);
}
if (!this.hasSufficientFunds(amount)) {
throw new InsufficientFundsError(this.balance, amount);
}
}
private hasSufficientFunds(amount: Money): boolean {
return this.balance.isGreaterThanOrEqual(amount);
}
}
// 4. INFRASTRUCTURE LAYER (Persistencia)
@Injectable()
export class TypeOrmAccountRepository implements AccountRepository {
constructor(
@InjectRepository(AccountEntity)
private repo: Repository<AccountEntity>
) {}
async findById(id: AccountId): Promise<Account> {
const entity = await this.repo.findOne({ where: { id: id.value } });
if (!entity) {
throw new AccountNotFoundError(id);
}
return AccountMapper.toDomain(entity);
}
async save(account: Account): Promise<void> {
const entity = AccountMapper.toEntity(account);
await this.repo.save(entity);
}
}
Beneficios de esta arquitectura:
- ✅ Testeable: Cada capa se testea independientemente
- ✅ Escalable: Puedes cambiar la DB sin tocar la lógica
- ✅ Mantenible: Cada archivo tiene un propósito claro
- ✅ Comprensible: Nuevos devs entienden el flujo en minutos
2. Microservicios (Cuando Realmente los Necesitas)
Regla de oro: Monolito primero, microservicios cuando duele.
// Arquitectura de MATIAS (matias.com.co)
// API Gateway (NestJS)
@Controller()
export class ApiGateway {
constructor(
@Inject('AUTH_SERVICE') private authService: ClientProxy,
@Inject('BILLING_SERVICE') private billingService: ClientProxy,
@Inject('INVENTORY_SERVICE') private inventoryService: ClientProxy
) {}
@Post('orders')
async createOrder(@Body() dto: CreateOrderDto) {
// 1. Validar usuario
const user = await firstValueFrom(
this.authService.send('validate.user', dto.userId)
);
// 2. Verificar inventario
const available = await firstValueFrom(
this.inventoryService.send('check.stock', dto.items)
);
if (!available) {
throw new InsufficientStockException();
}
// 3. Procesar billing
const order = await firstValueFrom(
this.billingService.send('create.order', { user, items: dto.items })
);
return order;
}
}
// Auth Service (independiente)
@Controller()
export class AuthController {
@MessagePattern('validate.user')
async validateUser(userId: string): Promise<User> {
// Lógica de autenticación
}
}
// Billing Service (independiente)
@Controller()
export class BillingController {
@MessagePattern('create.order')
async createOrder(data: CreateOrderData): Promise<Order> {
// Lógica de facturación
}
}
Cuándo usar microservicios:
- ✅ Teams grandes (>20 developers)
- ✅ Dominios claramente separados
- ✅ Necesidad de escalar independientemente
- ✅ Tecnologías diferentes por servicio
Cuándo NO usarlos:
- ❌ “Porque están de moda”
- ❌ Team pequeño (<5 developers)
- ❌ Startup en fase inicial
- ❌ No tienes experiencia con sistemas distribuidos
3. Event-Driven Architecture
Para sistemas que necesitan reaccionar a cambios en tiempo real.
// MATIAS IMPULSO: Sistema de IA conversacional
// 1. Event Definitions
export class MessageReceivedEvent {
constructor(
public readonly conversationId: string,
public readonly userId: string,
public readonly content: string,
public readonly timestamp: Date
) {}
}
export class AIResponseGeneratedEvent {
constructor(
public readonly conversationId: string,
public readonly response: string,
public readonly tokensUsed: number
) {}
}
// 2. Event Handlers
@Injectable()
export class MessageReceivedHandler implements IEventHandler<MessageReceivedEvent> {
constructor(
private aiService: AIService,
private eventBus: EventBus
) {}
async handle(event: MessageReceivedEvent): Promise<void> {
// Procesar con IA
const response = await this.aiService.generateResponse(event.content);
// Publicar respuesta
await this.eventBus.publish(
new AIResponseGeneratedEvent(
event.conversationId,
response.text,
response.tokensUsed
)
);
}
}
@Injectable()
export class AIResponseHandler implements IEventHandler<AIResponseGeneratedEvent> {
constructor(
private billingService: BillingService,
private notificationService: NotificationService
) {}
async handle(event: AIResponseGeneratedEvent): Promise<void> {
// Billing por tokens
await this.billingService.chargeTokens(event.tokensUsed);
// Notificar usuario
await this.notificationService.sendResponse(
event.conversationId,
event.response
);
}
}
// 3. Publisher
@Injectable()
export class ConversationService {
constructor(private eventBus: EventBus) {}
async receiveMessage(conversationId: string, message: string): Promise<void> {
// Persistir mensaje
await this.saveMessage(conversationId, message);
// Publicar evento (fire and forget)
await this.eventBus.publish(
new MessageReceivedEvent(conversationId, message, new Date())
);
}
}
Ventajas:
- 🔥 Desacoplamiento: Servicios no se conocen entre sí
- ⚡ Performance: Procesamiento asíncrono
- 📊 Auditoría: Todos los eventos están registrados
- 🔄 Resilencia: Si un handler falla, otros continúan
4. CQRS (Command Query Responsibility Segregation)
Separa lecturas de escrituras para optimizar ambas.
// COMMANDS (Escritura)
export class CreateProductCommand {
constructor(
public readonly name: string,
public readonly price: number,
public readonly stock: number
) {}
}
@CommandHandler(CreateProductCommand)
export class CreateProductHandler implements ICommandHandler<CreateProductCommand> {
constructor(private productRepo: ProductRepository) {}
async execute(command: CreateProductCommand): Promise<string> {
const product = Product.create(command.name, command.price, command.stock);
await this.productRepo.save(product);
return product.id;
}
}
// QUERIES (Lectura)
export class GetProductsQuery {
constructor(
public readonly filters?: ProductFilters,
public readonly pagination?: Pagination
) {}
}
@QueryHandler(GetProductsQuery)
export class GetProductsHandler implements IQueryHandler<GetProductsQuery> {
constructor(private productReadModel: ProductReadModel) {}
async execute(query: GetProductsQuery): Promise<ProductDto[]> {
// Lectura optimizada (denormalizada)
return this.productReadModel.find(query.filters, query.pagination);
}
}
// Uso
@Controller('products')
export class ProductController {
constructor(
private commandBus: CommandBus,
private queryBus: QueryBus
) {}
@Post()
async create(@Body() dto: CreateProductDto) {
return this.commandBus.execute(
new CreateProductCommand(dto.name, dto.price, dto.stock)
);
}
@Get()
async list(@Query() filters: ProductFilters) {
return this.queryBus.execute(new GetProductsQuery(filters));
}
}
Caso Real: Banco Estado Chile
Contexto: APIs críticas para transferencias interbancarias.
Requerimientos:
- 99.99% uptime
- <200ms latencia p95
- 10,000 TPS (transacciones por segundo)
Arquitectura implementada:
┌─────────────────────────────────────────────┐
│ API Gateway (Kong) │
│ - Rate limiting: 1000 req/min por IP │
│ - Authentication: JWT + mTLS │
│ - Logging: Request/Response │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Load Balancer (AWS ALB) │
│ - Health checks cada 30s │
│ - Auto-scaling: 2-10 instancias │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ NestJS Application (ECS Fargate) │
│ - Stateless │
│ - Redis para caché │
│ - SQS para jobs asíncronos │
└─────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────┐
│ Database Layer │
│ - RDS PostgreSQL (Multi-AZ) │
│ - Read replicas para queries │
│ - Redis para sesiones │
└─────────────────────────────────────────────┘
Resultados:
- ✅ Uptime: 99.98% (mejor que objetivo)
- ✅ Latencia: 85ms p95 (57% mejor)
- ✅ TPS: 15,000 (50% sobre objetivo)
- ✅ Zero downtime deploys con blue/green
Patrones de Arquitectura Esenciales
Repository Pattern
// Abstracción
export interface UserRepository {
findById(id: string): Promise<User>;
save(user: User): Promise<void>;
delete(id: string): Promise<void>;
}
// Implementación PostgreSQL
@Injectable()
export class PostgresUserRepository implements UserRepository {
async findById(id: string): Promise<User> {
// Implementación específica
}
}
// Implementación MongoDB
@Injectable()
export class MongoUserRepository implements UserRepository {
async findById(id: string): Promise<User> {
// Implementación específica
}
}
// Uso (no depende de la implementación)
export class UserService {
constructor(private userRepo: UserRepository) {} // inyección
async getUser(id: string): Promise<User> {
return this.userRepo.findById(id);
}
}
Factory Pattern
export class NotificationFactory {
static create(type: NotificationType): INotificationService {
switch (type) {
case 'EMAIL':
return new EmailNotificationService();
case 'SMS':
return new SMSNotificationService();
case 'PUSH':
return new PushNotificationService();
default:
throw new UnsupportedNotificationTypeError(type);
}
}
}
// Uso
const notifier = NotificationFactory.create('EMAIL');
await notifier.send('user@example.com', 'Message');
Strategy Pattern
interface PaymentStrategy {
charge(amount: number): Promise<PaymentResult>;
}
class CreditCardPayment implements PaymentStrategy {
async charge(amount: number): Promise<PaymentResult> {
// Lógica de tarjeta de crédito
}
}
class PayPalPayment implements PaymentStrategy {
async charge(amount: number): Promise<PaymentResult> {
// Lógica de PayPal
}
}
class PaymentProcessor {
constructor(private strategy: PaymentStrategy) {}
async process(amount: number): Promise<PaymentResult> {
return this.strategy.charge(amount);
}
setStrategy(strategy: PaymentStrategy): void {
this.strategy = strategy;
}
}
Checklist de Arquitectura
Antes de diseñar, pregúntate:
📊 Requisitos
- ¿Cuántos usuarios concurrentes?
- ¿Qué latencia es aceptable?
- ¿Cuál es el budget de infraestructura?
- ¿Qué uptime se necesita?
🔧 Complejidad
- ¿Monolito es suficiente?
- ¿Realmente necesito microservicios?
- ¿El equipo tiene experiencia con la arquitectura propuesta?
🧪 Mantenibilidad
- ¿Es fácil agregar features?
- ¿Los tests son simples?
- ¿Un nuevo dev puede entenderlo?
🚀 Escalabilidad
- ¿Puedo escalar horizontalmente?
- ¿Hay single points of failure?
- ¿Cómo manejo picos de tráfico?
Conclusión
Arquitectura no es sobre tecnología de moda. Es sobre decisiones pragmáticas que resuelven problemas reales.
Los mejores arquitectos:
- 🎯 Priorizan simplicidad sobre inteligencia
- 📈 Diseñan para evolución no perfección
- 🔍 Validan con métricas no opiniones
- 👥 Consideran al equipo que mantendrá el sistema
Empieza simple. Evoluciona cuando duela.
Lewis Lopez - Arquitecto de Software
Sistemas bancarios que procesan $100M+ USD anuales
Fundador de MATIAS IMPULSO
¿Consultas de arquitectura? contacto@lewislopez.io
Temas relacionados:
¿Te gustó este artículo?
Aprende arquitectura de élite y transforma tu carrera de programador 1x a arquitecto 10x.