Volver a Desarrollador

Desarrollo Backend

Preguntas de entrevista de backend — Java, Spring, concurrencia y diseño de servicios.

Fundamentos de Java y POO

¿Cuáles son los cuatro pilares de la Programación Orientada a Objetos en Java? Da un ejemplo de cada uno.
1) Encapsulamiento: ocultar los datos internos y exponer solo lo necesario mediante getters/setters (ej. clase CuentaBancaria con saldo privado). 2) Abstracción: representar conceptos del mundo real enfocándose en lo esencial (ej. interfaz Pago con método procesarPago()). 3) Herencia: una clase hija hereda atributos y métodos de una clase padre (ej. class Perro extends Animal). 4) Polimorfismo: un mismo método se comporta distinto según el objeto que lo invoque (ej. animal.hacerSonido() ladra si es perro, maúlla si es gato).
Explica los principios SOLID con un ejemplo breve de cada uno.
S (Responsabilidad Única): cada clase tiene un solo motivo de cambio — ej. FacturaPrinter solo imprime, FacturaCalculator solo calcula. O (Abierto/Cerrado): las clases están abiertas a extensión pero cerradas a modificación — ej. clase abstracta Descuento con subclases DescuentoPorcentaje y DescuentoFijo. L (Sustitución de Liskov): las subclases deben poder reemplazar a sus clases base sin alterar el comportamiento. I (Segregación de Interfaces): ninguna clase debe implementar métodos que no usa — preferir interfaces pequeñas como Volador, Nadador. D (Inversión de Dependencias): los módulos de alto nivel dependen de abstracciones — ej. UserNotifier depende de una interfaz Notificador, no de EmailService concreto.
¿Cuál es la diferencia entre una interface y una clase abstracta en Java? ¿Cuándo usarías cada una?
Una interfaz define un contrato que las clases deben cumplir — especifica qué hacer pero no cómo. Una clase abstracta puede proveer implementación parcial junto con el contrato. Se usan interfaces para definir comportamiento común entre clases distintas (ej. Serializable). Se usan clases abstractas cuando se quiere compartir lógica o estado entre clases relacionadas en una jerarquía de herencia.
¿Qué novedades importantes se introdujeron en Java 8, Java 11 y Java 17?
Java 8 introdujo programación funcional: lambdas, Streams API, Optional y la nueva API de fechas. Java 11 trajo mejoras de productividad como la inferencia de tipos con var y una API HTTP moderna con asincronismo. Java 17 añadió records, sealed classes y pattern matching, mejorando la legibilidad y seguridad de tipos.
Explica la diferencia entre == y equals() en Java.
== compara referencias — verifica si dos variables apuntan al mismo objeto en memoria. equals() compara el contenido o valor lógico de los objetos, según cómo esté implementado el método en la clase. Por ejemplo, dos objetos String distintos con el mismo texto devuelven false con == pero true con equals().
¿Qué es el Garbage Collector en Java y qué tipos de GC existen en la JVM?
El Garbage Collector libera automáticamente la memoria de objetos sin referencias, evitando fugas de memoria. Principales implementaciones: Serial GC (un solo hilo, para apps pequeñas), Parallel GC (recolección multi-hilo), G1 GC (basado en regiones, default desde Java 11, equilibra throughput y latencia), y ZGC/Shenandoah (diseñados para baja latencia en heaps grandes).
Nombra algunos patrones de diseño comunes y explica uno con un ejemplo en Java.
Patrones comunes: Singleton (garantiza una única instancia — usado en connection pools, loggers), Factory Method (delega la creación de objetos sin exponer lógica de instanciación), Observer (modelo de suscripción donde observadores reaccionan a cambios del sujeto — usado en sistemas de eventos como Kafka), Strategy (algoritmos intercambiables en tiempo de ejecución) y Saga (coordina transacciones locales entre microservicios sin transacciones distribuidas). Ejemplo — Singleton: constructor privado, campo estático de instancia y método público static getInstance() que garantiza una única instancia en toda la aplicación.
¿Qué es el Principio de Sustitución de Liskov? Da un ejemplo clásico de su violación.
El Principio de Sustitución de Liskov (LSP) establece que las subclases deben poder reemplazar a sus clases base sin alterar la correctitud del programa. Una violación clásica es hacer que Cuadrado extienda Rectángulo: si Rectángulo tiene métodos independientes setWidth/setHeight, un Cuadrado que fuerza ambas dimensiones iguales rompe código que espera dimensiones independientes. La solución es replantear la jerarquía para preservar invariantes.
¿Qué es el Principio de Segregación de Interfaces? ¿Cómo mejora el diseño?
El Principio de Segregación de Interfaces (ISP) establece que ninguna clase debería verse obligada a implementar métodos que no usa. En lugar de una interfaz grande con muchos métodos, se deben crear interfaces más pequeñas y enfocadas. Por ejemplo, en vez de una sola interfaz Animal con fly(), swim() y run(), crear interfaces separadas Volador, Nadador y Corredor. Esto reduce el acoplamiento y facilita la implementación y mantenimiento de las clases.
¿Qué es el patrón Strategy y cuándo lo usarías?
El patrón Strategy define una familia de algoritmos intercambiables encapsulados detrás de una interfaz común, permitiendo seleccionar el comportamiento de una clase en tiempo de ejecución. Se usa cuando hay múltiples formas de realizar una operación y se quieren evitar condicionales. Por ejemplo, un PaymentProcessor puede aceptar distintas estrategias (CreditCardStrategy, PayPalStrategy) y delegar la lógica de pago a la estrategia inyectada sin cambiar su propio código.
¿Qué algoritmo usarías para buscar palabras de forma eficiente horizontal y verticalmente en una matriz de caracteres?
Itera sobre cada fila para coincidencias horizontales y sobre cada columna para coincidencias verticales leyendo los caracteres de arriba hacia abajo. Para cada posición inicial, verifica si los caracteres restantes coinciden con la palabra objetivo. Usar un HashSet para la lista de palabras proporciona búsqueda O(1) por subcadena candidata, manteniendo la complejidad total cerca de O(F × C × L) donde L es la longitud máxima de palabra.
¿Cómo deduplicarías las palabras de la lista antes de procesarlas y por qué importa para el resultado?
Convierte la lista de palabras a un Set antes de buscar, para que cada palabra única se busque exactamente una vez. Sin deduplicación, una palabra que aparece varias veces en la entrada podría inflar su recuento, violando el requisito de que las entradas repetidas en la lista de palabras solo se cuenten una vez.
¿Cómo validas una matriz de caracteres en el backend antes de procesarla?
Comprueba que la matriz no sea nula ni vacía, que tenga como máximo 64 filas y que cada fila tenga como máximo 64 caracteres. Verifica también que todas las filas tengan la misma longitud para asegurar que la matriz es rectangular. Devuelve un error 400 con un mensaje descriptivo si se viola alguna restricción.
¿Cuáles son los comandos básicos de Git que usas regularmente y qué es un branch, merge y pull request?
Comandos comunes: git add . (agregar al staging), git commit -m "mensaje" (crear commit), git push (enviar al remoto), git checkout -b <rama> (crear y cambiar de rama), git pull (traer cambios remotos), git merge <rama> (combinar ramas). Un branch es una línea de desarrollo independiente que evita afectar el código principal. Un merge integra los cambios de una rama en otra. Un pull request es una petición formal de merge usada para revisión de código antes de la integración, común en flujos Git Flow y Feature Branching.
¿Qué debe incluir un archivo README para la entrega de un desafío de código para llevar a casa?
Debe cubrir: propósito y resumen del proyecto, requisitos previos y configuración del entorno, pasos para construir y ejecutar backend y frontend, cómo ejecutar las pruebas, y cualquier limitación conocida o decisión de diseño. Instrucciones claras y mínimas garantizan que los revisores puedan evaluar el trabajo sin necesidad de contactar al candidato.
¿Qué es la Inyección de Dependencias y por qué es útil?
La Inyección de Dependencias (DI) es un patrón de diseño donde un objeto o función recibe sus dependencias de una fuente externa en lugar de crearlas él mismo. Esto desacopla los componentes de sus dependencias concretas, facilitando las pruebas (inyectar mocks) y los cambios (intercambiar implementaciones sin tocar el código consumidor). En React, pasar props o usar un Context Provider son ejemplos cotidianos de DI.
Compara la Programación Orientada a Objetos (POO) con la Programación Funcional (PF). ¿Cuáles son las principales ventajas e inconvenientes?
La POO modela un sistema como objetos que interactúan encapsulando estado mutable mediante clases, herencia y polimorfismo. La PF modela la computación como una cadena de funciones puras sobre datos inmutables usando funciones de orden superior y evitando efectos secundarios. La POO es intuitiva para modelar el dominio; la PF destaca en transformaciones de datos, testabilidad (las funciones puras son trivialmente testeables) y seguridad en concurrencia gracias a la inmutabilidad.
¿Qué problema resuelve el patrón Business Delegate y cuál es su principal beneficio?
Los componentes de la capa de presentación suelen interactuar directamente con servicios remotos, exponiendo los detalles de implementación y generando un acoplamiento estrecho. Un Business Delegate actúa como proxy del lado del cliente que encapsula la búsqueda, el acceso al servicio y la traducción de excepciones, ocultando los detalles de implementación y reduciendo el impacto de los cambios del servicio en el código cliente.
¿Qué es el patrón Session Facade y por qué conviene evitar crear una fachada por cada caso de uso?
Session Facade es un patrón JEE en el que un bean de sesión actúa como único punto de entrada para un grupo de interacciones con objetos de negocio, gestionando su ciclo de vida y ocultando la complejidad al cliente. Crear una fachada por cada caso de uso genera una granularidad y complejidad excesivas; es preferible agrupar casos de uso relacionados en una sola Session Facade para simplificar la aplicación.
¿Qué es el patrón Composite Entity y cuáles son los roles del objeto de grano grueso y el objeto dependiente?
Composite Entity es un patrón JEE que modela y gestiona un conjunto de objetos relacionados como una sola unidad para reducir el acoplamiento y mejorar el mantenimiento. Un objeto de grano grueso contiene objetos dependientes, gestiona su ciclo de vida y tiene el propio; los objetos dependientes (generalmente de grano fino) dependen del objeto de grano grueso y pueden formar una estructura en árbol.
¿Qué es el patrón Transfer Object Assembler y cuándo es útil?
Transfer Object Assembler construye un objeto de transferencia compuesto agregando datos de múltiples servicios y componentes, y lo devuelve al cliente en una sola operación. Es útil cuando se desea centralizar la lógica de negocio, crear un modelo complejo para presentación sin sobrecargar al cliente, o reducir el acoplamiento entre el cliente y los componentes empresariales.
¿Qué es el patrón Service Locator en JEE y cuáles son sus principales responsabilidades?
Service Locator es un patrón JEE que proporciona un mecanismo uniforme y transparente para localizar servicios y componentes como fuentes de datos JDBC o servicios asíncronos. Centraliza y encapsula el mecanismo de búsqueda, incluida la creación del contexto inicial, para que los clientes no necesiten conocer los detalles de cómo se localizan o acceden los servicios.

Spring y Spring Boot

¿Qué anotaciones comunes de Spring se usan para definir beans y qué es la inyección de dependencias?
En Spring, @RestController marca clases de endpoints REST, @Service marca componentes de lógica de negocio y @Repository marca componentes de acceso a datos. Estas anotaciones registran las clases como beans administrados por el contenedor IoC de Spring. La inyección de dependencias permite que el contenedor conecte los beans automáticamente — típicamente vía inyección por constructor o @Autowired — eliminando la necesidad de instanciar dependencias manualmente con new.
¿Cómo se implementa caché en memoria con TTL en Spring Boot?
Se habilita el caché con @EnableCaching y se anota el método que obtiene el valor con @Cacheable. Para controlar el TTL se configura un CacheManager (por ejemplo, CaffeineCacheManager) con expireAfterWrite. Ejemplo: Caffeine.newBuilder().expireAfterWrite(30, TimeUnit.MINUTES).build() vinculado al manager. Spring servirá los valores cacheados hasta que expire la entrada.
¿Cómo se implementa un patrón de fallback al caché cuando un servicio externo no está disponible?
Se captura la excepción lanzada por la llamada externa y se intenta leer el último valor conocido del caché. Si existe un valor cacheado, se retorna y se registra una advertencia. Si no hay valor en caché, se propaga un error HTTP apropiado (por ejemplo, 503 Service Unavailable). Esto desacopla al llamador de fallos transitorios en servicios downstream.
¿Cómo se configura la lógica de reintentos para una llamada a un servicio externo en Spring Boot?
Se agrega spring-retry al classpath y se anota la clase de configuración con @EnableRetry. Luego se anota el método con @Retryable(maxAttempts = 3, backoff = @Backoff(delay = 500)). Tras agotar los intentos, un método @Recover puede devolver un valor de fallback o lanzar una excepción específica. Esto mantiene la lógica de reintentos separada de la lógica de negocio.
¿Cómo se implementa rate limiting (por ejemplo, 3 requests por minuto) en una API REST de Spring Boot?
Un enfoque común es usar la librería Bucket4j, que implementa el algoritmo token-bucket. Se crea un Bucket con una recarga de 3 tokens por minuto y se aplica en un filtro de servlet o un interceptor de Spring. Cuando el bucket está vacío, se retorna HTTP 429 Too Many Requests con un mensaje descriptivo. Para despliegues multi-réplica, el bucket se puede respaldar con un almacén distribuido como Redis.
¿Cómo se implementa el manejo global de errores HTTP en Spring Boot?
Se crea una clase anotada con @RestControllerAdvice y se definen métodos @ExceptionHandler para tipos de excepción específicos. Cada handler retorna una ResponseEntity con el estado HTTP apropiado (4XX o 5XX) y un cuerpo de error estructurado con mensaje, timestamp y path. Esto centraliza el formato de errores y mantiene los controladores limpios.
¿Cómo se implementan consultas paginadas en Spring Data JPA?
Los métodos del repositorio aceptan un parámetro Pageable y retornan Page<T>. El controlador acepta parámetros de consulta page y size, construye un PageRequest y lo pasa al repositorio. El objeto Page retornado contiene la lista de contenido, el total de elementos y el total de páginas, que pueden mapearse a un DTO para la respuesta de la API. Ejemplo: repository.findAll(PageRequest.of(page, size, Sort.by("createdAt").descending())).
¿Cuál es la diferencia entre Spring WebFlux y Spring MVC, y cuándo se elige WebFlux?
Spring MVC usa un modelo de un hilo por request y bloquea en I/O; Spring WebFlux usa un modelo no bloqueante basado en bucle de eventos (Project Reactor) y maneja muchas solicitudes concurrentes con menos hilos. WebFlux es beneficioso cuando el servicio realiza muchas llamadas HTTP o de base de datos downstream con alta concurrencia y requisitos de baja latencia. Para APIs CRUD simples o con uso intensivo de CPU, Spring MVC suele ser más sencillo y suficiente.
¿Cómo se genera documentación OpenAPI/Swagger para una API REST de Spring Boot?
Se agrega la dependencia springdoc-openapi-starter-webmvc-ui. Spring Boot auto-configura el endpoint /v3/api-docs y la UI de Swagger en /swagger-ui.html. Se enriquece la documentación con anotaciones @Operation, @Parameter y @ApiResponse en los controladores. Esto produce documentación interactiva y siempre actualizada que los clientes pueden usar para probar los endpoints directamente desde el navegador.
¿Cómo se prueban los escenarios de fallo del servicio externo en un test unitario de Spring Boot?
Se usa Mockito para mockear el cliente del servicio externo y se configura para lanzar una excepción con when(...).thenThrow(...). Se verifica que el mecanismo de reintento se dispara el número esperado de veces y que se retorna el valor de fallback (caché o respuesta de error). Para tests de integración, WireMock puede stubbear endpoints HTTP para retornar respuestas de error, proporcionando una simulación de fallo más realista.

Concurrencia y la JVM

¿Qué es un thread en Java, qué problemas de concurrencia pueden surgir y cómo los gestionarías?
Un thread es una unidad de ejecución independiente dentro de un proceso. El modelo de multithreading de Java mejora la eficiencia pero introduce riesgos como las race conditions — cuando varios hilos modifican datos compartidos simultáneamente generando resultados impredecibles. Las soluciones incluyen bloques synchronized, locks explícitos (ReentrantLock) y estructuras concurrentes de java.util.concurrent (ej. ConcurrentHashMap, AtomicInteger).
¿Qué es CompletableFuture en Java y cómo se encadenan múltiples operaciones asíncronas?
CompletableFuture es una clase que permite ejecutar tareas de forma asíncrona y no bloqueante, y componerlas. Se pueden encadenar operaciones con métodos como thenApply() (transformar resultado), thenCompose() (encadenar otro future) y thenCombine() (combinar dos futures). Se integra con lambdas y permite manejo de errores con exceptionally() o handle(), ideal para flujos con llamadas a servicios o I/O.
¿Cómo se implementa el registro asíncrono de llamadas en Spring Boot sin afectar el tiempo de respuesta principal?
Se anota el método de registro con @Async y se habilita con @EnableAsync en una clase de configuración. El llamador dispara y olvida: el hilo principal retorna inmediatamente mientras el registro se ejecuta en un pool de hilos separado. Se envuelve el método async en un try-catch para que cualquier fallo de persistencia sea silenciado y nunca se propague al llamador.
¿Qué es el patrón Service Activator y qué problema resuelve?
Service Activator es un patrón JEE que permite invocar de forma asíncrona uno o varios servicios, normalmente implementado mediante infraestructura de mensajería como JMS. Aborda la necesidad de aplicaciones que no pueden esperar una respuesta síncrona, por ejemplo cuando una tarea empresarial está compuesta de varias subtareas, proporcionando un modelo para enviar una petición y gestionar la respuesta cuando esté disponible.