Volver a Desarrollador
Diseño de Sistemas y Arquitectura
Principios de diseño, patrones y arquitectura — SOLID, DDD, CQRS, arquitectura limpia y microservicios.
¿Qué es un microservicio y qué ventajas y desafíos tiene frente a una arquitectura monolítica?
Un microservicio es una aplicación pequeña e independiente responsable de una funcionalidad de negocio específica. Ventajas: el desacoplamiento permite desarrollo, despliegue y escalado independientes; el aislamiento de fallos evita que un error afecte a todo el sistema. Desafíos: mayor complejidad operativa (comunicación entre servicios, tracing distribuido, versionado), sobrecarga de red, y testing de integración y depuración más difíciles.
¿Qué es REST y qué buenas prácticas sigues al diseñar una API REST?
REST (Representational State Transfer) es un estilo de arquitectura que usa HTTP para la comunicación cliente-servidor. Cada recurso se identifica con una URL y se manipula con métodos HTTP estándar (GET, POST, PUT, DELETE). Buenas prácticas incluyen: comunicación stateless, nombres de recursos claros, uso adecuado de códigos de estado HTTP (200, 404, 500), separación de capas y documentación con OpenAPI/Swagger.
¿Cómo asegurarías que un microservicio sea escalable y tolerante a fallos?
Estrategias clave: 1) Manejo de errores con retries, backoff y circuit breakers (ej. Resilience4j). 2) Escalabilidad horizontal diseñando servicios stateless detrás de un load balancer. 3) Observabilidad mediante logging estructurado, métricas (Micrometer/Prometheus) y tracing distribuido (Zipkin/OpenTelemetry). 4) Testing con pruebas unitarias e integración, y documentación (Swagger/OpenAPI). 5) Despliegue en contenedores con Docker y Kubernetes para auto-scaling.
¿Cuál es la diferencia entre GET, POST, PUT y DELETE en HTTP? ¿Cuáles son idempotentes?
GET obtiene un recurso sin efectos secundarios — idempotente. POST crea un nuevo recurso o envía datos — no idempotente (dos POST iguales pueden crear dos recursos). PUT reemplaza o actualiza un recurso completo — idempotente (la misma solicitud produce el mismo resultado). DELETE elimina un recurso — idempotente (borrar algo que ya no existe no cambia el estado). La idempotencia es importante para reintentos seguros en sistemas distribuidos.
¿Cómo desplegarías microservicios en la nube usando AWS?
Para despliegue en AWS: usar ECS/EKS o Elastic Beanstalk para orquestación de contenedores, Lambda para funciones serverless, S3/EBS para almacenamiento, RDS para bases relacionales y DynamoDB para NoSQL, ElastiCache (Redis/Memcached) para caching, CloudFront como CDN, CloudWatch/CloudTrail para monitoreo, SQS/SNS para mensajería asíncrona e IAM para seguridad. Pipelines CI/CD con AWS CodePipeline, GitHub Actions o Jenkins aseguran despliegues automatizados y reproducibles en ambientes dev, staging y producción.
¿Qué es el patrón Saga en microservicios?
El patrón Saga coordina una serie de transacciones locales en múltiples microservicios para mantener la consistencia de datos sin depender de una transacción distribuida (2PC). Cada servicio ejecuta su transacción local y publica un evento; si un paso falla, se disparan transacciones compensatorias para revertir los pasos anteriores. Este enfoque preserva la autonomía de los servicios mientras maneja procesos de negocio cross-service de forma confiable.
¿Cómo diseñarías un endpoint REST que recibe una matriz de caracteres y una lista de palabras y devuelve las palabras encontradas?
Define un endpoint POST (p. ej. POST /api/wordfinder) que acepte un cuerpo JSON con dos campos: la matriz (array de cadenas) y la lista de palabras (array de cadenas). La respuesta devuelve las palabras encontradas como un array JSON. Se prefiere POST sobre GET porque el payload puede ser grande y codificar una matriz en query-string es impráctico.
¿Cómo devuelves las N palabras encontradas con mayor frecuencia en el resultado de una búsqueda en matriz?
Guarda el recuento de apariciones en la matriz para cada palabra encontrada en un diccionario/mapa, luego ordena las entradas por recuento descendente y toma las primeras N. Si N es fijo (p. ej. 10), una ordenación parcial o un min-heap de tamaño N es más eficiente que una ordenación completa para conjuntos de resultados grandes.
¿Qué consideraciones de rendimiento aplican al buscar una lista grande de palabras en una matriz de 64×64 caracteres?
Una búsqueda por fuerza bruta es O(F × C × L × |palabras|). Preconstruir un Trie con la lista de palabras reduce la coincidencia a un único recorrido de la matriz: cada carácter se verifica contra el nodo del Trie, dando O(F × C × L) en total. Para la restricción de 64×64, incluso la fuerza bruta es aceptable, pero el enfoque con Trie escala mejor conforme crece la lista de palabras.
¿Cómo diseñarías un sistema de despacho de notificaciones para que agregar un nuevo canal (p. ej., WhatsApp) no implique modificar el código existente?
Define una interfaz común (p. ej.,
NotificationChannel) con un método send(notification) e impleméntala por separado para cada canal (Email, SMS, Push). Un despachador selecciona la implementación correcta en tiempo de ejecución según el campo canal de la notificación. Esto respeta el Principio Abierto-Cerrado: el sistema está abierto para extensión (nueva clase de canal) pero cerrado para modificación (no se toca el despachador ni los canales existentes).¿Cómo garantizas que un usuario solo pueda leer o modificar sus propias notificaciones?
Extrae el ID del usuario autenticado de los claims del JWT dentro del middleware de autenticación y luego aplica un filtro
WHERE user_id = :currentUserId en todas las consultas a la base de datos. Nunca te bases en un userId suministrado por el usuario en el cuerpo de la solicitud o en el path para las verificaciones de propiedad, ya que el cliente puede manipularlo.¿Por qué es importante disparar el envío de la notificación inmediatamente al crearla en lugar de como un paso manual separado?
Disparar el envío al crear mantiene la operación atómica desde la perspectiva del usuario: una notificación que existe siempre ha sido despachada. También simplifica la superficie de la API (sin endpoint /send separado) y reduce el riesgo de notificaciones huérfanas creadas pero nunca enviadas. Si el despacho puede fallar, debe manejarse mediante reintentos o una cola de trabajos en segundo plano en lugar de requerir orquestación del lado del cliente.
¿Por qué un caché en memoria (por ejemplo, Caffeine) es insuficiente en un despliegue multi-réplica y cuál es la alternativa recomendada?
Cada réplica mantiene su propio caché independiente, por lo que un valor cacheado en la réplica A es invisible para la réplica B, generando comportamiento inconsistente y llamadas externas redundantes. La alternativa recomendada es un caché distribuido como Redis, donde todas las réplicas leen y escriben en un almacén compartido. Spring Boot se integra con Redis a través de spring-boot-starter-data-redis y puede configurarse transparentemente como backend del CacheManager.
¿Cómo se estructura un docker-compose.yml para ejecutar una aplicación Spring Boot junto a una base de datos PostgreSQL?
Se definen dos servicios: uno para la app Spring Boot (construida desde un Dockerfile o descargada de Docker Hub) y otro usando la imagen oficial de postgres. Se usa un bloque environment para pasar las credenciales de la DB y una cláusula depends_on para que la app espere al servicio de base de datos. Se expone el puerto de la app (por ejemplo, 8080) y se monta un volumen nombrado para la persistencia de datos de PostgreSQL. Un healthcheck en el servicio de postgres asegura disponibilidad antes de que la app arranque.
¿Cómo debe diseñarse el registro de auditoría asíncrono para que los fallos en el registro nunca afecten la respuesta principal de la API?
El manejador principal de la solicitud persiste su resultado y luego publica un evento o llama a un método @Async para escribir el registro de auditoría. El manejador async envuelve toda la lógica de persistencia en un try-catch y descarta silenciosamente los fallos (o los envía a una cola de dead-letter para inspección posterior). Como el hilo principal nunca espera el resultado async, cualquier fallo de registro está completamente aislado del consumidor de la API.
¿Cómo funciona Webpack Module Federation con Next.js SSR y cuál es el principal riesgo?
El Module Federation estándar es solo del lado del cliente—los módulos remotos se cargan en tiempo de ejecución en el navegador. Cuando Next.js intenta server-renderizar una página que importa un componente remoto, falla porque el bundle remoto no está disponible en el servidor. La solución es usar @module-federation/nextjs-mf (node-federation) o envolver los componentes remotos en dynamic import con ssr: false. El principal riesgo es un error de hidratación: si el servidor renderiza un fallback pero el cliente carga el componente remoto real, React lanza un error de hidratación.
¿Qué estrategia usarías para migrar un frontend monolítico legacy a microfrontends?
Usar el Strangler Fig Pattern—evitar una reescritura total. Primero configurar una nueva aplicación shell (p. ej., Next.js). Luego enrutar el tráfico legacy mediante rewrites o un proxy inverso para que las URLs existentes sigan funcionando. A continuación, elegir un vertical de bajo riesgo (p. ej., Configuración) y reconstruirlo como microfrontend expuesto vía Module Federation. Finalmente, migrar las rutas de una en una hasta que la aplicación legacy pueda ser desmantelada. Esto mantiene el sistema antiguo en producción hasta que cada pieza sea reemplazada y verificada.
¿Qué es el problema N+1 en GraphQL y cómo se soluciona?
El problema N+1 ocurre cuando resolver una lista de N elementos provoca N consultas adicionales—una por elemento. Por ejemplo, obtener 10 usuarios más la dirección de cada uno dispara 1 + 10 = 11 consultas en lugar de 1. La solución estándar es DataLoader, que agrupa todas las sub-peticiones en una sola consulta dentro de un tick del event loop. Para el caché, como todas las operaciones GraphQL llegan a un único endpoint POST el caché en CDN es limitado; usar caché normalizado del lado del cliente (Apollo Client) o Persisted Queries para que las CDNs puedan cachear las consultas hasheadas como GETs.
¿Qué diferencia hay entre una base de datos relacional y una NoSQL? ¿Cuándo usarías cada una?
Las bases relacionales (SQL) almacenan datos en tablas con filas/columnas, establecen relaciones con claves primarias/foráneas, usan SQL para consultas complejas y garantizan transacciones ACID. Las NoSQL almacenan datos como documentos, pares clave-valor, columnas o grafos; ofrecen esquemas flexibles y están optimizadas para escalabilidad horizontal y lectura/escritura rápida. Usar relacional cuando se necesita consistencia fuerte y relaciones claras (ej. sistema bancario). Usar NoSQL cuando se necesita velocidad, flexibilidad y alta escalabilidad (ej. logs, catálogos, sesiones).
¿Qué es una transacción en base de datos y qué significa ACID?
Una transacción es un conjunto de operaciones que se ejecutan como una sola unidad de trabajo — si alguna falla, toda la transacción se revierte para mantener la integridad de los datos. ACID significa: Atomicidad (se ejecutan todas o ninguna), Consistencia (la base pasa de un estado válido a otro), Aislamiento (las transacciones concurrentes no se afectan entre sí) y Durabilidad (una vez confirmada, la transacción persiste aunque el sistema falle).
¿Qué establece el Principio Abierto-Cerrado y cómo se aplica en un sistema de notificaciones multicanal?
El Principio Abierto-Cerrado (OCP) establece que una entidad de software debe estar abierta para la extensión pero cerrada para la modificación. En un sistema de notificaciones, significa que cada nuevo canal se entrega como una nueva clase que implementa una interfaz compartida, mientras que la lógica central de despacho nunca necesita cambiar. Violar el OCP resultaría en un switch/case creciente en el despachador cada vez que se agrega un canal.
¿Cómo modelarías el esquema de base de datos relacional para un sistema de notificaciones con propiedad por usuario?
Dos tablas principales son suficientes:
users (id, email, password_hash, created_at) y notifications (id, user_id FK → users.id, title, content, channel, created_at, updated_at). La columna channel puede ser un enum de texto. Un índice en notifications.user_id acelera la consulta habitual de obtener todas las notificaciones de un usuario.¿Qué sabes de pruebas unitarias en Java? ¿Cómo diseñarías un test para un componente de servicio?
Las pruebas unitarias verifican el comportamiento de pequeñas unidades de código (métodos/clases) de forma aislada. En Java, JUnit provee la estructura de tests y Mockito crea mocks/stubs que simulan dependencias externas como repositorios. Para testear un service, se mockea el repositorio, se define el comportamiento esperado, se invoca el método del servicio y se verifican los resultados. Los tests deben cubrir casos positivos, negativos y de borde para asegurar correctitud y permitir refactoring seguro.
¿Qué códigos de estado HTTP debería devolver una API de búsqueda de palabras en diferentes escenarios de error?
Devuelve 200 OK con resultados en caso de éxito, 400 Bad Request cuando la matriz supera el límite de 64×64 o la entrada está mal formada, y 500 Internal Server Error para fallos inesperados del backend. Mensajes de error claros en el cuerpo de la respuesta ayudan al frontend a mostrar información útil al usuario.
¿Cómo funciona la autenticación basada en token en una API RESTful?
Al iniciar sesión, el servidor valida las credenciales y devuelve un JWT firmado. El cliente almacena este token y lo adjunta a las solicitudes posteriores mediante el encabezado
Authorization: Bearer <token>. El servidor verifica la firma y extrae la identidad del usuario a partir de los claims del token sin mantener estado de sesión en el servidor.¿Qué métodos HTTP y patrones de URL usarías para una API RESTful CRUD de notificaciones?
Convenciones REST estándar:
POST /notifications (crear), GET /notifications (listar las propias), GET /notifications/:id (leer una), PUT /notifications/:id o PATCH /notifications/:id (actualizar), DELETE /notifications/:id (eliminar). Todos los endpoints deben estar protegidos por un middleware de autenticación que valide el JWT antes de que se ejecute el handler de la ruta.¿Cuál es la forma correcta de almacenar contraseñas de usuarios en una base de datos?
Nunca almacenes contraseñas en texto plano. Usa un algoritmo de hash lento y salteado como bcrypt, Argon2 o scrypt. Estos algoritmos son deliberadamente costosos en cómputo, lo que hace impracticables los ataques de fuerza bruta o de diccionario incluso si la base de datos es comprometida. La sal se almacena junto al hash para que cada hash sea único aunque las contraseñas sean idénticas.
¿Qué código de estado HTTP debe retornar una API REST cuando se excede el límite de tasa, y qué debe contener la respuesta?
La API debe retornar 429 Too Many Requests. El cuerpo de la respuesta debe incluir un mensaje legible que explique el límite (por ejemplo, 'Se permite un máximo de 3 requests por minuto'). Opcionalmente, un header Retry-After puede indicar cuándo el cliente puede reintentar. Esto sigue el RFC 6585 y ayuda a los clientes a implementar estrategias de back-off adecuadas.
¿Cuál es el propósito de un refresh token?
Un refresh token permite a una aplicación obtener un nuevo access token cuando el actual expira, sin que el usuario tenga que iniciar sesión de nuevo. Los access tokens son de vida corta (minutos) para limitar el daño si son robados; el refresh token, de vida más larga, se almacena de forma más segura (p. ej., en una cookie HttpOnly) y se intercambia en el servidor por un nuevo access token.
¿Cuáles son los cuatro síntomas de degradación del diseño de software descritos en la guía y qué significa cada uno?
Los cuatro síntomas son: **Rigidez** (el software se vuelve difícil de cambiar incluso en tareas sencillas, con estimaciones cada vez más abultadas); **Fragilidad** (los cambios provocan roturas en múltiples partes no relacionadas del código); **Inmovilidad** (resulta prácticamente imposible reutilizar código de otros proyectos o partes del mismo proyecto debido a la gran carga de dependencias); y **Viscosidad** (es más sencillo hacer las cosas mal que por el camino correcto, y el entorno de desarrollo es lento e ineficiente).
¿Qué es el Principio de Responsabilidad Única (SRP) y cómo se relaciona con la cohesión y el acoplamiento?
El SRP establece que un módulo de software debe tener una y solo una razón para cambiar, siendo esa razón su responsabilidad. Está estrechamente relacionado con la cohesión y el acoplamiento: se busca aumentar la cohesión entre las cosas que cambian por las mismas razones y disminuir el acoplamiento entre las que cambian por razones diferentes. Cuando una clase tiene más de una responsabilidad, los cambios en una preocupación pueden afectar inadvertidamente a otra, dificultando la lectura, las pruebas y el mantenimiento del código.
¿Qué significa el Principio Abierto/Cerrado (OCP) y cómo se implementa habitualmente?
El OCP establece que los módulos de software deben ser abiertos para su extensión pero cerrados para su modificación. 'Abierto para la extensión' significa que se puede añadir nuevo comportamiento a medida que cambian los requisitos; 'cerrado para la modificación' significa que añadir ese nuevo comportamiento no debe requerir alterar el código fuente existente del módulo. En la práctica, el OCP se implementa mediante polimorfismo, usando interfaces o clases abstractas para que el nuevo comportamiento se introduzca añadiendo código nuevo en lugar de cambiar el antiguo.
¿Qué exige el Principio de Sustitución de Liskov (LSP) y por qué advierte sobre mapear automáticamente el mundo real en un modelo OO?
El LSP exige que los objetos de un programa sean reemplazables por instancias de sus subtipos sin alterar el correcto funcionamiento del programa. En la práctica, cualquier subclase debe respetar el contrato de comportamiento de su clase padre. El LSP advierte sobre mapear automáticamente el mundo real porque no existe una equivalencia unívoca entre ambos modelos; lo que parece una relación 'es-un' válida en el mundo real puede violar el contrato de comportamiento en el código.
¿Qué es el Principio de Inversión de Dependencias (DIP) y cuáles son sus dos reglas clave?
El DIP establece que las entidades de software deben depender de abstracciones, no de implementaciones concretas. Sus dos reglas clave son: (1) los módulos de alto nivel no deben depender de los de bajo nivel: ambos deben depender de abstracciones; y (2) las abstracciones deben definirse en función de las necesidades del consumidor/cliente, no de las capacidades de la implementación, de lo contrario la abstracción estará acoplada a la implementación y perderá flexibilidad. Esto permite reemplazar componentes sin afectar a los consumidores y facilita las pruebas mediante objetos mock.
¿Qué es el principio DRY (Don't Repeat Yourself) y por qué se aplica a la lógica y no solo al código?
DRY establece que cada pieza de funcionalidad debe tener una representación única, no ambigua y representativa dentro del sistema. De forma importante, DRY se aplica a la lógica (la función lógica) y no meramente a la sintaxis del código: tres métodos con distinto código pero el mismo propósito lógico (por ejemplo, todos abren una conexión a la base de datos) violan DRY. Cuando DRY se aplica eficientemente, un cambio en cualquier parte del proceso requiere cambios en un único lugar, reduciendo el riesgo de inconsistencias, disminuyendo el tamaño del código y ahorrando tiempo mediante la reutilización.
¿Qué es la Inversión de Control (IoC) y qué patrones de diseño son implementaciones de este principio?
IoC es un principio del diseño orientado a objetos en el que el control sobre diferentes tipos de flujo del programa (incluida la creación de objetos y la vinculación de dependencias) se delega en un tercero, logrando un bajo acoplamiento. Aumenta la modularidad y produce clases testeables, mantenibles y extensibles. Los patrones de diseño que implementan IoC son: Service Locator, Dependency Injection, Template Method, Strategy, Abstract Factory y Observer. El principio también se conoce como el 'Principio de Hollywood': 'No nos llame, nosotros lo llamamos'.
¿Qué es la Ley de Demeter (LoD) y qué tipo de código pretende evitar?
La Ley de Demeter (también conocida como Principio de Mínimo Conocimiento o 'No hables con extraños') establece que un método de un objeto solo debe interactuar con: (1) métodos del propio objeto, (2) sus argumentos, (3) cualquier objeto creado dentro del método, y (4) propiedades/campos directos del propio objeto. Pretende evitar cadenas de llamadas profundas como
object.getX().getY().getZ().doSomething(), que crean un fuerte acoplamiento a la estructura interna de las clases involucradas. Aplicar LoD reduce el acoplamiento, mejora la reutilización y facilita las pruebas del código.¿Qué es el principio de 'Composición sobre herencia' y cuándo se prefiere?
Este principio establece que las clases deben lograr comportamiento polimórfico y reutilización del código mediante composición (conteniendo instancias de otras clases que implementan la funcionalidad deseada) en lugar de a través de la herencia, siempre que sea posible. Con la herencia estructuramos las clases en función de lo que *son*; con composición, en función de lo que *hacen*. Se prefiere la composición porque la herencia crea jerarquías rígidas y estrechamente acopladas desde etapas muy tempranas del proyecto, dificultando los cambios futuros. La composición se usa cuando se verifica la relación TIENE-UN, mientras que la herencia solo es adecuada cuando realmente se cumple ES-UN y la jerarquía es simple.
¿Cuáles son las cuatro reglas del diseño simple de Kent Beck y en qué orden de importancia se presentan?
Las cuatro reglas de Kent Beck, ordenadas por relevancia, son: (1) **Los tests pasan** — cada funcionalidad debe funcionar como se espera y estar verificada por tests; (2) **Expresan intención** — el código es autoexplicativo, fácil de entender y comunica su propósito; (3) **No hay duplicidades (DRY)** — la duplicación lógica debe minimizarse para evitar la fragilidad; (4) **Mínimo número de elementos** — el número de componentes, clases y métodos debe reducirse a lo imprescindible, eliminando complejidad innecesaria. Nota: existe debate sobre si las reglas 2 y 3 deben tener igual importancia, y la regla 4 a menudo se considera consecuencia de aplicar continuamente las reglas 2 y 3.
¿Qué es la Regla del Boy Scout en el desarrollo de software y qué mentalidad promueve?
La Regla del Boy Scout, tomada del lema de los scouts de dejar el campamento más limpio de lo que se encontró, establece que cuando un desarrollador vea código que se puede mejorar, debe mejorarlo independientemente de quién lo haya escrito. El objetivo es evitar la degradación del código con el tiempo realizando mejoras pequeñas, seguras e incrementales que ayuden al siguiente desarrollador. Promueve una mentalidad de equipo por encima del individualismo: la calidad general del proyecto importa más que la finalización de la tarea individual. Aplicar esta regla requiere un sólido conocimiento de los principios SOLID.
¿Qué es el principio del Último Momento Responsable y por qué recomienda diferir las decisiones de diseño?
El principio del Último Momento Responsable recomienda diferir las decisiones de diseño — especialmente las irreversibles — hasta el último momento posible: aquel en el que NO tomar la decisión supondría un coste mayor que tomarla. La razón es que cuanto más tiempo se mantenga abierta una decisión, más información se acumula para elegir la opción más adecuada. En el desarrollo de software es habitual comenzar a construir funcionalidades antes de que los requisitos estén completamente definidos, por lo que las decisiones prematuras e irreversibles basadas en información incompleta son un riesgo significativo.
¿Qué es el principio 'Encapsula lo que varía' y qué patrones de diseño se basan en él?
'Encapsula lo que varía' establece que cuando se identifican partes de la aplicación que pueden cambiar, deben aislarse y encapsularse en abstracciones para que los cambios no afecten a otras partes. Se apoya en SRP y OCP. Los beneficios son dobles: las variaciones en los requisitos afectan únicamente al módulo encapsulado (reduciendo la fragilidad y aumentando la reutilización), y los nuevos requisitos se satisfacen añadiendo nuevos elementos en lugar de modificar los existentes (reduciendo la rigidez). Muchos patrones de diseño se basan en este principio, entre ellos Abstract Factory, Factory Method, Adapter, Bridge, Decorator, Iterator, Observer, State, Strategy, Template Method y Visitor.
¿Qué es un Value Object en el contexto de Domain-Driven Design y en qué se diferencia de una Entidad?
Un Value Object es un tipo inmutable identificado únicamente por los valores de sus propiedades; dos Value Objects son iguales si todas sus propiedades coinciden. Una Entidad, en cambio, posee una identidad única (un identificador), por lo que dos instancias de Entidad se consideran diferentes aunque tengan las mismas propiedades.
¿Qué es el patrón Shared Kernel en Domain-Driven Design y cuáles son sus restricciones clave?
El Shared Kernel es un subconjunto del modelo de dominio (junto con su código y diseño de base de datos asociado) que dos equipos acuerdan compartir para reducir la duplicidad y simplificar la integración. Este subconjunto es especial: no puede cambiarse libremente y no debe modificarse sin consultar al otro equipo. Cuando se realizan cambios, todos los tests de ambos equipos deben pasar antes de aceptar el cambio.
¿Qué es el patrón Customer/Supplier en DDD y qué desafíos organizativos puede generar?
Customer/Supplier describe una relación entre dos bounded contexts donde el componente descendente (cliente) consume la salida del componente ascendente (proveedor), con todas las dependencias fluyendo en una sola dirección. Los problemas surgen cuando el equipo proveedor teme romper el sistema cliente, limitando su libertad de evolución, o cuando el cliente queda indefenso ante los cambios del proveedor. Estos problemas se resuelven mejor formalizando la relación mediante una API documentada, un calendario de cambios y planificación conjunta.
¿Qué es el patrón Capa de Anticorrupción (ACL) y cuándo debe utilizarse?
La Capa de Anticorrupción es una capa de aislamiento ubicada entre un sistema nuevo y un sistema heredado o externo mal diseñado. Traduce las solicitudes en ambas direcciones entre los dos modelos de dominio sin requerir modificaciones significativas en el sistema externo. Se utiliza para asegurar que el diseño de la aplicación no quede limitado por dependencias de subsistemas externos, y fue descrito por primera vez por Eric Evans en su libro 'Domain-Driven Design'.
¿Cómo se organiza internamente una Capa de Anticorrupción?
Una ACL se organiza típicamente con tres elementos complementarios: una Fachada, que proporciona una interfaz simplificada y especializada al sistema externo sin cambiar su modelo; un Adaptador, que envuelve la fachada y traduce las llamadas en solicitudes semánticamente equivalentes que el sistema externo entiende; y un Traductor, un objeto ligero sin estado responsable de convertir objetos o datos conceptuales entre los dos modelos. Juntos, estos elementos gestionan la traducción completa entre bounded contexts.
¿Qué es CQRS (Command-Query Responsibility Segregation) y qué problema resuelve?
CQRS es un patrón arquitectónico que separa las operaciones de lectura (Queries) de las operaciones de escritura (Commands) en dos modelos independientes. El lado de escritura gestiona los cambios de estado y puede incluir lógica de validación de negocio, mientras que el lado de lectura devuelve datos sin modificar el estado y puede optimizar su representación para la interfaz de usuario. Esta separación permite escalar, asegurar y evolucionar cada parte de forma independiente, aunque añade complejidad adicional al sistema.
¿En qué escenarios es más beneficioso el uso de CQRS?
CQRS es más beneficioso cuando muchos usuarios acceden a los mismos datos y cada uno necesita realizar procesamiento en varios pasos; cuando existe una clara asimetría entre el volumen de operaciones de lectura y escritura, permitiendo su escalado independiente; y cuando se desea que la UI y las reglas de negocio evolucionen de forma independiente. Generalmente no se recomienda para sistemas simples, ya que requiere el doble de esfuerzo de mantenimiento para ambos modelos.
¿Qué es la Regla de Dependencia en Clean Architecture y por qué es importante?
La Regla de Dependencia establece que una capa interior debe estar completamente aislada de las capas exteriores; las capas internas no pueden depender de las externas, pero las externas sí pueden conocer los detalles de las internas. Esta regla garantiza que, a medida que el proyecto crece, se puede añadir nuevo código sin romper la lógica interna existente, y que las reglas de negocio son independientes de frameworks, bases de datos y otros detalles de infraestructura.
¿Cuáles son las tres capas principales en una Clean Architecture basada en DDD y cuál es la responsabilidad de cada una?
La capa de Dominio es el núcleo de la aplicación y contiene entidades, value objects, aggregates y servicios de dominio que codifican la lógica de negocio principal. La capa de Aplicación coordina los objetos de dominio para satisfacer las peticiones del usuario; no contiene lógica de negocio propia, sino que orquesta los casos de uso mediante servicios de aplicación. La capa de Infraestructura proporciona capacidades técnicas (p.ej., persistencia en base de datos, mensajería) a las capas superiores y debe estar completamente desacoplada de la capa de dominio para que cambiar el motor de persistencia no afecte al resto del sistema.
¿Qué es la Arquitectura Hexagonal (Puertos y Adaptadores) y cuál es el rol de un Puerto frente a un Adaptador?
La Arquitectura Hexagonal, introducida por Alistair Cockburn, organiza una aplicación de modo que todas las entradas y salidas pasen por puntos de conexión bien definidos que aíslan la lógica de negocio de las herramientas externas. Un Puerto es una interfaz que especifica cómo una herramienta externa puede usar la lógica de negocio o cómo ésta utiliza dicha herramienta. Un Adaptador es una clase que implementa o envuelve un puerto, transformando una interfaz en otra para que una herramienta externa (p.ej., un servidor web, una base de datos) pueda comunicarse con el núcleo de la aplicación.
¿Cuál es la diferencia entre Driving Adapters y Driven Adapters en la Arquitectura Hexagonal?
Los Driving Adapters (Adaptadores Primarios) inician acciones en la aplicación, por ejemplo, un controlador web que recibe una petición HTTP e invoca un caso de uso de la aplicación. Los Driven Adapters (Adaptadores Secundarios) reaccionan ante las instrucciones de la lógica de negocio y la conectan con herramientas de back-end como bases de datos o colas de mensajes, por ejemplo, una implementación de repositorio que persiste datos en MySQL. Se hace uso de Inversión de Control en todo momento: la lógica de negocio depende únicamente de interfaces de puerto, nunca de implementaciones concretas de adaptadores.
¿Cómo funciona CQRS con un Bus de Comandos y en qué se diferencia de CQRS sin Bus?
Sin Bus, el controlador tiene una dependencia directa con un objeto Command o Query, que contiene y ejecuta la lógica del caso de uso. Con Bus, el controlador depende del Bus y despacha un Command o Query (que actúa sólo como portador de datos); el Bus enruta el mensaje al Command Handler adecuado, que contiene la lógica real del caso de uso. El uso de un Bus desacopla la solicitud de su ejecución, mejorando la extensibilidad y permitiendo que preocupaciones transversales como logging o transacciones sean gestionadas por el Bus.
¿Qué estrategia de testing recomienda Clean Architecture y por qué?
Clean Architecture recomienda que la mayoría de los tests sean unitarios, ya que cubren el mayor rango de caminos de lógica de negocio sin necesitar ningún framework, base de datos ni herramienta de infraestructura. También son necesarios tests de integración para verificar que las implementaciones de infraestructura funcionan correctamente en conjunto, pero no necesitan re-testear todos los caminos de la lógica de negocio. Los tests end-to-end (aceptación, UI, API) son los más fiables pero también los más costosos y frágiles, por lo que deben mantenerse en un porcentaje razonable.
¿Qué es la Arquitectura Orientada a Eventos (EDA) y cuáles son sus tres componentes clave?
La Arquitectura Orientada a Eventos es un patrón que promueve la comunicación asíncrona entre componentes independientes mediante eventos, y es común en aplicaciones de microservicios. Sus tres componentes clave son: los Publishers, que emiten eventos cuando se produce un cambio de estado; el Message Broker (router), que filtra y enruta los eventos a los consumidores interesados; y los Consumers, que se suscriben a tipos concretos de eventos y los procesan. Publishers y consumidores están completamente desacoplados, lo que permite escalar, actualizar e implementar cada uno de forma independiente.
¿Cuáles son los principales beneficios de la Arquitectura Orientada a Eventos?
Los beneficios clave incluyen: escalado y gestión de errores independientes, ya que los servicios sólo interactúan con el message broker y no se conocen entre ellos; desarrollo ágil, porque el broker gestiona automáticamente el filtrado y el enrutamiento sin código de sondeo personalizado; reducción de costes mediante un modelo push que elimina el sondeo continuo; implementación más sencilla de patrones de control de flujo como el backpressure; y sin penalización para consumidores lentos, ya que cada consumidor procesa los eventos de forma independiente sin bloquear a los más rápidos.
¿Cómo debe gestionarse la comunicación entre componentes en una arquitectura Clean/Hexagonal para mantener el desacoplamiento?
Cuando un componente necesita funcionalidad que pertenece a otro, una llamada directa a un método crearía un acoplamiento fuerte. En su lugar, la comunicación debe ser mediada por un mecanismo como un Event Dispatcher que enrute eventos entre componentes, o como mínimo a través de una API pública bien definida expuesta por el componente destino. Esto mantiene los componentes independientes y permite que cada bounded context evolucione sin romper a los demás.
¿Qué es Event Sourcing y en qué se diferencia del almacenamiento de estado tradicional?
En Event Sourcing, en lugar de almacenar el estado actual de una entidad, la aplicación almacena una secuencia ordenada de eventos de cambio de estado del dominio. El estado actual se reconstruye reproduciendo esa secuencia de eventos. Esto hace que guardar el estado sea siempre atómico (un evento = una operación) y proporciona un registro de auditoría fiable y la posibilidad de consultar el estado en cualquier momento. Una limitación clave es que para consultar por campos arbitrarios es necesario construir vistas materializadas.
En una arquitectura de microservicios orientada a eventos, ¿qué garantía de consistencia ofrecen las transacciones entre servicios y en qué se diferencia de ACID?
Las transacciones entre servicios implementadas a través de un message broker ofrecen consistencia eventual (garantías BASE), no ACID. Cada servicio actualiza atómicamente su propia base de datos y publica un evento, pero el sistema en su conjunto puede estar temporalmente inconsistente hasta que todos los servicios downstream hayan procesado esos eventos.
¿Cuáles son los principales inconvenientes de una arquitectura orientada a eventos en microservicios?
El modelo de programación es más complejo y requiere un aprendizaje específico. Las aplicaciones deben implementar mecanismos de compensación para recuperarse de errores a nivel de aplicación, gestionar datos temporalmente inconsistentes y los suscriptores deben detectar e ignorar eventos duplicados.
¿Qué es el problema de la actualización atómica en microservicios orientados a eventos y por qué es crítico?
Cuando un servicio debe actualizar su base de datos y publicar un evento, ambas operaciones deben realizarse de forma atómica. Si el servicio falla tras actualizar la base de datos pero antes de publicar el evento, el sistema queda en un estado inconsistente. La solución estándar es una transacción distribuida que involucre la base de datos y el message broker, pero esto implica elegir entre disponibilidad y coherencia.
¿Cómo coordinan los microservicios una transacción de negocio en varios pasos mediante el patrón Saga con eventos?
En el patrón Saga, cada paso de la transacción es gestionado por un microservicio independiente que actualiza su entidad local y publica un evento en un message broker. Ese evento desencadena el siguiente microservicio en la secuencia. El message broker garantiza la entrega al menos una vez, lo que permite que la transacción global abarque múltiples servicios sin una transacción ACID distribuida, confiando en la consistencia eventual.
¿Cuáles son las tres categorías originales de los patrones de diseño y por qué su uso excesivo puede ser perjudicial?
Las tres categorías originales son Creacionales, Estructurales y De comportamiento. Con el tiempo surgieron nuevas categorías como los patrones de concurrencia. El uso innecesario o excesivo de patrones puede suponer una sobreingeniería, generando un sistema excesivamente complejo con diseño ineficiente, bajo rendimiento y problemas de mantenimiento.
¿Qué es el patrón de diseño Builder y cuáles son sus participantes principales?
El patrón Builder separa la lógica de construcción de un objeto de su representación. Sus participantes principales son: Builder (interfaz abstracta para crear productos), ConcreteBuilder (implementación concreta que crea productos de un tipo determinado) y Director (encargado de utilizar el Builder para construir los objetos).
¿Qué es el patrón Singleton y cuándo se utiliza típicamente?
El patrón Singleton garantiza que solo exista una instancia de una clase, definiendo un único punto global de acceso a ella. Se usa cuando se quiere controlar el acceso a un único recurso físico (por ejemplo, un fichero de lectura de uso exclusivo) o cuando hay datos que deben estar disponibles para el resto de objetos de la aplicación (como una instancia de log). Hay que prestar atención a los problemas de acceso exclusivo en entornos concurrentes.
¿Qué es la Inyección de Dependencias y cómo se relaciona con la Inversión de Control?
La Inyección de Dependencias (DI) es un patrón que extrae la responsabilidad de la creación de instancias de un componente para delegarla en otro (el inyector). Un objeto recibe sus dependencias (servicios) desde el exterior en lugar de crearlas él mismo. El cliente solo necesita conocer las interfaces de los servicios, no su implementación concreta. La DI es una forma de lograr la Inversión de Control (IoC).
¿Qué es el patrón Service Locator y en qué se diferencia de la Inyección de Dependencias?
El patrón Service Locator usa un registro central (el ServiceLocator) que, a demanda, devuelve el componente necesario para una tarea determinada. La principal diferencia con la Inyección de Dependencias es que en Service Locator hay una solicitud explícita para obtener la dependencia, mientras que en DI la dependencia se proporciona automáticamente. Los críticos argumentan que dificulta las pruebas, mientras que sus defensores dicen que simplifica las aplicaciones basadas en componentes. Al igual que DI, es una implementación del principio IoC.
¿Qué es el patrón Abstract Factory y cuáles son sus participantes principales?
Abstract Factory proporciona una interfaz para crear familias de objetos relacionados sin especificar clases concretas. El cliente usa la interfaz genérica de la factoría y no sabe qué objetos concretos obtiene. Sus participantes son: Cliente, AbstractFactory (define las interfaces de las factorías), ConcreteFactory (crea una familia de productos concretos), Producto abstracto (interfaz para la familia de productos genéricos) y Producto concreto (implementaciones específicas). También es una implementación del principio IoC.
¿Qué es el patrón Decorator y qué ventajas ofrece frente a la herencia?
El patrón Decorator asigna responsabilidades adicionales a un objeto de forma dinámica, proporcionando una alternativa flexible a la herencia para extender funcionalidad. Sus participantes son Component (interfaz), ConcreteComponent, Decorator (mantiene una referencia al Component y delega en él) y ConcreteDecorator. Entre sus ventajas destacan: es más flexible que la herencia, permite añadir y eliminar responsabilidades en tiempo de ejecución, evita jerarquías de clases profundas y la herencia múltiple.
¿Qué es el patrón Observer y cuándo debe aplicarse?
El patrón Observer define una dependencia entre objetos de forma que cuando el Subject cambia de estado, todos los Observers dependientes son notificados y pueden reaccionar. El Subject mantiene una lista de Observers y provee métodos de suscripción y cancelación. Debe aplicarse cuando un cambio en un objeto requiere cambiar otros sin saber cuántos, o cuando un objeto debe poder notificar a otros sin saber quiénes son. Respeta el principio Open/Closed, permitiendo añadir Observers sin modificar el Subject.
¿Qué es el patrón Command y qué capacidades habilita?
El patrón Command encapsula una solicitud como un objeto, permitiendo definir una interfaz común para invocar acciones diversas. El Client crea un objeto Command (normalmente pasándole el Receiver) y el Invoker almacena el Command e inicia su ejecución llamando al método execute. Al encapsular la solicitud como un objeto, se habilitan capacidades adicionales como encolamiento, registro y operaciones de deshacer/rehacer, gracias al desacoplamiento entre la solicitud y su ejecución.
¿Qué es el patrón Strategy y cómo se relaciona con el Principio Open/Closed?
El patrón Strategy define una familia de algoritmos, encapsula cada uno de ellos y los hace intercambiables, permitiendo que el algoritmo varíe independientemente de un cliente a otro. Los comportamientos no deben heredarse sino encapsularse mediante interfaces, de modo que se puedan añadir nuevos algoritmos sin modificar el contexto o la interfaz de estrategia existente. Esto es compatible directamente con el Principio Open/Closed (OCP): las clases están abiertas para la extensión pero cerradas para la modificación.
¿Qué es el patrón State y en qué se diferencia de usar sentencias condicionales?
El patrón State permite que un objeto modifique su comportamiento cada vez que cambie su estado interno, pareciendo que cambia de clase. Cada estado se representa mediante una clase separada que implementa una interfaz común, sustituyendo sentencias condicionales complejas. Respeta el Principio Open/Closed y el Principio de Responsabilidad Única (SRP). Las transiciones de estado son atómicas para el Context, evitando estados internos inconsistentes.
¿Qué es el patrón Template Method y cómo demuestra la Inversión de Control?
El patrón Template Method define el esqueleto de un algoritmo en una clase base, delegando algunos pasos en subclases, que pueden redefinir esos pasos sin cambiar la estructura general del algoritmo. En tiempo de ejecución, el algoritmo se ejecuta enviando el mensaje de plantilla a una instancia de una subclase concreta, que rellena los detalles delegados mediante herencia. Demuestra la Inversión de Control porque el código de alto nivel no determina qué algoritmo ejecutar; en su lugar, se selecciona un algoritmo de nivel inferior en tiempo de ejecución.
¿Qué es el patrón Front Controller en JEE y cuáles son sus componentes principales?
El patrón Front Controller utiliza una clase única como intermediaria entre el cliente y los recursos solicitados, centralizando operaciones comunes como autenticación y gestión de errores para evitar duplicación de código. Sus componentes principales son: FrontController (intercepta todas las peticiones web y las delega al Dispatcher), Dispatcher (coordina las acciones para resolver las peticiones con ayuda de un Helper), Helper (contiene la lógica de negocio) y View (muestra el resultado al cliente).
¿Qué es un antipatrón y qué debe incluir para ser reconocido como tal?
Un antipatrón es una mala solución que se aplica comúnmente a un problema recurrente. Para ser reconocido como tal debe: describir un ejemplo de mala solución, analizar las causas que llevaron a ella, recopilar los síntomas y consecuencias que permiten identificarla, y finalmente exponer una solución refactorizada que muestre cómo pasar del diseño deficiente a uno bien diseñado.
¿Qué es el antipatrón The Blob y cómo se debe refactorizar?
El antipatrón The Blob ocurre cuando una clase todopoderosa monopoliza todos los procedimientos y la lógica de negocio mientras el resto de clases solo contienen datos, resultado de un desarrollo iterativo sin distribución adecuada de responsabilidades. La refactorización implica mover parte de la lógica a otras clases, crear objetos más pequeños y especializados, o introducir una clase coordinadora, y su prevención requiere planificar la arquitectura antes de programar.
¿Qué es el antipatrón Lava Flow y cómo se resuelve?
Lava Flow describe código que ha crecido de forma orgánica sin una arquitectura definida, originado normalmente como prototipo que llegó a producción, dejando código no documentado y a menudo sin uso que nadie se atreve a eliminar. La solución exige detener el desarrollo de nuevas funcionalidades, analizar todo el sistema para identificar lo que realmente se usa, redefinir la arquitectura a partir de los requisitos de negocio actuales, refactorizar y documentar exhaustivamente.
¿Qué es el antipatrón Poltergeists y cómo se soluciona?
Los Poltergeists son clases con responsabilidades muy limitadas y ciclos de vida cortos que solo existen para desencadenar acciones en otras clases, añadiendo abstracciones innecesarias y rutas de navegación redundantes al diseño. Se solucionan eliminándolas por completo y trasladando su lógica de inicialización o lanzamiento a las clases que invocaban.
¿Qué es el antipatrón Spaghetti Code y cuáles son sus principales causas?
Spaghetti Code se caracteriza por una estructura de control de flujo excesivamente compleja e incomprensible, con relaciones mínimas entre objetos, métodos orientados a procesos y ausencia de herencia o polimorfismo. Las causas más habituales son la inexperiencia del desarrollador con tecnologías orientadas a objetos, revisiones de código ineficaces o inexistentes, y la falta de análisis y diseño previos a la implementación.
¿Qué es el antipatrón Golden Hammer y cómo se puede prevenir?
Golden Hammer es la tendencia a usar la misma tecnología, framework o lenguaje familiar para resolver cualquier problema, independientemente de si es la mejor opción, impulsada a menudo por la comodidad, grandes inversiones previas o la inercia organizacional. Su prevención requiere fomentar una cultura de aprendizaje continuo, mantenerse al día con nuevas tecnologías y contratar personas con perfiles técnicos diversos.
¿Qué es el patrón Data Access Object (DAO) y qué problema resuelve?
El patrón DAO abstrae y encapsula todo el acceso a la fuente de datos, gestionando la conexión para obtener y almacenar datos. Desacopla la capa de negocio de la implementación de la fuente de datos, de modo que la aplicación no depende de un motor de base de datos concreto. Su principal valor hoy en día reside en la testeabilidad: al mockear la interfaz DAO, se pueden probar las clases de negocio sin conexiones reales a la base de datos.
¿Cómo resuelve el patrón Outbox el problema de la actualización atómica en sistemas orientados a eventos?
El patrón Outbox introduce una tabla EVENT en la base de datos del propio servicio, que actúa como cola de mensajes. En una única transacción local, el servicio actualiza sus entidades de negocio e inserta un registro de evento en la tabla EVENT. Un hilo o proceso separado consulta la tabla EVENT, publica los eventos en el message broker y los marca como publicados usando otra transacción local, garantizando la atomicidad sin necesidad de una transacción distribuida.
¿En qué consiste el enfoque de transaction log mining para publicar eventos y cuáles son sus ventajas e inconvenientes?
El transaction log mining utiliza un hilo o proceso dedicado que lee el registro de transacciones de la base de datos y publica los eventos correspondientes en el message broker cuando se producen cambios en los datos. Un beneficio clave es que garantiza un evento por cada actualización y separa limpiamente la publicación de eventos de la lógica de negocio. El principal inconveniente es que el formato del registro de transacciones es específico de cada base de datos, puede cambiar entre versiones y puede ser difícil construir eventos de alto nivel a partir de entradas de bajo nivel en el registro.
Parte de esta sección está adaptada de una guía de estudio de diseño de software (Izertis, 2024), con licencia CC BY-SA 4.0. Este contenido derivado se comparte bajo la misma licencia.