Entrevistas fallidas

Constancia de las preguntas que respondí mal, en público para no volver a errarlas. Cada una viene con la respuesta que debería haber dado.

Sweatworks TypeScript Engineer May 2026

Una entrevista · 2 preguntas que debería haber clavado

  1. ¿Qué son los closures?

    Dónde me equivoqué: Respondí "una función dentro de otra función". Eso describe una función anidada — básicamente un callback — no un closure. Encima lo planteé como una función que recibe un parámetro, que no tiene nada que ver. Y después me mandé del todo y dije que métodos de array como map, forEach y reduce eran "ejemplos de closures" — no lo son. Esos son funciones de orden superior; el callback que les pasás es un closure solo si captura variables del ámbito que lo rodea. Lo que se me escapó es que un closure se define por retener estado de su ámbito envolvente, no por estar anidado.

    La respuesta que debería haber dado

    Un closure es una función empaquetada junto con las referencias a su ámbito léxico circundante. El rasgo que lo define no es el anidamiento: es que la función interna conserva acceso a las variables de la función externa incluso después de que esta ya retornó, y puede leer y actualizar ese estado capturado entre llamadas. Una función anidada (o un callback) recién se vuelve un closure cuando efectivamente "cierra sobre" (recuerda) estado de su ámbito envolvente. Ese estado retenido y privado es el punto central. Y los métodos de array como map, forEach o reduce tampoco son closures: son funciones de orden superior (funciones que reciben un callback); el callback es un closure solo cuando busca variables fuera de sí mismo.

    function contador() {
      let cuenta = 0;           // capturada por el closure
      return () => ++cuenta;    // recuerda `cuenta` después de que contador() retornó
    }
    const siguiente = contador();
    siguiente(); // 1
    siguiente(); // 2  ← el estado persistió gracias al closure
    ClosuresÁmbito léxicoFunciones de orden superiorJavaScript
  2. ¿JavaScript es de un solo hilo o multihilo?

    Dónde me equivoqué: Dije "de un solo hilo" y después traté de respaldarlo con el event loop y me trabé. Llamé al event loop "una cola" y me quedé ahí — es cierto, pero nunca expliqué por qué es una cola, cómo funciona en realidad, ni cómo eso se conecta con que haya un solo hilo. Media respuesta vaga cae peor que un "sí, un solo hilo" dicho con seguridad.

    La respuesta que debería haber dado

    JavaScript corre sobre un único hilo principal con una sola pila de llamadas, así que ejecuta una cosa a la vez. El event loop es lo que mantiene responsivo a ese único hilo: ejecuta la tarea actual hasta terminarla, mientras que todo lo asíncrono — temporizadores, E/S, fetch, eventos del DOM — se delega al entorno anfitrión (el navegador, o Node vía libuv), que hace ese trabajo por fuera y encola un callback cuando termina. En realidad hay dos niveles: la cola de macrotareas (setTimeout, E/S, eventos de interfaz) y la cola de microtareas (callbacks de promesas, queueMicrotask). El loop recién toma el siguiente callback cuando la pila de llamadas está vacía, y vacía todas las microtareas antes de pasar a la próxima macrotarea. Ese orden — y el hecho de que un bloque síncrono largo congela todo — es exactamente por qué es una cola y no ejecución en paralelo. Para paralelismo real hay que salir del lenguaje: los Web Workers (navegador) y los Worker Threads (Node) corren en hilos del sistema operativo separados y se comunican por paso de mensajes, con SharedArrayBuffer + Atomics para memoria compartida. Entonces: modelo de ejecución de un solo hilo, concurrencia vía event loop, multihilo real vía workers.

    console.log('1: síncrono');                        // corre ahora, en la pila
    setTimeout(() => console.log('4: macrotarea'), 0);  // encolado como macrotarea
    Promise.resolve().then(() => console.log('3: microtarea'));
    console.log('2: síncrono');
    // → 1, 2, 3, 4
    // la pila se vacía → drena TODAS las microtareas (3) → luego la macrotarea (4)
    Event loopMicrotareasWeb WorkersConcurrencia
De oídas Entrevistas de desarrollador Web3 Jun 2026

No es una entrevista que di — es una pregunta que circulaba por internet y que quise entender de verdad.

  1. ¿Qué es un árbol de Merkle?

    Dónde la escuché: Esta no es una pregunta que me hayan hecho: no soy desarrollador web3 y nunca me postulé. Simplemente leía una y otra vez a gente contratando para puestos de web3 quejándose de que los juniors no sabían explicar qué es un árbol de Merkle. Así que leí un artículo por curiosidad, y la idea de ir hasheando pares de hijos una y otra vez para armar un árbol me pareció demasiado linda como para dejarla fuera de esta página.

    Qué es en realidad

    Un árbol de Merkle (o árbol de hashes) es un árbol construido enteramente a partir de hashes. Dividís tus datos en fragmentos y hasheás cada uno: esos hashes son las hojas. Después emparejás las hojas, concatenás cada par y lo hasheás para obtener su padre, y repetís nivel por nivel, hasheando pares de hijos en un único padre, hasta quedarte con un solo hash arriba de todo: la raíz de Merkle. Esa raíz es una huella compacta de todo el conjunto de datos — cambiá un solo byte en cualquier hoja y su hash cambia, lo que cambia su padre, y eso se propaga hasta la raíz. El beneficio es doble: es evidente ante manipulaciones (cualquier cambio se ve en la raíz) y da pruebas de pertenencia baratas. Para probar que un fragmento está en el conjunto no necesitás el conjunto entero, solo el hash hermano de cada nivel en el camino desde tu hoja hasta la raíz (una "prueba de Merkle"), que son apenas unos log₂(n) hashes para n hojas. Cualquiera que tenga la raíz confiable puede recalcularla a partir de tu fragmento más esos hermanos y confirmar que coincide. Así es exactamente como un cliente liviano de Bitcoin o Ethereum verifica que una transacción está en un bloque sin descargar el bloque completo, y la misma idea sostiene a Git, IPFS y Certificate Transparency. (Un detalle que vale la pena saber: cuando un nivel tiene una cantidad impar de nodos, la mayoría de las implementaciones — Bitcoin incluido — simplemente duplican el último hash para poder emparejarlo.)

    import { createHash } from 'node:crypto';
    const sha = (s) => createHash('sha256').update(s).digest('hex');
    
    // 1. hasheá cada fragmento de datos → estas son las hojas
    let nivel = ['a', 'b', 'c', 'd'].map(sha);
    
    // 2. hasheá pares de hijos en un padre, repetí hasta arriba
    while (nivel.length > 1) {
      const siguiente = [];
      for (let i = 0; i < nivel.length; i += 2) {
        const izq = nivel[i];
        const der = nivel[i + 1] ?? izq; // ¿sobra uno? emparejalo consigo mismo
        siguiente.push(sha(izq + der));
      }
      nivel = siguiente;
    }
    
    const raiz = nivel[0]; // cambiá CUALQUIER hoja y esta raíz cambia
    Árbol de MerkleHashingBlockchainEstructuras de datos
micro1 Full-stack Agentic Engineer Sep 2026

Una entrevista asincrónica que conduce la reclutadora con IA de micro1 — nadie del otro lado que interprete el silencio.

  1. Bloqueo optimista vs. pesimista en Postgres: ¿cuál es la diferencia y cuándo usarías cada uno?

    Dónde me equivoqué: Me quedé en blanco. No tenía ninguno de los dos términos — no habría podido decirte cuál toma el candado de entrada y cuál deja que la escritura corra y recién después verifica. Lo que más molesta es que no es una pregunta exótica: es el problema de siempre de "dos personas editan la misma fila" con nombres puestos, y los nombres son la parte barata.

    La respuesta que debería haber dado

    Los dos son respuestas a la misma pregunta: dos transacciones quieren modificar la misma fila y solo una puede tener razón. Postgres es MVCC, así que los lectores nunca bloquean a los escritores ni los escritores a los lectores — toda la pelea es escritor contra escritor. El bloqueo pesimista toma el candado de entrada: SELECT ... FOR UPDATE pone un bloqueo exclusivo a nivel de fila sobre las filas que devuelve, y lo mantiene hasta que la transacción hace COMMIT o ROLLBACK; cualquier otro que intente bloquear o actualizar esas filas espera en la puerta. Hay variantes más débiles (FOR NO KEY UPDATE, FOR SHARE, FOR KEY SHARE) y dos válvulas de escape que sí importan: NOWAIT tira error en vez de esperar, y SKIP LOCKED saltea las filas que ya tiene tomadas otro — que es como se arma una cola de trabajos donde N workers agarran tareas sin bloquearse jamás entre sí. pg_advisory_xact_lock() hace lo mismo para algo que no es una fila. El costo es que estás sosteniendo un candado: si dejás una transacción abierta mientras el usuario piensa o durante un viaje HTTP de ida y vuelta, se te apilan; y tomar candados en distinto orden en distintos caminos del código te lleva a deadlocks (Postgres los detecta después de deadlock_timeout, un segundo por defecto, y mata a uno de los dos). El bloqueo optimista no bloquea nada. Leés, calculás y, a la salida, afirmás que nada se movió: UPDATE ... WHERE id = ? AND version = ?, y después mirás la cantidad de filas afectadas. Cero filas significa que alguien commiteó antes que vos — recargás, reaplicás y reintentás. Lo estándar es una columna entera version; Postgres además expone la columna de sistema oculta xmin (el id de la transacción que escribió la versión actual de la fila), así que podés hacer la misma verificación sin agregar ninguna columna — eso sí, es un contador de 32 bits que se lee como congelado cuando el vacuum congela una fila vieja, con lo cual una columna propia es más segura para datos de larga vida. Hay una tercera respuesta que vale nombrar: dejar que lo resuelva el motor. REPEATABLE READ y SERIALIZABLE son optimistas a nivel de base de datos — SERIALIZABLE usa Serializable Snapshot Isolation para rastrear dependencias de lectura/escritura entre transacciones concurrentes y aborta una con un error de serialización 40001 en lugar de dejar pasar la anomalía. Es la opción con menos código de aplicación, pero solo si escribís el bucle de reintento; no existe la concurrencia optimista sin reintento. Y la trampa que está debajo de todo esto: con el READ COMMITTED por defecto, un simple UPDATE accounts SET balance = balance - 10 WHERE id = 42 ya es seguro, porque el propio UPDATE toma su bloqueo de fila y re-evalúa contra la versión más nueva de la fila cuando se destraba. Lo que no es seguro es hacer leer-modificar-escribir en el código de la aplicación: SELECT balance, restar en tu lenguaje, UPDATE ... SET balance = 90. Eso es la actualización perdida, y es justamente lo que ambas estrategias existen para evitar. Cómo elegir: optimista cuando los conflictos son raros y la transacción abarca tiempo humano, porque nadie debería sostener un candado de base de datos mientras un usuario completa un formulario; pesimista cuando los conflictos son lo normal, cuando reintentar es caro o tiene efectos secundarios que no podés deshacer, o cuando el objetivo es entregar una tarea exactamente una vez.

    -- Pesimista: tomás el candado primero. El resto espera en la puerta.
    BEGIN;
      SELECT balance FROM accounts WHERE id = 42 FOR UPDATE; -- retenido hasta el COMMIT
      UPDATE accounts SET balance = balance - 10 WHERE id = 42;
    COMMIT;
    
    -- Optimista: sin candado. A la salida afirmás que nadie se movió antes.
    UPDATE accounts
       SET balance = balance - 10, version = version + 1
     WHERE id = 42 AND version = 7;
    -- 0 filas actualizadas → alguien commiteó antes que vos → recargá y reintentá
    
    -- La que conviene memorizar: una cola donde N workers nunca se bloquean.
    SELECT id FROM jobs
     WHERE status = 'pending'
     ORDER BY created_at
     LIMIT 1
       FOR UPDATE SKIP LOCKED;
    PostgresBloqueosMVCCTransacciones