El relato, en primera persona, de cómo afronté un clásico test de entrevista backend: siete ejercicios sueltos, cada uno con su propio bug esperando debajo, y una regla que terminó marcando todo el proceso — no dar nada por bueno sin ejecutarlo.
El reto
El enunciado (ejercicios.md) no pedía construir nada desde cero: era un proyecto Maven multi-módulo ya montado, con siete ejercicios independientes y un bug distinto agazapado en cada uno — desde un factorial que no factorializaba nada hasta un contexto de Spring que se negaba a arrancar. Es el formato típico de una entrevista técnica exprés, con una restricción propia por ejercicio: el 2 pedía corregir los fallos sin usar anotaciones, y el 3 exactamente lo contrario.
Antes de tocar una sola línea, documenté cada enunciado en un README.md por módulo. Así cada ejercicio quedaba con su contrato explícito por escrito antes de empezar a picar código — y con un sitio claro donde anotar, más tarde, qué se había roto y por qué.
El proceso, ejercicio a ejercicio
Cada ejercicio se cerró con su propio commit — nueve en total, uno de ellos dedicado solo a limpiar los nombres de los módulos. El historial completo, con más detalle técnico del que cabe aquí, queda en HISTORIAL.md.
0 · Factorial
getFactorial devolvía directamente el número que recibía, sin calcular nada — Main esperaba un 24 para getFactorial(4) y se encontraba con un 4. Mi primer bucle while resolvía el caso feliz pero escondía una trampa: arrancaba result en n y frenaba comparando con 1, así que con n=0 la condición nunca se cumplía y el bucle no terminaba jamás.
La versión final arranca result en 1 — el valor correcto de 0! por definición matemática, el producto vacío, no una elección arbitraria — y solo multiplica mientras n > 1. El primer recordatorio de la sesión: un caso límite obvio puede colarse incluso en un ejercicio que parece resuelto.
1 · Servlet
MainServlet estaba vacía; la resolví con un AtomicInteger que cuenta cada GET y devuelve el valor actual, justo lo que pedía el enunciado. Pero se coló un bug más interesante que el propio ejercicio: al escribir @WebServlet, el autocompletado importó jakarta.servlet.annotation.WebServlet en una clase que seguía extendiendo javax.servlet.http.HttpServlet. Compilaba sin quejarse — Java no exige que una anotación case con la jerarquía de tipos de la clase —, pero en un despliegue real habría sido papel mojado o, según la versión de Tomcat, un fallo de carga directo.
La causa real estaba en el pom.xml: había añadido tomcat-embed-core (que trae sus propias clases jakarta.servlet.*) a un proyecto pensado para desplegarse sobre un Tomcat ya existente. Quitar esa dependencia y la anotación, dejando que web.xml se encargara en solitario del mapeo, fue la solución de verdad.
2 · Spring (sin anotaciones)
Este venía con truco doble. BeanExample tenía getter pero no setter, así que Spring no podía escribir la property que el XML intentaba inyectar. Y, más escondido, base.properties definía la clave fake.property mientras el XML pedía resolver ${base.property} — un desajuste invisible para el compilador y el IDE, que solo revienta al ejecutar.
Añadí el setter y renombré la clave, sin tocar el XML ni usar una sola anotación, tal y como pedía el enunciado. Lo confirmé arrancando Main de verdad, no leyendo el código: "La prueba ha ido bien" en consola.
3 · Spring Annotations
Mismo contexto, restricción inversa: había que resolverlo con anotaciones. BeanExample no llevaba @Component, así que el component-scan pasaba de largo; y aunque lo hubiera encontrado, a ServiceExample.beanExample le faltaba @Autowired, con lo que print() habría reventado con un NullPointerException.
@Component + @Value("${base.property}") en un lado, @Autowired en el otro, sin tocar el XML. Confirmado igual que el ejercicio anterior: arrancando el contexto real.
4 · Spring MVC — CRUD de usuarios
Monté los cuatro verbos sobre UserService (ya resuelto de antemano), delegando en él y traduciendo UserNotFounException a un 404. La lógica de negocio salió bien a la primera. Lo que no salió bien fue @RestController("/users"), escrito con la intención de fijar /users como prefijo — pero ese valor es el nombre del bean, heredado de @Component, no una ruta. Nunca participa en el routing, ni escrito así ni como value="/users".
Arranqué la aplicación de verdad y miré el log de arranque: los endpoints reales vivían en la raíz (/, /{id}), y /users solo respondía a medias por un mecanismo heredado de Spring que registra como URL cualquier bean cuyo nombre empiece por «/». Con curl en caliente la inconsistencia era flagrante: GET /users/1 devolvía 405; GET /users sin id devolvía 400 al intentar convertir "users" en Integer. La corrección fue separar @RestController de @RequestMapping("/users") — el único mecanismo real para fijar un prefijo a nivel de clase. Repetí la batería completa de curl y esta vez las cinco peticiones devolvieron justo lo esperado: 404, 201, 200, 204, 404.
5 · Número más cercano a cero
getClosestToZero devolvía 0 a pelo — acertaba de milagro en el único test que esperaba justo ese valor. La resolví recorriendo el array con un candidato que se actualiza por valor absoluto, con un criterio de desempate explícito: si dos números son equidistantes de cero, gana el positivo. Los cuatro casos del enunciado dieron el resultado esperado, y probé aparte un par de empates que los tests originales no cubrían ({-2,2} y {2,-2}) para confirmar que el orden de aparición no rompía la regla.
De paso · Nombres de módulo
Revisando los pom.xml encontré que 5.ArrayTest tenía el <name> literalmente copiado y pegado del ejercicio 0: se llamaba «Factorial». Aproveché para alinear los siete módulos bajo la misma convención N. Título.
6 · Palíndromo
La primera versión de isPalindrome comparaba carácter a carácter tras un simple trim(), y pasaba los cinco tests que ya existían... hasta que la probé contra el propio ejemplo que el enunciado usa para definir qué es un palíndromo: «a esa paloma ese amo la pasea». Fallaba — igual que un clásico como «Anita lava la tina» — porque trim() solo quita espacios de los extremos, no los internos, y no normaliza mayúsculas.
La corrección pasa la cadena a minúsculas y elimina los espacios internos antes de comparar. Y ya que el propio test pedía explícitamente «completar las pruebas que le falten», añadí la frase del enunciado, un caso con mayúsculas mezcladas y la cadena vacía a PalindromeTest.
Decisiones que pesé
- Verificar en caliente, no fiarse de la lectura
- En más de un ejercicio el código compilaba perfectamente y aun así estaba roto: la mezcla
javax/jakartadel ejercicio 1 y el enrutado fantasma del 4 solo se ven arrancando la aplicación de verdad y golpeándola concurl. Para eso tuve que salir del modo offline de Maven más de una vez — el caché local no tenía versiones antiguas de algunos plugins, sobre todo del Spring Boot 1.5.16 del ejercicio 4 —, un recordatorio de que «compila» y «funciona» son afirmaciones distintas. - Perseguir la causa, no el síntoma
- Ni el setter que faltaba en el ejercicio 2 ni la clave
fake.propertymal escrita en subase.propertieseran errores de lógica: eran de cableado. Lo mismo con@RestController("/users")en el 4 — el síntoma (rutas raras) apuntaba a un sitio, la causa (un atributo que nunca fue una ruta) estaba en otro completamente distinto. - Un commit por ejercicio, un capítulo por commit
- Cada ejercicio se resolvió y se documentó como una unidad propia: su commit con mensaje en conventional commits, y su capítulo en
HISTORIAL.mdcontando qué estaba roto y por qué la solución era esa y no otra. Elgit loges, en sí mismo, la prueba de que cada pieza se puede revisar por separado.
Qué me habría gustado pulir
Siendo objetivo, verificar en caliente el ejercicio 4 dejó a la vista un par de detalles menores que no incumplían el enunciado, pero que anoté igualmente:
- El
POST /userssiempre devuelve un headerLocationapuntando a"/", en vez de a la URL del recurso recién creado. - Los
ResponseEntitydel controlador van sin tipar, lo que renuncia a parte de la seguridad de tipos que Java ofrece de fábrica. - El manejo de
UserNotFounExceptionse repite en cada método del controlador; un@ExceptionHandlercentralizado lo habría limpiado.
El resultado
Siete ejercicios, nueve commits, siete bugs distintos — y en casi todos, el fallo real estaba un paso más allá de lo que el enunciado insinuaba. El código final es deliberadamente simple: ninguna abstracción que el ejercicio no pidiera, solo la solución correcta a cada problema y la evidencia de que de verdad lo es.