96 puntos por GN⁺ 2025-08-18 | 1 comentarios | Compartir por WhatsApp
  • Un buen diseño de sistemas es aquel que no parece complejo y en el que no surgen mayores problemas durante mucho tiempo
  • Manejar el estado (state) es la parte más difícil del diseño de sistemas, y es importante reducir al máximo la cantidad de componentes que almacenan estado
  • La base de datos suele ser el lugar donde se guarda el estado, por lo que se necesita un enfoque centrado en el diseño de esquemas e indexación, además de la eliminación de cuellos de botella
  • Caching, procesamiento de eventos y trabajos en segundo plano deben introducirse con cuidado para mejorar el rendimiento y la mantenibilidad, evitando abusar de ellos
  • Más que un diseño complejo, la clave para construir sistemas sostenibles y estables está en usar adecuadamente componentes y metodologías simples que ya han sido suficientemente probados

Definición del diseño de sistemas y enfoque general

  • Si el diseño de software es ensamblar código, el diseño de sistemas es el proceso de combinar distintos servicios
  • Los principales componentes del diseño de sistemas son servidores de aplicaciones, bases de datos, cachés, colas, buses de eventos y proxies
  • Un buen diseño provoca reacciones como: "no hay ningún problema especial", "terminó siendo más fácil de lo pensado" o "no hace falta preocuparse por esta parte"
  • En cambio, un diseño complejo y llamativo puede ocultar problemas fundamentales o reflejar sobreingeniería
  • En vez de introducir un sistema complejo desde el inicio, conviene partir de una estructura simple y mínima que funcione, y evolucionarla gradualmente

Distinción entre estado (state) y sin estado (stateless)

  • La parte más complicada del diseño de software es justamente la gestión del estado
  • Los servicios que no almacenan información y devuelven resultados de inmediato, como el renderizado de PDF de GitHub, son stateless
  • En cambio, los servicios que escriben en una base de datos sí gestionan estado
  • Es mejor reducir al máximo los componentes con almacenamiento de estado dentro del sistema. Eso disminuye la complejidad y la probabilidad de fallas
  • Se recomienda una arquitectura donde solo un servicio gestione el estado, mientras que los demás se concentren en roles stateless, como llamadas API o emisión de eventos

Diseño de bases de datos y cuellos de botella

Diseño de esquema e índices

  • Para almacenar datos, se necesita un diseño de esquema fácil de leer para las personas
  • Un esquema demasiado flexible, por ejemplo guardar todo en una columna JSON, puede cargar innecesariamente al código de la aplicación y al rendimiento
  • Hay que definir índices adecuados según las columnas sobre las que se harán consultas frecuentes. Poner índices en todo solo genera overhead innecesario

Cómo resolver cuellos de botella

  • El acceso a la base de datos suele convertirse en un cuello de botella pesado
  • Siempre que sea posible, es mejor procesar datos complejos dentro de la base de datos, por ejemplo con joins (JOIN), en lugar de hacerlo en la aplicación, por razones de rendimiento
  • Si se usa un ORM, hay que evitar el error de disparar consultas dentro de un loop
  • Según el caso, dividir una consulta para ajustar la carga de la base de datos o la complejidad del query también puede ser una opción
  • Una estrategia eficaz es distribuir las consultas de lectura a réplicas (read replicas) para reducir la carga del nodo principal de escritura
  • Cuando se concentra una gran cantidad de consultas, las transacciones y operaciones de escritura pueden sobrecargar la base de datos con facilidad, por lo que conviene considerar query throttling (limitación de consultas)

Separar trabajos lentos y rápidos

  • Las tareas con las que interactúa el usuario necesitan responder en unos cientos de milisegundos
  • Para trabajos que tardan mucho, por ejemplo conversiones grandes de PDF, resulta efectivo entregar de inmediato en el frontend solo lo mínimo necesario y dejar el resto en segundo plano
  • Los trabajos en segundo plano suelen funcionar con una cola, por ejemplo Redis, junto con un job runner
  • Para tareas programadas a mucho tiempo vista, en lugar de Redis suele ser más práctico gestionarlas con una tabla aparte en la base de datos y ejecutarlas mediante un scheduler

Caching

  • El caching ayuda a reducir costos y mejorar el rendimiento cuando se repiten operaciones iguales o costosas
  • Normalmente, los ingenieros junior que acaban de aprender sobre caché quieren cachearlo todo, mientras que los ingenieros con más experiencia son más cautelosos al introducir caché
  • La caché introduce un nuevo estado, por lo que existen riesgos de sincronización, errores y datos stale
  • Lo recomendable es intentar primero mejoras de rendimiento, como añadir índices a las consultas, y solo después aplicar caching
  • Para cachés de gran volumen, también puede utilizarse una estrategia de almacenamiento periódico en document storage como S3 o Azure Blob Storage, en lugar de Redis o Memcached

Procesamiento de eventos

  • La mayoría de las empresas cuentan con un event hub, como Kafka, y varios servicios distribuyen el procesamiento con base en eventos
  • Más que abusar de los eventos, un diseño simple de API de solicitud–respuesta es más útil para logging y resolución de problemas
  • El procesamiento basado en eventos encaja cuando el emisor no necesita preocuparse por cómo actúa el receptor, o en escenarios de alto volumen y tolerancia a la latencia

Formas de entrega de datos: push y pull

  • Hay dos formas de entrega de datos: pull (respuesta después de una solicitud) y push (entrega automática cuando hay cambios)
  • El enfoque pull es simple, pero puede provocar solicitudes repetidas y sobrecarga
  • El enfoque push entrega los cambios al cliente apenas ocurren en el servidor, por lo que es más eficiente y favorable para mantener datos actualizados
  • Para manejar grandes volúmenes de clientes, hace falta ampliar la infraestructura según el enfoque elegido, por ejemplo colas de eventos o varios servidores de caché

Enfoque en las hot paths

  • Las hot paths son las rutas más importantes del sistema y por donde fluye la mayor cantidad de datos
  • Las hot paths dejan poco margen de maniobra y, si el diseño falla, pueden provocar problemas graves en todo el servicio, así que requieren un diseño especialmente cuidadoso
  • Más que dispersar recursos en funciones menores con muchas opciones, resulta más efectivo concentrar el diseño y las pruebas en las hot paths

Logging, métricas y tracing

  • Para diagnosticar la causa de una falla, hay que registrar de forma activa logs detallados de las rutas anómalas (unhappy path)
  • Es necesario recopilar métricas básicas de observabilidad, como recursos del sistema (CPU/memoria), tamaño de las colas y tiempos de solicitudes o trabajos
  • En vez de mirar solo promedios, también hay que observar métricas de distribución como latencias p95 y p99. Un pequeño porcentaje de solicitudes lentas puede ser justamente el problema de los usuarios clave

Kill switch, reintentos y recuperación ante fallas

  • Es importante usar estratégicamente un kill switch (bloqueo temporal del sistema) y los reintentos
  • Reintentar sin criterio solo carga a otros servicios; para que sea efectivo, primero hay que controlar las solicitudes con mecanismos como un circuit breaker
  • Introducir una Idempotency Key permite evitar trabajo duplicado al reprocesar una misma solicitud
  • En algunos escenarios de falla, hay que elegir entre fail open o fail closed. Por ejemplo, en rate limiting conviene más fail open para reducir el impacto sobre el usuario. En cambio, en autenticación, fail closed es indispensable

Cierre

  • Aunque aquí se omitieron algunos temas como separación de servicios, contenedores, adopción de VM y tracing, usar componentes bien probados en el lugar adecuado sigue siendo la forma más estable de construir sistemas a largo plazo
  • Los diseños técnicamente especiales son en realidad muy raros, y un diseño tan simple que hasta parece aburrido es el que más se usa en la práctica
  • En esencia, un buen diseño de sistemas es un proceso de combinar con seguridad metodologías suficientemente probadas sin llamar la atención

1 comentarios

 
GN⁺ 2025-08-18
Opiniones de Hacker News
  • A menudo siento que estoy solo en esto. Los ingenieros ven sistemas complejos y, como hay muchos elementos interesantes, piensan: “¡Aquí sí se está haciendo diseño de sistemas de verdad!”, pero en realidad muchas veces un sistema complejo es el resultado de la ausencia de un buen diseño. Si estás buscando trabajo, conviene olvidar por completo este hecho durante la entrevista. Yo mismo cometí el error de expresar esta idea con honestidad en entrevistas de diseño de sistemas. En una entrevista para una app de una startup hipotética, respondí cosas como “con este nivel de QPS, el backpressure no importa”, “no hace falta usar una cola en vez de un cron job, aunque claro que hay trade-offs”, “¿SQL vs NoSQL? usa lo que el equipo mejor conozca”, pero los entrevistadores no quieren ese tipo de respuestas. Quieren que llenes todo el pizarrón y muestres un diseño tan complejo que Kubernetes administre a Kubernetes, porque eso les da la señal que están buscando

    • Lo digo como alguien que ha hecho cientos de entrevistas de diseño de sistemas y ha entrenado a muchas personas. Las respuestas que mencionas dan una señal débil (excepto la de la cola); lo que de verdad quieren saber los entrevistadores es por qué tomaste esas decisiones, qué factores consideraste y escuchar tu proceso mental. Si no explicas tu respuesta en detalle, desde el punto de vista del entrevistador es fácil pensar “esto no me da mucha información”. Por eso el candidato tiene que comunicar activamente la información que el entrevistador quiere obtener. Incluso un buen entrevistador, si tiene que sacarte las respuestas a la fuerza, probablemente anotará algo como “el razonamiento es sólido, pero la comunicación es ineficiente”. La habilidad de comunicación también se evalúa. Y por último, no estoy de acuerdo con la respuesta de SQL/NoSQL. La experiencia del equipo importa, pero las diferencias entre tecnologías son claras y, según el caso, la diferencia de rendimiento puede ser grande. Esa respuesta deja la impresión de que te falta experiencia en una variedad de escenarios

    • Como dicen, “la entrevista es de dos vías”, y me parecen respuestas muy razonables. Si yo fuera el entrevistador, más bien te daría una calificación alta. En cambio, si una empresa te rechaza por respuestas así, probablemente la empresa sea la que no vale mucho la pena. Pero en la práctica muchas veces uno necesita conseguir un puesto rápido, así que también hace falta encontrar un equilibrio y ajustar las respuestas a lo que la otra parte quiere escuchar

    • Este consejo no es bueno. Un diseño simple y elegante no empieza por ignorar problemas potenciales. Las preguntas de seguimiento no son un momento para soltar trivia técnica, sino una señal para discutir entre ambos. Tus respuestas no muestran sabiduría; más bien dan la impresión de inmadurez. No es culpa del entrevistador

    • Coincido con el punto que marcó otro comentario sobre que “la entrevista es de dos vías”, pero un buen entrevistador diría con honestidad algo como “esta respuesta también es buena, pero en este momento estoy probando tus conocimientos en este tema”. Si la persona sigue insistiendo en irse por otro lado, eso más bien es una señal de alarma

    • Me parece un ejemplo perfecto de por qué existe el LinkedIn-driven development. En la práctica, listar un montón de tecnologías en el CV se ve mucho más impresionante que explicar que usaste bien un solo Postgres y un monolito modular

  • Me parece un artículo realmente muy bueno. Pero también quiero mencionar los límites de estas mejores prácticas. Por ejemplo, está el consejo de “no dejes que 5 servicios distintos escriban en una tabla; haz que 4 llamen a un API o emitan eventos, y que solo 1 servicio escriba en la tabla”. En la realidad, las cosas no se separan de forma tan limpia. Si los cinco acceden a la DB, ya estás construyendo un sistema distribuido, pero la DB de por sí ya ofrece permisos, transacciones y queries personalizadas, así que ni siquiera necesitas diseñar una interfaz aparte. En cambio, si construyes una interfaz de más alto nivel con un solo servicio, ahora te toca implementar por tu cuenta autenticación, transacciones y manejo de excepciones. La duda es si en la práctica eso no agrega más modos de falla y más impuesto operativo de microservicios complejos. Por otro lado, que varios servicios accedan a una misma DB puede ser en sí un code smell. Quizá esa DB sea en realidad el rastro de varias bases unidas, y tal vez esos servicios en el fondo podrían reducirse a dos o tres

    • Sobre la pregunta de “qué ganas con eso”, un API es mucho más adaptable al cambio que usar un esquema de DB compartido. Después de haber trabajado con muchos sistemas, no volvería a diseñar una arquitectura donde varios servicios compartan una misma DB. Tal vez era aceptable en una empresa pequeña a inicios de los 2000, pero desde entonces solo he visto casos de fracaso (salvo cuando dentro del mismo servicio solo están separados los caminos de lectura y escritura)

    • No estoy de acuerdo con la idea de que, como la DB ya es la interfaz, no hace falta diseño adicional. Si varios clientes usan la misma DB, los patrones de acceso difieren y los problemas de migración crecen. Al final terminas necesitando diseño extra, como vistas y control de permisos, y también aumenta la carga de mantenimiento. En una situación ideal, un API es mucho más limpio. En la realidad, por la presión de sacar funciones rápido, se termina permitiendo el acceso directo a la DB como atajo, pero en el fondo eso pasa porque mucha gente evita rediseñar todo para ajustarse a nuevos requerimientos o diseños

    • Cuando hace falta cambiar algo, la meta es minimizar el alcance de la coordinación necesaria. Si tienes que cambiar la estructura del datastore, debes controlar todas las partes que acceden a él, así que mientras menos rutas de acceso haya, más fácil es el cambio. Por ejemplo, en un trabajo real, cuando se separó una DB, más de 40 equipos tuvieron que modificar código. Y eso fue por un requerimiento de feature. Si hubiera sido por un problema de escalabilidad, el producto entero podría haberse roto

    • Dijiste que conectar varios servicios a una sola DB es un “code smell”, pero al revés, si cada servicio tiene que tener su propia DB física, la disponibilidad podría pasar de N a N a la potencia de M y en la práctica volverse más frágil (si hablamos a nivel de clústeres de DB)

  • Cuando consultas una base de datos, muchas veces lo más eficiente es consultar de verdad a la DB. Si necesitas datos de varias tablas, conviene usar joins en vez de consultar cada una desde la aplicación y combinar todo ahí. Y también recomiendo activamente usar vistas o incluso procedimientos almacenados. Las vistas son una capa de abstracción de datos, así que ayudan mucho al diseño, y el código SQL, si está bien escrito, puede ser fácil de entender y mantener

    • Justamente por esto los ORMs causan muchos problemas. Usar directamente vistas SQL o queries personalizadas en cada vista MVC de un entorno SSR es una forma eficiente y elegante de construir grandes servicios web. Hay que dejarle el trabajo pesado al RDBMS y hacer que el servidor web simplemente pase el resultado SQL a la tabla. Los RDBMS heredados como MSSQL u Oracle tienen muchísimas optimizaciones integradas. En cambio, los ORMs imponen un modelo único de objetos y casi no dejan flexibilidad

    • Los procedimientos almacenados parecen útiles, pero en la práctica, por las limitaciones del lenguaje (como T-SQL), es difícil unificar el desarrollo en un lenguaje moderno que todo el equipo conozca bien. Estoy manteniendo una base de código grande en T-SQL y ni el control de versiones ni las herramientas de diagnóstico son muy buenas; el código de los recién llegados todavía se deja leer, pero T-SQL es una pesadilla

    • Yo no estoy de acuerdo. En arquitecturas modernas orientadas a escalabilidad, es mejor hacer los joins en el backend delante de la DB. Si estructuras el sistema para que la DB haga búsquedas simples por índice y el backend haga los joins, la escalabilidad de la DB mejora y también puede ser más rápido. Es más fácil aumentar instancias de servidor que escalar la DB. Solo si el join requiere una cantidad descomunal de datos que necesariamente debe procesarse en la DB, entonces habría que cambiar la arquitectura. Si incluso puedes mover el join al frontend, también ganas en caché de resultados

    • ¿De verdad? Por ejemplo, con 10 mil clientes y 1 millón de pedidos, si haces join y envías todo junto entre una tabla de clientes con 20 campos y una de pedidos con 5 campos, estarías transfiriendo 25 millones de campos. Si en cambio traes ambas con dos queries independientes y luego haces el join, serían 5 millones de campos de pedidos más 200 mil de clientes. En ancho de banda y rendimiento, eso es mucho mejor

    • Esta regla sirve como punto de partida, pero hay que saber bien cuándo hace falta una excepción. Una app en la que trabajé tenía una estructura donde los joins hacían que los registros crecieran de forma exponencial. Entonces separamos las queries y terminó siendo mucho más rápido, porque el beneficio en procesamiento y filtrado superó por mucho el overhead de red. Más adelante incluso mejoró más cuando cambiamos a una estructura en la que todos los datos se guardaban como JSONB

  • Me parece una lástima que, hablando de buen diseño de sistemas, no se mencione en absoluto el dominio del problema. Lo más importante y difícil del diseño de sistemas es la interfaz que el sistema ofrece a los usuarios. Al final, un sistema de software es un intercambio del tipo “te doy esta funcionalidad, pero a cambio tienes que entender esta estructura o este modelo”. Los errores de diseño de interfaz son los más costosos, y si no pasas la mayor parte del tiempo hablando de la interfaz, entonces estás dejando fuera lo más importante. Los demás elementos del sistema luego se pueden corregir todo lo que haga falta sin tocar al usuario

  • Me identifiqué mucho con la frase “el buen diseño no se nota, y el mal diseño a veces se ve más convincente”. Parece que la evaluación de los técnicos se hace con base en la “complejidad”, y eso termina incentivando el sobrediseño. El principio KISS lleva demasiado tiempo sin recibir la atención suficiente

    • A veces miro partes del codebase por las que uno pasa sin pensar demasiado, y justo eso suele ser señal de que ahí hubo un buen diseño

    • Esto, por desgracia, es cierto. A la mayoría le atraen más las soluciones complejas, y si das una respuesta simple puedes parecer incompetente. Pero en la realidad, una estructura simple y fácil de mantener aporta mucho más al éxito total del proyecto. Claro que hay problemas inevitablemente complejos, pero la mayoría son solo webapps normales

  • Lo más importante en el diseño de esquemas es la flexibilidad. Cuando ya se acumuló información, cambiar el esquema se vuelve muy difícil. Pero si diseñas con demasiada flexibilidad (¡meter todo en JSON o en una estructura EAV!), el código de la aplicación se vuelve infinitamente más complejo y además aparecen problemas raros de rendimiento. Por eso normalmente prefiero esquemas que, con solo ver la estructura de tablas, te permitan intuir para qué sirven; esquemas fáciles de leer para personas. Cuando veo EAV o columnas/tablas JSON demasiado seguido, de verdad me dan ganas de dejar el desarrollo. Sin duda hay casos donde EAV tiene utilidad, pero en la mayoría de situaciones solo mete caos en el trabajo real. Problemas N+1, generación dinámica de queries, patrones donde los datos de auditoría se guardan en la misma DB y acaban absorbidos por la lógica de negocio, entornos Oracle complejos, y diseños que separan mal qué debe ir en la DB y qué en la app: cada una de esas variables le quita muchísimo a la calidad de vida del desarrollador

    • Relacionado con esto, el libro “SQL Antipatterns” de Bill Karwin explica muy bien los riesgos y límites del patrón EAV. Aun así, a veces puede usarse como medida temporal cuando cuesta definir el esquema (por ejemplo, con columnas JSONB en Postgres), pero no puede convertirse en la regla modelo. Si se puede normalizar, siempre es mejor optar por la normalización

    • Sobre eso de que “si guardas los datos de auditoría en la misma DB, al final se vuelven parte de la lógica de negocio y eso complica las cosas”, entonces me da curiosidad cuál sería la “forma correcta”. ¿Una DB separada? ¿Un almacenamiento totalmente independiente?

  • Respecto al consejo de “evita que 5 servicios escriban en la misma tabla; haz que 4 solo llamen a un API o emitan eventos, y que solo 1 escriba directo en la DB”, lo ideal es estructurarlo desde el inicio para que esos 5 servicios nunca tengan que escribir en la misma tabla. Si pasa, quizá en realidad haya mucha lógica compartida entre ellos. Entonces vale la pena preguntarse si esos 5 servicios realmente tienen que ser distintos o si no se pueden unir en uno solo. En la práctica, darles tablas separadas o resolverlo con refactorización puede arreglar mejor el problema

  • La distinción entre stateful y stateless es clave para repartir las responsabilidades entre infraestructura y desarrollo. Si corres algo stateless en contenedores, no hay mucho que pueda salir mal, así que si falla, simplemente lo vuelves a desplegar. Mientras evites errores de DB lo bastante graves como para dañar el dataset, la mayoría de las veces se puede recuperar rápido. Gente con niveles muy distintos de experiencia, tiempo y disciplina profesional puede manejar bien hasta ahí. En cambio, las áreas con estado, como bases de datos o almacenamiento de archivos, son completamente distintas. Un solo error puede poner en riesgo a todo el negocio, así que deberían estar a cargo de personal dedicado con mucha experiencia real. Una DB que funciona sin problemas pero no tiene backups ya es un riesgo enorme. En la práctica, estos son problemas que no se resuelven aunque despliegues algo en unos minutos

    • En la parte de “si es una app de contenedores stateless no hay grandes accidentes → se recupera con un despliegue”, siento que de pronto el argumento cambió a hablar de lo stateful, y no entiendo bien el flujo lógico
  • Sobre el consejo de “usar timestamp en vez de bool”, me parece una guía demasiado general. Por ejemplo, is_ontrue, on_at1023030 es claro; pero is_a_beartrue, a_bear_at12312231231 suena rarísimo. La mayoría de los osos no “se vuelven osos” en un momento específico... parece algo que solo aplica en ciertos casos

    • Yo diría que en casi todos los casos es mejor usar timestamp o integer en vez de boolean. Sobre todo porque los campos con solo dos estados muchas veces terminan evolucionando hacia una “clasificación de tipos”. Por ejemplo, incluso si solo hubiera osos, conviene anticipar una posible expansión a un tipo enum; y los campos de estado también suelen pasar de simple activo/inactivo a múltiples estados como detenido, eliminado, pausado, etc., así que los boolean se multiplican y al final complican más las cosas. Mejor integer

    • Si tomamos la afirmación literalmente, entonces usar boolean en la DB ya sería en sí un olor raro, y con eso sí estoy de acuerdo. Pero este enfoque (cambiar bool por timestamp) muchas veces funciona más como una conveniencia en joins que como una “solución completa”. Si los cambios en tiempo real importan, desde el principio lo correcto sería una tabla de auditoría. Con el soft delete me pasa igual: me parece una solución tibia. La intención real es evitar borrados, pero eso se protege mejor con backups y recuperación

    • El tipo boolean ocupa menos espacio, así que en algunas cargas de trabajo (por ejemplo, grandes volúmenes de datos analíticos) es eficiente. A veces sí tiene sentido lógico guardar un boolean. Por ejemplo, para el resultado de un proceso (marcar éxito o fracaso), un boolean es práctico

    • Me pregunto si realmente hay una razón para usar solo los boolean como timestamp. También podrías querer saber cuándo cambiaron isDarkTheme o paginationItems. Se siente más como una especie de changelog improvisado

    • En ese caso sería mejor usar un valor enum como Bear

  • Si buscas un libro desde una perspectiva más abstracta para aprender sobre buen diseño de sistemas, recomiendo muchísimo Systemantics de John Gall. Como ingeniero, de verdad vale la pena leerlo

    • Es un libro corto, pero me divertí mucho leyéndolo. También me impresionó lo inusual de su estilo de escritura