ArquitecturaMicroserviciosClean CodeEscalabilidad

Arquitectura de Software: Del Caos al Sistema Escalable

Lewis Lopez
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

Compartir este artículo:

¿Te gustó este artículo?

Aprende arquitectura de élite y transforma tu carrera de programador 1x a arquitecto 10x.