Blog

Ingeniería en 2026 y más allá

Estamos construyendo una infraestructura que casi nunca puede fallar. Para lograrlo, avanzamos rápido mientras entregamos software de calidad increíble, sin atajos ni compromisos. Este documento describe los estándares de ingeniería que nos guiarán en 2026 y más allá.

Keith WilliamsOct 14, 2025

Estructura del equipo

Nuestra organización de ingeniería consta de cinco equipos centrales, cada uno con responsabilidades distintas:

  • Foundation Team: Se centra en establecer y mantener estándares de código y patrones arquitectónicos. Este equipo trabaja de forma colaborativa con otros equipos para establecer buenas prácticas en toda la organización.

  • Consumer, Enterprise y Platform Teams: Equipos centrados en el producto que lanzan funciones rápidamente mientras mantienen los estándares de calidad establecidos en este documento. Estos equipos demuestran que velocidad y calidad no se excluyen mutuamente.

  • Community Team: Se encarga de revisar rápidamente los PRs de la comunidad open source, aportar feedback y acompañar ese trabajo hasta su merge. Este equipo garantiza que quienes contribuyen a nuestro open source tengan una gran experiencia y que sus aportaciones cumplan nuestros estándares de calidad. Mantener una sólida cultura open-source sigue dando forma a cómo Cal.com aborda la colaboración en ingeniería y el desarrollo de producto.

Nuestros resultados hasta ahora

Los datos hablan por sí solos. Durante el último año y medio, hemos transformado por completo cómo construimos software:

Screenshot 2025-10-01 at 9.40.36 PM.png

Hemos duplicado aproximadamente nuestra producción de ingeniería al mismo tiempo que mejorábamos la calidad. Lo aún más impresionante es el cambio en lo que estamos construyendo:

Screenshot 2025-10-01 at 9.41.51 PM.png

Hemos reasignado con éxito aproximadamente un 20% del esfuerzo de ingeniería lejos de las correcciones y hacia funcionalidades, mejoras de rendimiento, refactors y tareas. Este cambio demuestra que invertir en calidad y arquitectura no te ralentiza. Te acelera.

La base de Cal.com permite la excelencia para coss.com

  • Cal.com es un negocio estable y rentable que seguiremos haciendo crecer.

    • Este éxito nos da una ventaja única mientras construimos coss.com. A diferencia de los primeros días de Cal.com, cuando necesitábamos movernos rápido para lograr product-market fit y construir un negocio sostenible, coss.com parte desde una posición de fortaleza.
  • No necesitamos darnos prisa con coss.com.

    • La estabilidad de Cal.com significa que podemos permitirnos construir coss.com bien desde el primer día. Tenemos el lujo de implementar estos estándares de ingeniería sin la presión de una demanda inmediata del mercado ni de restricciones de financiación. Es un punto de partida fundamentalmente distinto.
  • La "lentitud" es una inversión, no un coste.

    • Sí, seguir estos estándares puede parecer más lento al principio e incluso puede frustrar a algunos ingenieros. Escribir DTOs lleva más tiempo que pasar los tipos de base de datos directamente al frontend. Crear abstracciones adecuadas e inyección de dependencias requiere más diseño inicial. Mantener una cobertura de pruebas superior al 80% para código nuevo exige disciplina. Pero esta aparente lentitud es temporal, y la recompensa es exponencial.

Piensa en los rendimientos compuestos...

  • El código que está bien arquitectado desde el principio no necesita grandes refactors después

  • Una alta cobertura de pruebas evita bugs que, de otro modo, consumirían semanas de depuración y hotfixes (consulta 2023 hasta mediados de 2024)

  • Las abstracciones adecuadas hacen que añadir nuevas funcionalidades sea drásticamente más rápido con el tiempo

  • Los límites claros y los DTOs evitan la erosión arquitectónica que, al final, obliga a reescrituras completas

La trayectoria de Cal.com muestra lo que pasa cuando optimizas para la velocidad inmediata. Alta velocidad inicial que se degrada gradualmente a medida que se acumula deuda técnica, los atajos arquitectónicos crean cuellos de botella y se dedica cada vez más tiempo a arreglar problemas que a construir funcionalidades (consulta el gráfico anterior donde estábamos dedicando entre el 55 y el 60% del esfuerzo de ingeniería a correcciones).

La trayectoria de coss.com abrazará el poder de construir bien desde el primer día. Un poco menos de velocidad al principio mientras se establecen los patrones adecuados, seguida de una aceleración exponencial a medida que esos patrones dan frutos y permiten desarrollar más rápido con mayor confianza.

Principios básicos

1. Sin calidad aplazada

  • Vamos a minimizar el "lo haré en un PR de seguimiento" para los refactors pequeños.

    • Los PRs de seguimiento para mejoras menores rara vez llegan a materializarse. En su lugar, se acumulan como deuda técnica que nos pesa meses o años más tarde. Si un pequeño refactor puede hacerse ahora, hazlo ahora. Los seguimientos deben reservarse para cambios sustanciales que realmente justifiquen PRs separados o para casos excepcionales y urgentes.

2. Altos estándares en el code review

  • No dejes pasar PRs con muchos detalles menores solo para evitar ser "la mala persona".

    • Así es exactamente como las codebases se vuelven descuidadas con el tiempo. El code review no va de ser amable. Va de mantener los estándares de calidad que exige nuestra infraestructura. Cada detalle importa. Cada violación de patrones importa. Corrígelos antes de hacer merge, no después.

3. Empujarnos mutuamente a hacer lo correcto

  • Nos hacemos responsables unos a otros de la calidad

    • Cortar esquinas puede parecer más rápido en el momento, pero crea problemas que luego frenan a todo el mundo. Cuando veas a un compañero a punto de hacer merge de un PR con problemas evidentes, dilo. Cuando alguien sugiera un apaño rápido en lugar de la solución correcta, recházalo. Cuando te tiente saltarte pruebas o ignorar patrones arquitectónicos, espera que tus compañeros te cuestionen.
  • No se trata de ser difícil ni de ralentizar a la gente

    • Se trata de la responsabilidad colectiva sobre nuestra codebase y nuestra reputación. Cada atajo que toma una persona se convierte en un problema para todos. Cada esquina recortada hoy significa más sesiones de depuración, más hotfixes y clientes más frustrados mañana.
  • Haz normal cuestionar las malas decisiones, con respeto

    • Si alguien dice "vamos a hardcodear esto por ahora", la respuesta esperada debería ser "¿qué haría falta para hacerlo bien a la primera?". Si alguien quiere hacer commit de código sin probar, el equipo debe oponerse. Si alguien propone copiar y pegar en vez de crear una abstracción adecuada, señálalo con respeto.
  • Estamos construyendo algo que casi nunca puede fallar

    • Ese nivel de fiabilidad no aparece por casualidad. Ocurre cuando cada ingeniero se siente responsable de la calidad, no solo de su propio código, sino de todo el sistema. Salimos adelante como equipo o fracasamos como equipo.

4. Apunta a la simplicidad

  • Prioriza la claridad por encima de la brillantez

    • El objetivo es un código fácil de leer y entender rápidamente, no una complejidad elegante. Los sistemas simples reducen la carga cognitiva de cada ingeniero.
  • Hazte las preguntas adecuadas

    • ¿Estoy resolviendo realmente el problema que tengo delante?

    • ¿Estoy pensando demasiado en posibles casos de uso futuros?

    • ¿He considerado al menos otra alternativa para resolver esto? ¿Cómo se compara?

  • Simple no significa sin funcionalidades

    • Solo porque nuestro objetivo sea crear sistemas simples no significa que deban sentirse anémicos y sin funciones obvias.

5. Automatiza todo

  • Aprovecha la IA

    • Genera el 80% del código repetitivo y no crítico usando IA, permitiéndonos centrarnos exclusivamente en la lógica de negocio compleja y las arquitecturas críticas. Los equipos que adoptan workflows modernos asistidos por IA dependen cada vez más de herramientas como los mejores servidores MCP para mejorar la productividad y la orquestación del desarrollo.

    • Crea alertas sin ruido y un manejo inteligente de errores.

    • Las pruebas manuales son cada vez más cosa del pasado. La IA puede construir rápidamente y de forma inteligente megasuites de pruebas para nosotros.

  • Nuestra CI es el jefe final

    • Todo en este documento de estándares se comprueba antes de que el código se haga merge en los PRs

    • Ninguna sorpresa llega a main

    • Las comprobaciones son rápidas y útiles

Estándares arquitectónicos

Estamos pasando a un modelo arquitectónico estricto basado en Vertical Slice Architecture y Domain-Driven Design (DDD). Los siguientes patrones y principios se aplicarán con rigor en las revisiones de PR y mediante linting.

Vertical Slice Architecture: packages/features

Nuestra codebase está organizada por dominio, no por capa técnica. El directorio packages/features es el corazón de este enfoque arquitectónico. Cada carpeta dentro representa una vertical slice completa de la aplicación, impulsada por el dominio al que afecta.

Estructura:

packages/features/  
  availability/  
  bookings/  
  ooo/  
  payments/  
  routing/  
  scheduling/  
  ...

Cada carpeta de feature es una vertical slice autocontenida que incluye todo lo necesario para ese dominio:

  • Lógica de dominio: Las reglas de negocio y entidades principales específicas de esa feature

  • Servicios de aplicación: Orquestación de casos de uso para ese dominio

  • Repositorios: Acceso a datos específico de las necesidades de esa feature

  • DTOs: Objetos de transferencia de datos para cruzar límites

  • Componentes de UI: Componentes del frontend relacionados con esta feature (cuando aplique)

  • Tests: Tests unitarios, de integración y e2e para esta feature

Por qué importan las vertical slices

La arquitectura tradicional por capas organiza por preocupaciones técnicas:

// Bad: Organized by layer
src/  
  controllers/    
    AvailabilityController.ts    
    BookingController.ts    
    OOOController.ts  
  services/    
    AvailabilityService.ts    
    BookingService.ts    
    OOOService.ts  
  repositories/    
    AvailabilityRepository.ts    
    BookingRepository.ts    
    OOORepository.ts

Esto crea varios problemas:

  • Los cambios en una feature requieren tocar archivos repartidos por varios directorios

  • Es difícil entender qué hace una feature porque su código está fragmentado

  • Los equipos se pisan los unos a los otros cuando trabajan en diferentes features

  • No puedes extraer o descontinuar fácilmente una feature

La arquitectura de vertical slice organiza por dominio:

// Good: Organized by domain
packages/features/
  availability/    
    services/      
      AvailabilityApplicationService.ts      
      AvailabilityDomainService.ts    
    repositories/      
      AvailabilityRepository.ts    
    dto/      
      AvailabilityDto.ts  
bookings/    
  services/      
    BookingApplicationService.ts      
    BookingDomainService.ts    
  repositories/      
    BookingRepository.ts    
  dto/      
    BookingDto.ts

Esto soluciona estos problemas:

  • Todo lo relacionado con Availability vive en packages/features/availability

  • Puedes entender toda la feature de Availability explorando un solo directorio

  • Los equipos pueden trabajar en diferentes features sin conflictos (si el equipo de ingeniería de Cal.com crece, pero lo más seguro es que en coss.com tengamos equipos encargándose de paquetes importantes)

  • Las features están débilmente acopladas y pueden evolucionar de forma independiente

Directrices para la organización de features

En teoría, cada feature se puede desplegar de forma independiente. Aunque quizá no las despleguemos por separado, organizarlas así nos obliga a mantener claras las dependencias y mínimo el acoplamiento. Este es el principio y el éxito de los microservicios, aunque todavía no vamos a desplegar microservicios.

Las features se comunican a través de interfaces bien definidas. Si bookings necesita datos de availability, importa desde @calcom/features/availability mediante interfaces exportadas, no entrando en detalles internos de implementación.

El código compartido vive en los lugares adecuados:

  • Utilidades agnósticas al dominio y preocupaciones transversales (auth, logging): packages/lib

  • Primitivas de UI compartidas: packages/ui (y pronto coss.com ui)

Los límites del dominio se aplican automáticamente. Vamos a construir reglas de linting que impidan entrar en el interior de las features donde no deberías poder hacerlo. Si packages/features/bookings intenta importar desde packages/features/availability/services/internal, el linter lo bloqueará. Todas las dependencias entre features deben pasar por la API pública de la feature.

good_architecture_diagram.pngbad_architecture_diagram.png

Las nuevas features empiezan como vertical slices. Cuando construyas algo nuevo, crea una nueva carpeta en packages/features con el vertical slice completo. Así queda claro qué estás construyendo y todo se mantiene organizado desde el primer día.

Beneficios

  • Facilidad de descubrimiento

    • ¿Buscas la lógica de bookings? Está toda en packages/features/bookings. No hace falta buscar entre controllers, services, repositories y utilities repartidos por todo el código.
  • Más fácil de testear

    • Prueba toda la feature como una unidad. Tienes todas las piezas en un solo lugar, lo que hace que las pruebas de integración sean naturales y sencillas.
  • Dependencias más claras

    • Cuando ves import { getAvailability } from '@calcom/features/availability', sabes exactamente de qué feature dependes. Cuando las dependencias se vuelven demasiado complejas, es obvio y se puede abordar.

Repository Pattern y Dependency Injection

Las decisiones tecnológicas no deben filtrarse por toda la aplicación. El problema de Prisma lo ilustra perfectamente. Ahora mismo tenemos referencias a Prisma repartidas por cientos de archivos. Esto crea un acoplamiento enorme y hace que los cambios tecnológicos sean prohibitivamente caros. Estamos sintiendo ese dolor ahora al actualizar Prisma a v6.16. Algo que debería haber sido solo un refactor localizado detrás de repositories protegidos ha sido una persecución enrevesada, casi interminable, de incidencias a través de varias apps.

El estándar a partir de ahora:

  • Todo acceso a la base de datos debe pasar por clases Repository. Ya llevamos bastante adelantado este camino.

  • Los repositories son el único código que conoce Prisma (o cualquier otro ORM). No debe haber lógica en ellos.

  • Los repositories se inyectan mediante contenedores de Dependency Injection

  • Si alguna vez cambiamos de Prisma a Drizzle u otro ORM, los únicos cambios necesarios serán:

    • Implementaciones de los repositories

    • Cableado del contenedor DI para los nuevos repositories

    • Nada más en el código debería importarle ni cambiar

No es teórico. Así es como construimos sistemas mantenibles.

Objetos de Transferencia de Datos (DTOs)

Los tipos de base de datos no deben filtrarse al frontend. Esto se ha convertido en un atajo popular en nuestro stack tecnológico, pero es un code smell que crea varios problemas.

  • Acoplamiento tecnológico (los tipos de Prisma acaban en componentes de React)

  • Riesgos de seguridad (filtración accidental de campos sensibles)

  • Contratos frágiles entre servidor y cliente (esto es especialmente problemático a medida que construimos muchas más API)

  • Incapacidad de evolucionar el esquema de la base de datos de forma independiente

  • Todas las conversiones de DTOs pasan por Zod, incluso para una respuesta de API, para garantizar que todos los datos se validan antes de enviarlos al usuario. Mejor fallar que devolver algo incorrecto.

El estándar a partir de ahora:
Crea DTOs explícitos en cada frontera arquitectónica.

  1. Capa de datos → Capa de aplicación → API: transforma los modelos de base de datos en DTOs de la capa de aplicación y luego transforma los DTOs de aplicación en DTOs específicos de la API

  2. API → Capa de aplicación → Capa de datos: transforma los DTOs de la API a través de la capa de aplicación y en DTOs específicos de datos

Sí, esto requiere más código. Sí, merece la pena. Las fronteras explícitas evitan la erosión arquitectónica que crea pesadillas de mantenimiento a largo plazo.

Patrones de Domain-Driven Design

Los siguientes patrones deben usarse correctamente y de forma consistente:

  • Application Services

    • Orquestan casos de uso, coordinan entre domain services y repositories
  • Domain Services

    • Contienen la lógica de negocio que no encaja de forma natural en una sola entidad
  • Repositories

    • Abstraen el acceso a datos, aíslan las decisiones tecnológicas
  • Dependency Injection

    • Permite un acoplamiento flexible, facilita las pruebas, aísla responsabilidades
  • Caching Proxies

    • Envuelven repositories o services para añadir comportamiento de caché de forma transparente

    • No es la única forma de hacer caché, claro, pero sí un buen punto de partida

  • Decorators

    • Añaden preocupaciones transversales (logging, métricas, etc.) sin contaminar la lógica de dominio

Consistencia del código base

Nuestros codebases deberían parecer que los escribió una sola persona. Este nivel de consistencia exige una adhesión estricta a patrones establecidos, reglas de linting completas que impongan estándares arquitectónicos, code reviews que rechacen violaciones de patrones y la ayuda de revisores de código con IA.

Mover las condiciones al punto de entrada de la aplicación

Las sentencias if deben ir en el punto de entrada, no repartidas por tus services. Este es uno de los principios arquitectónicos más importantes para mantener un código limpio y centrado que no se deslice hacia una complejidad inmantenible.

Así es como el código se degrada con el tiempo: se escribe un service para un propósito claro y específico. La lógica es limpia y está enfocada. Luego llega un nuevo requisito de producto, y alguien añade una sentencia if. Unos años y varios requisitos más tarde, ese service está lleno de comprobaciones condicionales para distintos escenarios. El service se ha convertido en:

  • Complicado y difícil de leer

  • Difícil de entender y razonar sobre él

  • Más propenso a bugs

  • Violar la responsabilidad única (gestionar demasiados casos distintos)

  • Casi imposible de probar a fondo

El service se ha salido de sus límites en términos de responsabilidades y lógica.

Una solución: patrón Factory con services especializados
Usa el patrón factory para tomar decisiones en el punto de entrada y luego delega en services especializados que gestionen su lógica concreta sin condicionales.

Ejemplo de nuestro código base:
BillingPortalServiceFactory determina si la facturación es para una organización, un equipo o un usuario individual, y luego devuelve el service adecuado:

export class BillingPortalServiceFactory {  
  static async createService(teamId: number): Promise<BillingPortalService> {    
    const team = await teamRepository.findById({ id: teamId });        
    if (!team) {      
      throw new Error("Team not found");    
    }        
    // Decision made HERE at the entry point    
    if (team.isOrganization) {      
      return new OrganizationBillingPortalService();    
    }        
    return new TeamBillingPortalService();  
  }   
  
  static createUserService(): UserBillingPortalService {    
    return new UserBillingPortalService();  
  }
}

Cada service gestiona entonces su lógica específica sin necesidad de comprobar «¿soy una org o un team?»:

// OrganizationBillingPortalService handles ONLY organization logic
class OrganizationBillingPortalService extends BillingPortalService {  
  async checkPermissions(userId: number, teamId: number): Promise<boolean> {    
    return await this.permissionService.checkPermission({      
      userId,      
      teamId,      
      permission: "organization.manageBilling",  // Organization-specific      
      fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],    
    });  
  }  
  // ... more organization-specific logic
}

// TeamBillingPortalService handles ONLY team logic
class TeamBillingPortalService extends BillingPortalService {  
  async checkPermissions(userId: number, teamId: number): Promise<boolean> {    
    return await this.permissionService.checkPermission({      
      userId,      
      teamId,      
      permission: "team.manageBilling",  // Team-specific      
      fallbackRoles: [MembershipRole.ADMIN, MembershipRole.OWNER],    
    });  
  }  
  // ... more team-specific logic
}

Por qué esto importa

  • Los services se mantienen centrados

    • Cada service tiene una sola responsabilidad y no necesita saber nada de otros contextos. OrganizationBillingPortalService no contiene sentencias if que comprueben if (isTeam) o if (isUser). Solo sabe cómo gestionar organizaciones.
  • Los cambios se aíslan

    • Cuando necesites modificar la lógica de facturación de la organización, solo tocas OrganizationBillingPortalService. No te arriesgas a romper la facturación del equipo o del usuario. No tienes que seguir condicionales anidados para averiguar qué ruta toma tu código.
  • Probar es sencillo

    • Prueba cada servicio de forma independiente con sus escenarios específicos. No hace falta probar todas las combinaciones de condicionales en distintos contextos.
  • Los nuevos requisitos no ensucian el código existente

    • Por ejemplo, cuando necesites añadir facturación para enterprise con reglas distintas, creas EnterpriseBillingPortalService. La factory gana un condicional más, pero los servicios existentes siguen intactos y centrados.

Cómo conseguirlo

  • Sube los condicionales a controladores, factories o lógica de routing. Deja que estos puntos de entrada tomen decisiones sobre qué servicio usar.

  • Mantén los servicios puros y centrados en una sola responsabilidad. Si un servicio necesita comprobar "qué tipo soy", probablemente necesitas varios servicios.

  • Prefiere el polimorfismo a los condicionales

    • Las interfaces definen el contrato. Las implementaciones concretas aportan los detalles.
  • Vigila la acumulación de if

    • Durante la revisión de código, si ves que un servicio va ganando condicionales para distintos escenarios, eso es una señal para refactorizarlo en servicios especializados.

Diseño de API: controladores ligeros y abstracción HTTP

  • Los controladores son capas ligeras que solo manejan asuntos de HTTP.

    • Reciben solicitudes, las procesan y mapean los datos a DTOs que se pasan a la lógica central de la aplicación. De ahora en adelante, no debería verse lógica de aplicación o lógica central en las rutas de la API ni en los handlers de tRPC.
  • Debemos separar la tecnología HTTP de nuestra aplicación.

    • La forma en que transferimos datos entre cliente y servidor (ya sea REST, tRPC, etc.) no debería influir en cómo funciona nuestra aplicación central. HTTP es un mecanismo de entrega, no un motor arquitectónico.

Responsabilidades del controlador (y SOLO estas):

  • Recibir y validar las solicitudes entrantes

  • Extraer datos de parámetros, cuerpo y cabeceras de la solicitud

  • Transformar los datos de la solicitud en DTOs

  • Llamar a los servicios de aplicación adecuados con esos DTOs

  • Transformar las respuestas del servicio de aplicación en DTOs de respuesta

  • Devolver respuestas HTTP con los códigos de estado correctos

Los controladores NO deberían:

  • Contener lógica de negocio o reglas de dominio

  • Acceder directamente a bases de datos o servicios externos

  • Realizar transformaciones complejas de datos o cálculos

  • Tomar decisiones sobre qué debe hacer la aplicación

  • Conocer detalles de implementación del dominio

Ejemplo de patrón de controlador ligero:

// Good: Thin controller
export async function createBookingHandler(req: Request, res: Response) {  
  // Extract and validate request data  
  const createBookingRequest = validateCreateBookingRequest(req.body);    
  // Transform to DTO  
  const createBookingDto = {    
    userId: req.user.id,    
    eventTypeId: createBookingRequest.eventTypeId,    
    startTime: createBookingRequest.startTime,    
    // ... other fields  
  };    
  // Delegate to application service  
  const result = await bookingApplicationService.createBooking(createBookingDto);    
  // Transform response and return  
  const responseDto = mapBookingToResponseDto(result);  
  res.status(201).json(responseDto);
}

// Bad: Fat controller with business logic
export async function createBookingHandler(req: Request, res: Response) {  
  // This is wrong - business logic in controller  
  if (req.body.startTime < new Date()) {    
    return res.status(400).json({ error: "Cannot book in the past" });  
  }    
  
  // This is wrong - direct database access  
  const existingBooking = await prisma.booking.findFirst({    
    where: { eventTypeId: req.body.eventTypeId, startTime: req.body.startTime }  
  });    
  
  // This is wrong - complex business calculations  
  const duration = calculateBookingDuration(req.body.eventTypeId);  
  // ... more business logic
}

Versionado de la API y cambios incompatibles

Sin cambios incompatibles. Esto es crítico. Una vez que un endpoint de la API es público, debe seguir siendo estable. Los cambios incompatibles destruyen la confianza de los desarrolladores y crean pesadillas de integración para nuestros usuarios.

Estrategias para evitar cambios incompatibles:

  • Añade siempre los nuevos campos como opcionales

  • Usa versionado de la API cuando tengas que cambiar el comportamiento existente

  • Depreca los endpoints antiguos con suavidad y con rutas de migración claras

  • Mantén la compatibilidad hacia atrás durante al menos dos versiones principales

Cuando tengas que hacer cambios incompatibles:

  • Crea una nueva versión de la API usando el versionado específico por fecha en API v2 (quizá también veamos el versionado con nombre que Stripe ha introducido recientemente)

  • Ejecuta ambas versiones a la vez durante la transición (ya hacemos esto en API v2)

  • Proporciona herramientas automáticas de migración cuando sea posible

  • Da a los usuarios tiempo suficiente para migrar (mínimo 6 meses para APIs públicas)

  • Documenta exactamente qué cambió y por qué

Rendimiento y complejidad algorítmica

Construimos para grandes organizaciones y equipos. Lo que funciona bien con 10 usuarios o 50 registros puede derrumbarse bajo el peso de una escala empresarial. El rendimiento no es algo que optimizamos después. Es algo que construimos bien desde el principio.

Piensa en la escala desde el primer día

Cuando desarrolles funciones, pregúntate siempre: "¿Cómo se comporta esto con 1.000 usuarios? ¿10.000 registros? ¿100.000 operaciones?" La diferencia entre algoritmos O(n) y O(n²) puede ser imperceptible en desarrollo, pero catastrófica en producción.

Patrones O(n²) habituales que debes evitar:

  • Iteraciones anidadas sobre arrays (.map dentro de .map, .forEach dentro de .forEach)

  • Métodos de array como .some, .find o .filter dentro de bucles o callbacks

  • Comprobar cada elemento contra todos los demás sin optimización

  • Filtros encadenados o mapeos anidados sobre listas grandes

Ejemplo del mundo real: Para 100 huecos disponibles y 50 periodos ocupados, un algoritmo O(n²) hace 5.000 comprobaciones. Llévalo a 500 huecos y 200 periodos ocupados, y estarás haciendo 100.000 operaciones. Eso supone un aumento de 20x en la carga computacional para solo un aumento de 5x en los datos.

Elige las estructuras de datos y los algoritmos adecuados

La mayoría de los problemas de rendimiento se resuelven eligiendo mejores estructuras de datos y algoritmos:

  • Ordenación + salida anticipada: ordena tus datos una vez y luego sale de los bucles cuando sepas que los elementos restantes no coincidirán

  • Búsqueda binaria: usa búsqueda binaria para consultas en arrays ordenados en lugar de recorridos lineales

  • Técnicas de dos punteros: para fusionar o intersectar secuencias ordenadas, recorre ambas con punteros en lugar de bucles anidados

  • Mapas/conjuntos hash: usa objetos o Sets para búsquedas O(1) en lugar de .find o .includes en arrays

  • Árboles de intervalos: para scheduling, disponibilidad y consultas por rangos, usa estructuras de árbol adecuadas en lugar de comparaciones a fuerza bruta

Transformación de ejemplo:

// Bad: O(n²) - checks every slot against every busy time
availableSlots.filter(slot => {  
  return !busyTimes.some(busy => checkOverlap(slot, busy));
});

// Good: O(n log n) - sort once, break early
const sortedBusy = [...busyTimes].sort((a, b) => a.start - b.start);
availableSlots.filter(slot => {  
  for (const busy of sortedBusy) {    
    if (busy.start >= slot.end) break; // No more overlaps possible    
    if (busy.end <= slot.start) continue; // This one doesn't overlap    
    return false; // Overlap found  
  }  
  return true;
});

Comprobaciones automatizadas de rendimiento

Vamos a implementar varias capas de defensa contra regresiones de rendimiento:

Reglas de linting que detecten:

  • Funciones con bucles anidados o métodos de arrays anidados

  • Varias llamadas anidadas a .some, .find o .filter

  • Recursión sin memoización

  • Anti-patrones conocidos para nuestro dominio (scheduling, comprobaciones de disponibilidad, etc.)

Benchmarks de rendimiento en CI que:

  • Ejecuten algoritmos críticos con datos realistas y a gran escala

  • Comparen los tiempos de ejecución con la referencia en cada PR

  • Bloqueen merges que introduzcan regresiones de rendimiento

  • Prueben con datos a escala empresarial (miles de usuarios, decenas de miles de registros)

Monitorización en producción que:

  • Siga el tiempo de ejecución de las rutas críticas

  • Alerta cuando los algoritmos se ralentizan a medida que crecen los datos

  • Detecte regresiones antes de que los usuarios las noten

  • Proporcione datos de rendimiento reales para guiar las optimizaciones

El rendimiento es una característica

El rendimiento no es opcional. No es algo que "dejemos para más tarde". Para clientes enterprise que reservan a través de grandes equipos, unas respuestas lentas significan pérdida de productividad y usuarios frustrados (nuestra experiencia con algunos clientes enterprise más grandes puede dar fe de ello).

Toda persona ingeniera debería:

  • Perfila tu código antes de optimizarlo, pero piensa en la complejidad desde el principio

  • Prueba con datos realistas y a gran escala (no solo 5 registros de prueba). Ya tenemos scripts de seed creados. Probablemente haya que ampliarlos.

  • Elige algoritmos y estructuras de datos eficientes desde el principio

  • Fíjate en las iteraciones anidadas durante la revisión de código

  • Cuestiona cualquier algoritmo que escale con el producto de dos variables

La realidad NP-hard de la programación

Los problemas de programación son, por naturaleza, NP-hard. Esto significa que, a medida que crece el número de restricciones, participantes o franjas horarias, la complejidad computacional puede dispararse de forma exponencial. La mayoría de los algoritmos óptimos de programación tienen una complejidad temporal exponencial en el peor caso, así que la elección del algoritmo es absolutamente crítica.

Implicaciones en el mundo real:

  • Encontrar la hora óptima de reunión para 10 personas en 3 zonas horarias con restricciones individuales de disponibilidad es computacionalmente costoso

  • Añadir detección de conflictos, búferes y un sinfín de opciones más amplifica el problema

  • Las malas elecciones de algoritmo que van bien para equipos pequeños se vuelven completamente inutilizables para organizaciones grandes

  • Lo que tarda milisegundos para 5 usuarios puede tardar muchos segundos para organizaciones

Estrategias para gestionar la complejidad NP-hard:

  • Usa algoritmos de aproximación que encuentren soluciones "suficientemente buenas" rápido en lugar de soluciones perfectas lentamente

  • Implementa una caché agresiva de horarios y disponibilidad calculados

  • Precalcula los escenarios comunes en horas de menor actividad

  • Divide los grandes problemas de programación en bloques más pequeños y manejables

  • Establece límites de tiempo razonables y recurre a algoritmos más sencillos cuando haga falta

Por eso el rendimiento no es solo algo deseable en el software de programación. Es la base que determina si tu sistema puede escalar para cubrir las necesidades enterprise o si se derrumba bajo patrones de uso del mundo real.

Requisitos de cobertura de código

  • Seguimiento global de la cobertura

    • Seguimos la cobertura global de la base de código como una métrica clave que mejora con el tiempo. Esto nos da visibilidad sobre nuestra madurez en pruebas y ayuda a identificar las áreas que necesitan atención. El porcentaje de cobertura global se muestra de forma destacada en nuestros paneles.
  • Cobertura del 80%+ para código nuevo

    • Cada PR debe tener una cobertura de pruebas cercana al 80%+ para el código que introduce o modifica. Esto se aplica automáticamente en nuestro pipeline de CI. Si añades 50 líneas de código nuevo, esas 50 líneas deben estar cubiertas por pruebas. Si modificas una función existente, tus cambios deben probarse. Esto es la cobertura global de pruebas. La cobertura de pruebas unitarias debe estar cerca del 100%, especialmente con la posibilidad de aprovechar la IA para ayudar a generarlas.

Abordando el argumento de que "la cobertura no cuenta toda la historia": Sí, sabemos que la cobertura no garantiza pruebas perfectas. Sabemos que puedes escribir pruebas sin sentido que cubran cada línea pero no prueben nada relevante. Sabemos que la cobertura es solo una métrica entre muchas. Pero, sin duda, es mejor aspirar a un porcentaje alto que no tener ni idea de dónde estás.

Medir el éxito

  • "Velocity" (lo tomamos prestado de Scrum aunque no vayamos a usar Scrum)

    • Crecimiento continuado en las métricas mensuales (funcionalidades, mejoras, refactors)
  • Calidad

    • Reducir el esfuerzo de PR dedicado a correcciones del 35% actual al 20% o menos antes de final de 2026 (calculado en función de cambios de archivos y altas/bajas)
  • Salud arquitectónica

    • Métricas sobre cumplimiento de patrones, acoplamiento tecnológico, violaciones de límites
  • Eficiencia de revisión

    • PR más pequeños, revisiones más rápidas, menos rondas de feedback
  • Disponibilidad de la aplicación y de la API

    • ¿Qué tan cerca estamos del 99.99%?
Keith Williams

Post by

Keith Williams

Head of Engineering