- Las propuestas de diseño de sistemas que empiezan con “let’s just” por lo general se vuelven mucho más complejas de lo esperado, y las buenas decisiones de arquitectura dependen en gran medida de reglas prácticas y de entender el contexto
- Las estructuras de plugins, agregar APIs y las capas de abstracción pueden sonar razonables, pero en la práctica también tienen que hacerse cargo de la compatibilidad de comportamiento, el mantenimiento, la seguridad, el rendimiento y las exigencias del ecosistema
- El procesamiento asíncrono, el control de acceso y la sincronización de datos parecen temas conocidos, pero en entornos de producto suelen terminar en errores difíciles de reproducir, rediseños del modelo de seguridad y problemas complejos de sincronización
- El enfoque cross-platform y las salidas nativas (
native escape) pueden servir en productos iniciales y simples, pero cuando se separan las funciones de la plataforma y el estado interno, se vuelve difícil mantener la calidad y la consistencia - Estos patrones no siempre están mal, pero la mayoría de las veces no hacen falta o tienen alternativas, y en vez de elegir enfoques propensos al fracaso conviene volver a resolver el problema desde los primeros principios (
first principles)
Por qué “let’s just” es peligroso
- Las propuestas que vienen después de “let’s just” terminan, 9 de cada 10 veces, siendo un trabajo mucho más complejo de lo que se imaginó en la sala de reuniones
- La ingeniería también tiene algo de ciencia social, así que lo que funciona depende en gran medida del contexto
- Cuando dices que cierto enfoque no funciona, para un ingeniero eso puede sentirse de inmediato como un reto para demostrar un contraejemplo
- Gran parte de la gestión de ingeniería y de la arquitectura de software es una combinación de reglas prácticas (
rule of thumb) obtenidas de la experiencia y lecciones aprendidas con mucho esfuerzo
“Hagámoslo pluginizable”
- Cuando una sola implementación parece insuficiente, meter una nueva implementación en la misma arquitectura hace pensar que se podrán obtener mejoras o funciones nuevas sin cambiar a quien llama la API
- Pero como “una API no es un archivo de encabezado ni documentación, sino el comportamiento mismo”, casi no existen plugins que simplemente funcionen
- El componente de software moderno más cercano a un plugin es un driver de dispositivo
- En el pasado, los drivers tenían una calidad de funcionamiento tan mala que dejaron de tolerarse, o los sistemas operativos modernos han cambiado hacia construir sus propios drivers
- Para crear una estructura realmente pluginizable, hay que diseñar al mismo tiempo la implementación base y una segunda implementación, porque solo así existe al menos una prueba de que funciona una vez
“Agreguemos una API”
- Muchas veces, después de que un producto o una empresa logra cierto éxito, aparece la idea de “tenemos que convertirnos en plataforma y atraer desarrolladores”, y entonces se agrega una API
- Quien ofrece una API tiene que negociar constantemente entre agregar funciones y mantener la compatibilidad e interoperabilidad, y su libertad de cambio se reduce mucho por el comportamiento y las características de rendimiento ya existentes
- Que exista una API no significa que alguien necesariamente quiera usarla
- Muchas APIs nuevas aparecen cuando el producto quiere cierta capacidad pero no le asigna una prioridad interna suficientemente alta
- El mercado objetivo puede ser pequeño, muy vertical o limitado a un dominio específico, y se espera que socios externos llenen ese hueco por medio de la API
- Pero esos socios también tienen su propio negocio y sus propios clientes, así que quizá no quieran incorporar otro producto adicional para resolver el problema
- Convertirse en plataforma es un negocio con una demanda real considerable, y rara vez basta con ofrecer unas cuantas APIs para darles a terceros una base económica sostenible
“Agreguemos otra abstracción”
- La frase de Butler Lampson, “todos los problemas de la informática pueden resolverse con otro nivel de indirección”, contiene una verdad real
- Los fracasos suelen aparecer de dos formas
- Una abstracción introducida demasiado temprano se queda en la arquitectura como sobreabstracción sin un plan real de uso
- Una abstracción agregada después puede volver muy complejos el mantenimiento, la seguridad y la optimización de rendimiento
- Windows NT tuvo muchas sobreabstracciones incluidas desde el principio que en la práctica no se usaron
- En la evolución de Mac OS hubo casos en que una abstracción que al principio parecía rara resultó útil dos versiones después, y la diferencia estaba en que sí había un plan
- Si una abstracción añadida a posteriori solo se usa en parte del código, termina habiendo mucho código que no usa esa nueva abstracción, lo que aumenta la carga de mantenimiento
“Hagámoslo asíncrono”
- Buena parte de los primeros 25 años de la informática se dedicó a entender e implementar el comportamiento asíncrono
- En los posgrados de los años 80 se pasaba mucho tiempo con temas como los filósofos comensales, productor-consumidor y el barbero dormilón
- Hoy muchos ingenieros tratan los problemas asíncronos de forma bastante abstraída gracias a las reglas de la capa de datos y a los frameworks web
- Si se administra la asincronía directamente fuera del framework o de la capa de datos, al principio puede parecer que funciona bien, pero un año después pueden aparecer errores difíciles de reproducir
- Solo queda esperar que esos errores no impliquen corrupción de datos
“Luego le agregamos control de acceso”
- La ubicación del control de acceso fue objeto de debates teóricos, pero los sistemas actuales viven en un entorno mucho más complejo porque están bajo ataque constante
- Todo el mundo sabe que la seguridad es necesaria desde el comienzo, pero por la velocidad de salida al mercado casi no existen sistemas que diseñen por completo desde el inicio el control de acceso y el modelo de seguridad
- Si no se parte de la perspectiva del cliente y del atacante, es difícil crear un diseño de control de acceso adecuado para el producto
- Agregar el control de acceso después puede fracasar, o llevar a una situación en la que más adelante haya que reescribir el producto
- Y esa reescritura no suele ser una buena experiencia para nadie, incluidos los clientes
“Sincronicemos los datos”
- En entornos con varios dispositivos, aplicaciones SaaS y almacenes de datos, la propuesta de “simplemente sincronicemos los datos” aparece con frecuencia
- Como enfatizaba Ray Ozzie, pionero de client/server y de la sincronización de datos, la sincronización es un problema difícil
- En informática, un problema difícil suele significar uno muy delicado, lleno de desafíos que solo se aprenden con experiencia
- Incluso en almacenes de datos con semántica completa y transacciones, la sincronización es difícil, y cuando entran blobs, datos no estructurados y transformaciones de datos, la dificultad crece de forma abrupta
- Basar una solución en la sincronización de datos casi nunca es lo que uno quiere, y por eso existen empresas de miles de millones de dólares dedicadas solo a resolver sincronización
“Hagámoslo cross-platform”
- El debate sobre lo cross-platform se repite desde hace mucho tiempo, y siempre hay alguien que dice que su código funciona muy bien o pone como ejemplo a Unity y los videojuegos
- Cuando dices que vas a hacer algo cross-platform, en realidad estás prometiendo algo muy parecido a construir uno de estos tres: un sistema operativo, un proveedor de nube o un navegador
- Lo cross-platform funciona bien en dos casos
- Cuando la plataforma es nueva y simple, por ejemplo cuando la nube está al nivel de cómputo y almacenamiento simple
- Cuando la aplicación o el producto es nuevo y simple
- Esas condiciones se rompen cuando empiezas a separarte de la plataforma base o a crear funciones que se expresan de forma completamente distinta en cada plataforma objetivo
- A Microsoft se le hizo difícil crear Office para Mac y Office para Windows con el mismo código, así que bifurcó el código de Office en 1998 y nunca volvió atrás
- Microsoft originalmente existía como un negocio de crear aplicaciones cross-platform, pero eso funcionaba en una época en que la documentación de APIs del sistema operativo era de unas 100 páginas y cada sistema operativo derivaba de CP/M
- Artículo relacionado: Divergent Thoughts on Cross Platform
“Si hace falta, dejemos una salida nativa”
- Como lo cross-platform solo funciona bien durante poco tiempo, los frameworks y las abstracciones de API suelen ofrecer una salida nativa (
native escape) - La idea es que, a medida que la plataforma evoluciona, se pueda invocar directamente una función de la plataforma nativa que el framework todavía no expone
- Pero el framework o la API que ofrece la abstracción mantiene estado interno o cachés
- Si se llama directamente a la plataforma nativa, se modifican estructuras de datos y estados que el framework no conoce
- Algunos frameworks ofrecen mecanismos para intercambiar datos o estado entre el código de escape y el framework, pero eso se parece más a reintroducir arquitecturas tipo
malloc/freeen la era de la administración automática de memoria
Se puede elegir, pero no debería ser la opción por defecto
- Esto no significa que siempre haya que responder “no” a estos enfoques
- En ciertos contextos, estas formas de trabajo pueden funcionar
- Pero en la mayoría de los casos, estos patrones no hacen falta o existe una mejor manera
- En vez de empezar por patrones de software con alta probabilidad de fallar, hay que resolver el problema desde los primeros principios
2 comentarios
En el caso de los plugins, lo más importante es diseñar la interfaz filtrando al máximo solo las acciones esenciales.
Si solo tomas la estructura general del código actual para crear la interfaz, inevitablemente se convierte en una interfaz innecesaria atada a esa implementación; y lamentablemente, eso sucede más de lo que debería...
Opiniones de Hacker News
El problema con ideas como esta está menos en la idea en sí y más en el enfoque tipo “solo hagámoslo” o en las expectativas que la preceden.
Por ejemplo, si se ve una API como “solo una función más” del producto, en plan “solo agreguemos una API”, creo que tendría una tasa de éxito parecida a “solo agreguemos una UI”.
Para crear una buena UI hay que ser cuidadoso y exhaustivo, y también se necesitan especialistas en esa área.
No hay razón para que otra interfaz del producto sea distinta; más que si es una mala o buena idea, lo central es que no es algo que se deba hacer “así nomás”.
La única persona que podía decir “solo” era el desarrollador realmente responsable de hacerlo funcionar; si otro desarrollador lo decía, se entendía que se estaba ofreciendo para encargarse de esa tarea.
Era una regla que nos funcionaba bien.
Hacer bien una API implica mucho diseño y complejidad.
Si no está bien diseñada, el cliente puede tener que hacer varias llamadas donde debería bastar una, o la API puede ser confusa y terminar llamándose mal o no poder usarse en absoluto.
También se necesitan autenticación y autorización, así que hay que configurar OAuth2 o, como mínimo, contar con generación, almacenamiento y validación seguros de tokens de API.
Los datos también deben manejarse de forma segura, y si el rendimiento es malo, la base de datos puede quedar aplastada por la carga.
Si se agrega caché, aparece la complejidad adicional de la invalidación de caché y de servidores o procesos para soportarla.
Sin límites de velocidad, un cliente descuidado puede martillar la API, y si la documentación es pobre, en la práctica no sirve.
Según el caso, incluso hay que ofrecer SDK para varios lenguajes; y sin buenos mensajes de error, un usuario primerizo no sabrá por qué falló su llamada.
Generalizan lo que a alguien le salió mal, pero casi nunca se aplican tal cual salvo que seas la misma persona en una situación muy parecida.
Los consejos que son consistentemente correctos, como “piensa con cuidado, intenta hacer lo correcto y revisa los resultados”, son tan generales que casi se vuelven inútiles.
Con ese tipo de frases no se venden muy bien ni entradas de blog ni libros.
Porque hay que asegurarse de que el cliente no se asuste cuando uno empieza a sacar del agua el resto del iceberg.
Así no sería un tema de posible/imposible, sino de las formas en que algo tiene éxito o fracasa.
Cuando conoces los detalles necesarios, se vuelve difícil decir “solo” sin saber siquiera qué es lo que no sabes.
Eso sí, sobre las DSL estoy casi 100% de acuerdo.
Suelen ser una complejización innecesaria y con pretensión de ser simpática, y quien esté calificado para crear una debería ser alguien que ya haya creado un lenguaje de programación exitoso y lo haya actualizado hasta una segunda edición o un segundo lenguaje incorporando sus errores, pero aun así se haya equivocado en muchas cosas.
(1) Los DSL a veces funcionan muy bien. Ver https://www.jooq.org/
(2) Elastic Load Balancer es un bucle de control que reacciona a la carga de trabajo, y ese tipo de tecnología ya está generalizada
(3) En la mayoría de las industrias, el subaprovisionamiento está muy extendido. Ver https://erikbern.com/2018/03/27/waiting-time-load-factor-and... y https://www.amazon.com/Goal-Process-Ongoing-Improvement/dp/0...
(4) La detección de anomalías no es intrínsecamente un problema de sistemas distribuidos como los otros puntos, pero alguien que ya se haya quemado fuerte puede sentir que la necesita
También es un campo intelectualmente difícil
El primer algoritmo que me pareció más o menos inteligente fue https://scikit-learn.org/1.5/modules/outlier_detection.html#..., y a veces funciona como por milagro
Cuando lo apliqué a texto con embeddings basados en CNN que usaba en 2018, encajó bien, pero con SBERT no tuve nada de suerte
Resolvieron el problema y nadie se quejó
Creo que el factor más importante fue que ambos eran pequeños
Eran muy parecidos y reutilizaban código
Uno era para escribir reglas que validaban formularios enormes, y el otro para escribir reglas de decisión según las respuestas del formulario
Un buen DSL permite hacer algo a quien antes no podía hacerlo
Un DSL creado para ahorrar tiempo tiene muchas menos probabilidades de ser útil, porque es muy posible que en realidad no ahorre tiempo
En ambos casos había que llevar al programa comportamiento complejo del dominio
Así que hay que enseñarle el dominio al programador, juntar al programador con un experto del dominio, o enseñarle programación al experto del dominio
Si hay mucho trabajo, es atractivo darle poder al experto del dominio
El programador puede hacer otras cosas y el ciclo de feedback se acorta
Si el dominio es profundo, probablemente no quieras mandar al programador a estudiar para aprenderlo; si el dominio es superficial, quizá pueda encargarse alguien más barato
Los DSL implican una gran carga cognitiva
Pero si la alternativa es aprender un lenguaje de programación completo, se vuelven más razonables
Un DSL para ahorrar tiempo es el caso en que alguien que ya sabe escribir código quiere escribir menos código, pero el ahorro es pequeño y en general no vale mucho la pena
Luego, cuando un programador quiere cambiar algo, en vez de leer código intuitivo tiene que aprender o recordar todo este DSL
Como regla práctica más simple: un DSL para programadores tiene menos probabilidades de ser una buena idea que un DSL para no programadores
Escribo consultas SQL, las pruebo en herramientas como DataGrip y luego paso horas averiguando cómo convertirlas a ese DSL
Si usas funciones SQL “exóticas”, como expresiones JSON, el problema empeora
Depurar se vuelve un flujo de “imprimir el SQL generado, copiarlo en algo como DataGrip, ajustar la consulta y luego encontrar cómo volver a encajar eso en el DSL”
Es una pérdida de tiempo enorme
El principal argumento de venta de jOOQ son las consultas type-safe, pero perdió importancia cuando IntelliJ empezó a validar el SQL en strings dentro del código contra datos reales
El flujo de editar SQL directamente y probarlo de inmediato contra la base de datos simplemente es mejor
jOOQ refuerza el argumento del post original sobre los DSL
Ejemplos populares de DSL que podrían considerarse malos o casi fracasos son HCL, E4X, XUL y el lenguaje de formateo de strings de Common Lisp
HCL es el lenguaje de configuración de Terraform, y era evidente que desde el inicio no contemplaba el problema muy común de aprovisionar equipos similares según la cantidad de variables
Los intentos posteriores de agregar esa funcionalidad también fueron torpes y no resolvieron el problema por completo
E4X era un DSL de JavaScript para trabajar con XML; en casos simples permitía expresar el trabajo con XML de forma más concisa, pero enseguida podía volverse difícil de leer, como una pared de signos de puntuación
Al igual que LINQ de Microsoft, tampoco le daba al autor ninguna pista sobre la complejidad computacional del código interno
Al final, el código que usaba este DSL a menudo se reescribía de una forma menos concisa, pero más fácil de analizar
XUL era un lenguaje de UI para extensiones del chrome del navegador Firefox, y estaba bien para crear extensiones de Firefox
Pero Firefox también quería venderlo como tecnología base para aplicaciones internas de empresas, y en ese ámbito se quedaba muy corto
Para hacer cosas simples hacía falta usar muchos trucos y rodeos
El lenguaje de formateo de strings de Common Lisp es parecido: está bien para problemas pequeños, pero no escala
Algunos problemas de formateo requieren soluciones muy raras o directamente no tienen respuesta, y detesto ver código que llama recursivamente a
formatEn general, el problema más común de este enfoque es que es una solución improvisada y no escala bien
Pronto te topas con problemas que no puedes resolver correctamente, y los programas grandes escritos en DSL suelen ser una pesadilla de manejar
Si montas un DSL sobre Lisp, solo tienes que escribir la lógica del dominio, no el lenguaje base
La mayor parte del trabajo ya está hecha, y el lenguaje es útil desde el primer día
Si lo haces como un DSL alojado sobre Lisp, puede usarse de verdad; no entiendo por qué alguien insistiría en crear un lenguaje nuevo desde cero y verlo marchitarse
Todos estos elementos tienen muchos casos de éxito
Es difícil usar la palabra “casi” como escapatoria
Esto simplemente parece pesimismo y cinismo cansado
Entiendo ese sentimiento y también lo he sentido, y a veces es difícil hacer que un ingeniero entusiasta abandone una mala idea
Pero este tono me parece dañino
Bucles de control que se disparan hacia infinito o hacia límites máximos/mínimos, cachés que no se recuperan de fallas distribuidas, estado corrompido durante migraciones en vivo, ráfagas que causan sobrecarga en los momentos menos convenientes, falsas alertas de detección de anomalías que te avisan de feriados en todo el mundo, y cosas por el estilo
Debajo de todas esas ideas hay una maraña de complejidad que casi todo el mundo subestima
Esta es una lista de ideas de sistemas que son más difíciles de lo que uno piensa al principio, y hay que abordarlas en serio, no tomarlas a la ligera
Suena vagamente parecido a “se ven bien, pero casi nunca funcionan”, pero en los detalles es completamente distinto
Si las tratas como problemas difíciles e inviertes en consecuencia, que funcionen bien es un resultado perfectamente alcanzable
Si son una función agregada a posteriori o se asume ingenuamente que son fáciles, suelen salir mal
Lo leo como algo sobre ingenieros obsesionados con la optimización prematura aunque no haya beneficio para el negocio
Es algo realmente muy extendido en la industria: diseñar y construir una nave espacial de autoescalado redundante es divertido, aunque preparar respaldos que puedan desplegarse en unas horas y sobreaprovisionar servidores al 200% podría costar menos de una décima parte
A veces estas ideas tienen sentido, pero solo después de que se vuelven necesarias
No son algo que debas diseñar por adelantado en la etapa inicial de un producto
Muy pocos productos necesitan realmente la escala, disponibilidad y complejidad que estas implementaciones buscan resolver
Parece que aquí mucha gente intenta encontrar una función de juicio sutil para separar las excepciones, pero en realidad es sencillo
Estas ideas son excelentes cuando las hago yo, y nunca funcionan como se pretendía cuando las hizo el idiota que me precedió
Y a veces ese idiota que me precedió soy yo mismo hace unos meses
Me gustaría agregar aquí el diseño guiado por el dominio
Intentar alinear la aplicación con la estructura del negocio y, con eso, fijar el diseño del negocio es una receta para el desastre
Si el negocio es pequeño o está estancado, quizá no notes el problema
Pero si el negocio tiene éxito o crece, pronto te arrepentirás de haber intentado crear dominios con nombres horriblemente descriptivos, atados a prácticas de trabajo ya obsoletas
En cambio, es mucho más flexible diseñar alrededor de capas funcionales, como se ha hecho de manera probada durante décadas, y poner la lógica de negocio tanto como sea posible en configuración, filas de base de datos y flujos de trabajo de usuario
Mencionaste como trampas del diseño guiado por el dominio el lenguaje obsoleto y la baja reutilización de código y sistemas para nuevos intentos, pero a la inversa, un diseño altamente abstracto que pone toda la lógica de negocio en configuración, flujos de trabajo, etc., solo es flexible cuando toda la organización entiende bastante bien esa abstracción, la configuración y sus innumerables combinaciones
Esa combinatoria explota rápidamente en forma de laberinto y crea comportamientos desconocidos e inesperados de los que la gente termina dependiendo
El onboarding de nuevos desarrolladores y el costo de reemplazar equipos de desarrollo también se vuelven difíciles de sostener
La organización termina hablando dos idiomas distintos
La mayoría de las solicitudes de funcionalidades que parecen simples, si rompen la abstracción, se convierten en un rediseño enorme del sistema, o bien en “hackeemos esta abstracción para que por ahora parezca un cambio más seguro y pequeño”
Lo primero siempre es muy difícil, incluso con ingenieros sobresalientes que entienden por completo el comportamiento de todo el sistema y la base de código, además de buenas prácticas y procesos de ingeniería, y puede llevar meses o años
Lo segundo ocurre con más frecuencia, y por eso los proyectos “altamente abstractos, en capas funcionales, basados en configuración y con lógica de negocio emergente” al principio parecen perfectos y flexibles, pero al final se convierten en “qué diablos es esto”
Una vez implementado el sistema, esa lógica de negocio emergente se convierte en el idioma que todos hablan
Cuando la organización habla dos o tres idiomas totalmente irreconciliables, es muy doloroso, y si no hay varias personas que puedan traducir con fluidez entre ellos hacia arriba, hacia abajo y hacia los lados, terminas sintiendo que habría sido mejor representar el dominio de forma más cercana
Si diseñas un estado de modo que sea imposible representarlo con tipos, debes poder estar seguro de que, durante la vida útil de ese diseño, ese estado realmente será un estado imposible
No entiendo bien el punto sobre los bucles de control que reaccionan a la carga
Son un componente básico y fundamental de muchísimos sistemas
Los reguladores centrífugos de las máquinas de vapor del siglo XIX o de los tocadiscos Victrola del siglo XX también son bucles de control que reaccionan a la carga
Toda la electrónica es una red de bucles de control que reaccionan a la carga, y lo mismo ocurre con las transmisiones automáticas de los autos
El uso de CPU es un ejemplo interesante
Por ejemplo, puedes ver una situación en la que un balanceador de carga entre regiones pelea contra el rechazo de carga dentro del proceso
Porque la señal del balanceador de carga no refleja el rechazo de carga, o lo refleja de forma incorrecta
Otro problema es cuando un bucle de control que intenta optimizar resultados locales del servicio perjudica el resultado global
En general, creo que es mejor tener pocos bucles de control ubicados en puntos de alto impacto
La carga de CPU tiene varios problemas, como se explica en https://arxiv.org/abs/2312.10172
Estos problemas tienen un patrón en común
Todos son preocupaciones transversales que agregan restricciones al modelo de programación de procesamiento secuencial de datos con el que los programadores están familiarizados
Cada vez que se agrega una restricción, aumenta lo que todos los desarrolladores posteriores deben tener presente mientras sigan desarrollando en ese sistema
Es fácil caer en una situación en la que el sistema queda excesivamente restringido y no se puede avanzar sin relajar algunas restricciones
Aunque no sea imposible, se vuelve más lento
Porque cada vez el desarrollador debe considerar cómo una nueva función interactúa con las API, la seguridad, la sincronización, la latencia, otras plataformas y el código nativo que ya se prometió soportar
Por eso también es posible soportar todas estas propiedades
Por ejemplo, si se toma la sincronización transparente de datos como la propuesta de valor central de la plataforma, todo el desarrollo posterior priorizará soportarla y hará evolucionar el conjunto de funciones posibles dentro de esas restricciones
Puede que ese conjunto de funciones no sea exactamente lo que el usuario quiere, pero ese es el alcance que soporta
El producto se ve atractivo para los clientes que ponen esa propiedad como el principal criterio de compra
He trabajado en proyectos que usaban varios DSL, caché P2P y paralelismo híbrido, y todos funcionaron
Además, fueron realmente divertidos de construir
Salvo una excepción, fueron buenas inversiones
La caché P2P no hacía falta, así que al final no se recuperó mucho de esa inversión
Por lo tanto, decir que estas cosas casi nunca funcionan es claramente incorrecto
Son complejas, pero esa complejidad aporta capacidades difíciles de obtener de otra manera
La lección del ejemplo de la caché P2P es asegurarse primero de si esa capacidad realmente es necesaria
Se lee un poco extraño, porque he implementado con éxito bastantes de estas ideas
“Simplemente sincronicemos los datos” es, para mí, la razón por la que existen los días difíciles
He visto demasiados sistemas a los que se les agregaron colas, procesamiento de eventos y demás pensando en algo como “escala de Internet”, cuando su alcance natural real está muy por debajo de ese umbral
Estos equipos son ingenuos o, en el peor de los casos, se aprovechan de directivos que no entienden de ingeniería para conseguir dinero y jugar con estos problemas por diversión
Para hacerlo bien, las colas y el procesamiento de eventos son indispensables