ENTREVISTAS TÉCNICAS

Preguntas frecuentes en entrevistas

Conceptos técnicos que suelo repasar antes de una entrevista, explicados de forma sencilla y con ejemplos. La mayoría son de ingeniería de software en general; los de colecciones son específicos de Java, mi lenguaje principal.

Principios de diseño

SOLID son 5 principios de diseño orientado a objetos que ayudan a escribir código mantenible y fácil de extender:

S – Responsabilidad Única: Una clase debe tener una sola responsabilidad, un solo motivo para cambiar. Ejemplo: separa la lógica de guardar una factura en base de datos de la lógica de enviarla por correo; son responsabilidades distintas.
O – Abierto/Cerrado: El código debe estar abierto a extensión, pero cerrado a modificación. Ejemplo: para agregar un nuevo medio de pago, creas una clase nueva que implemente una interfaz PaymentMethod, sin tocar el código que ya funciona.
L – Sustitución de Liskov: Una subclase debe poder reemplazar a su clase padre sin romper el comportamiento del programa. Ejemplo: si Ave tiene el método volar(), no deberías heredar Pinguino de Ave, porque un pingüino no vuela.
I – Segregación de Interfaces: Es mejor tener varias interfaces pequeñas y específicas que una sola interfaz grande. Ejemplo: separa una interfaz Trabajador en Comestible y Programable, para que una clase Robot no tenga que implementar comer().
D – Inversión de Dependencias: Los módulos de alto nivel no deben depender de módulos de bajo nivel, sino de abstracciones. Ejemplo: un servicio de notificaciones debe depender de una interfaz NotificationSender, no directamente de una clase EmailSender.
"Don't Repeat Yourself" (no te repitas): evita duplicar la misma lógica o regla de negocio en varias partes del código. Si algo está escrito en dos lugares, un cambio futuro puede actualizar uno y olvidar el otro, generando bugs. Ejemplo: si validas el formato de un correo en tres controladores distintos, mejor crea un solo método de validación y reúsalo.
"You Aren't Gonna Need It" (no lo vas a necesitar): no construyas funcionalidad extra "por si acaso" se necesita en el futuro. Ejemplo: si hoy solo necesitas guardar usuarios en una base de datos relacional, no construyas de entrada una capa para soportar 5 tipos de bases de datos que quizás nunca uses — es complejidad innecesaria.
Alta cohesión: los elementos dentro de una misma clase o módulo están fuertemente relacionados y enfocados en una sola responsabilidad.
Bajo acoplamiento: los módulos o clases dependen lo menos posible unos de otros, idealmente comunicándose a través de interfaces.
Un sistema con alta cohesión y bajo acoplamiento es más fácil de entender, probar y modificar, sin que un cambio en una parte rompa otras.

Arquitectura y microservicios

Un estilo de arquitectura donde una aplicación se divide en varios servicios pequeños e independientes, cada uno enfocado en una función de negocio específica (pagos, usuarios, notificaciones...). Cada servicio tiene su propia base de datos, se despliega por separado y se comunica con los demás por red (REST, colas o eventos). Esto permite escalar y evolucionar cada parte del sistema de forma independiente.
API Gateway: un único punto de entrada que enruta las peticiones del cliente hacia el microservicio correcto, y puede manejar autenticación y límites de tasa.
Circuit Breaker: evita que un servicio siga llamando a otro que está fallando; "corta el circuito" temporalmente para no saturar el sistema.
Saga: coordina transacciones que abarcan varios microservicios mediante una secuencia de pasos con acciones compensatorias si algo falla, ya que no existe una transacción distribuida única.
Service Discovery: permite que los servicios se encuentren entre sí dinámicamente en la red, sin direcciones fijas.
Strangler Fig: permite migrar un monolito a microservicios poco a poco, reemplazando funcionalidades una por una en vez de reescribir todo de golpe.
ACID: (Atomicidad, Consistencia, Aislamiento, Durabilidad) son las garantías clásicas de las bases de datos relacionales: una transacción se ejecuta completa o no se ejecuta, y el sistema siempre queda consistente.
BASE: (Basically Available, Soft state, Eventually consistent) es el enfoque típico de muchas bases NoSQL y sistemas distribuidos: priorizan disponibilidad y escalabilidad, aceptando que los datos tarden un poco en ser consistentes en todos los nodos.
Una operación es idempotente si ejecutarla una o varias veces produce el mismo resultado final. Un PUT que actualiza un recurso a un valor específico es idempotente; repetirlo no cambia nada más. Un POST que crea un registro nuevo cada vez normalmente no lo es. Es clave en sistemas de pagos, para evitar cobrar dos veces si una petición se reintenta por un error de red.

Patrones y buenas prácticas

Singleton: garantiza que una clase tenga una única instancia en toda la aplicación, por ejemplo una configuración compartida.
Factory: centraliza la creación de objetos en un método, para no repetir la lógica de instanciación por toda la app.
Builder: permite construir un objeto complejo paso a paso, útil cuando tiene muchos parámetros opcionales.
Strategy: permite intercambiar un algoritmo por otro en tiempo de ejecución, por ejemplo distintas formas de calcular un descuento.
Observer: permite que varios objetos se suscriban y sean notificados cuando algo cambia, como en sistemas basados en eventos.
Adapter: permite que dos interfaces incompatibles trabajen juntas, por ejemplo al integrar una librería externa con una interfaz distinta a la que espera tu código.
Un objeto no crea directamente las dependencias que necesita, sino que las recibe desde afuera (por ejemplo, por el constructor). Frameworks como Spring gestionan estas dependencias mediante un contenedor de IoC. Ejemplo: un PagoService no debería hacer "new EmailSender()" dentro de sí mismo; en vez de eso recibe una interfaz NotificationSender ya lista, así es más fácil cambiarla o probarla con un mock.
Autenticación: responde "¿quién eres?" — verificar la identidad de un usuario, por ejemplo con usuario/contraseña o un token.
Autorización: responde "¿qué puedes hacer?" — una vez identificado, verificar si tiene permiso para una acción o recurso. Ejemplo: iniciar sesión es autenticación; que solo un usuario "admin" pueda borrar registros es autorización.
Es un antipatrón donde las clases del dominio solo tienen atributos con getters/setters, pero toda la lógica de negocio vive afuera, en clases de servicio. El objeto es solo una "bolsa de datos" sin comportamiento propio. Ejemplo: una clase Factura con getTotal()/setTotal() donde toda la lógica para calcular impuestos vive en FacturaService.
Se prefiere un modelo rico, donde el propio objeto encapsula sus reglas: factura.calcularTotal(), factura.aplicarDescuento(), etc. Así la lógica queda junto a los datos que usa, con mayor cohesión.

Programación orientada a objetos

Consiste en modelar un objeto quedándote solo con lo esencial para el problema, ocultando los detalles de implementación. Ejemplo: una interfaz Notificador expone enviar(mensaje), sin exponer si por dentro usa correo, SMS o push.
Consiste en ocultar el estado interno de un objeto y exponer solo lo necesario a través de métodos públicos, protegiendo los datos de modificaciones inválidas. Ejemplo: una clase CuentaBancaria no permite modificar el saldo directamente; solo lo hace a través de métodos como depositar() o retirar(), que validan las reglas de negocio.
Permite que una clase (subclase) reutilice atributos y comportamiento de otra clase (superclase), estableciendo una relación "es un". Ejemplo: TarjetaCredito y TarjetaDebito pueden heredar de una clase Tarjeta que ya define el número y el titular. Debe usarse con cuidado para no violar Liskov: solo hereda si la subclase realmente "es un tipo de" la clase padre.
Permite que distintos objetos respondan de forma distinta a un mismo mensaje o método. Ejemplo: si Tarjeta tiene el método calcularInteres(), TarjetaCredito y TarjetaDebito pueden implementarlo cada una a su manera, y el código que las usa solo llama tarjeta.calcularInteres() sin saber (ni importarle) de qué tipo específico es.

Estructuras de datos y colecciones

Son estructuras de Java que resuelven problemas distintos:

HashMap: guarda pares clave-valor. El acceso, inserción y búsqueda por clave son casi instantáneos (O(1) en promedio) gracias a una función de hash. Úsalo cuando necesitas buscar algo rápido por un identificador, como un usuario por su ID.
LinkedList: es una lista donde cada nodo apunta al siguiente. Insertar o eliminar al inicio o en medio es rápido (O(1)), pero buscar un elemento por posición es lento (O(n)) porque hay que recorrer nodo por nodo.
Extra: ArrayList: a diferencia de LinkedList, guarda los elementos en un arreglo contiguo: acceder por índice es muy rápido (O(1)), pero insertar en medio es más costoso porque hay que desplazar elementos.
List: una colección ordenada que permite elementos duplicados y acceso por índice, como ArrayList o LinkedList.
Set: una colección que no permite elementos duplicados, como HashSet. Útil cuando solo te importa si un elemento existe o no.
Map: una colección de pares clave-valor, sin claves duplicadas, como HashMap. Útil para buscar un valor a partir de un identificador.
HashMap: no garantiza ningún orden de las claves; es la opción más rápida cuando el orden no importa.
LinkedHashMap: mantiene el orden de inserción de las claves, útil cuando necesitas recorrerlas en el mismo orden en que las agregaste.
TreeMap: mantiene las claves ordenadas (por orden natural o un comparador), a costo de operaciones un poco más lentas (O(log n) en vez de O(1)).
HashMap no es seguro para usarse desde varios hilos al mismo tiempo (puede corromperse o entrar en bucles infinitos). ConcurrentHashMap permite lecturas y escrituras concurrentes de forma segura, dividiendo internamente el mapa en segmentos, sin bloquear toda la estructura como haría un HashMap sincronizado manualmente. Se usa mucho en servicios backend con alta concurrencia, como cachés en memoria.

¿TE HAN HECHO OTRA PREGUNTA?

Cuéntame una pregunta que te hayan hecho en una entrevista y la respuesta correcta — te la puedo agregar a esta lista.