Codea Seguro Codea Seguro
Arquitectura de Software Nivel: basic Arquitectura Software

Ejercicio entrevista: acortador de URLs para campañas de marketing

Ejercicio práctico de diseño de sistemas nivel básico — un acortador de URLs con métricas para medir campañas de marketing.

Un ejercicio típico de diseño de sistemas para un perfil junior, con respuestas modelo. Ideal para practicar porque es simple de plantear pero da mucho para conversar.

Planteo del problema

El equipo de marketing lanza campañas en varios canales (email, Instagram, YouTube…) y quiere compartir enlaces cortos y con marca, por ejemplo codea.se/verano26. Cuando alguien entra al enlace corto, lo redirigimos a la URL original (la landing de la campaña).

Pero además hay un requisito clave del negocio: marketing necesita saber cuántos clics recibió cada enlace para medir qué campaña funcionó mejor.

¿Cómo diseñarías este servicio?

Qué preguntar primero (requisitos)

Antes de tirar código, conviene aclarar el alcance:

  • ¿Cuántos enlaces por día? ¿Se leen (clics) mucho más de lo que se crean? → Sí: muchísima lectura, poca escritura.
  • ¿El código corto lo genera el sistema o marketing quiere personalizarlo (/verano26)?
  • ¿Necesitamos solo el total de clics o también desglose por canal/fecha?

Solución propuesta

El diseño

Cuando alguien entra al enlace corto pasan tres cosas: (1) el servicio busca la URL —primero en la caché, y solo si no está va a la base de datos—, (2) redirige al usuario, y (3) registra el clic de forma asíncrona (por una cola) para no demorar el redirect.

flowchart LR
    subgraph campana["Campaña de marketing"]
        direction LR
        email["Email"]
        ig["Instagram"]
        yt["YouTube"]
    end

    email --> corta["codea.se/verano26"]
    ig --> corta
    yt --> corta

    corta --> api["Servicio acortador"]

    %% 1. Lectura: buscar la URL, con caché delante de la base de datos
    api -->|"1. busca el codigo"| cache[("Caché")]
    cache -.->|"si no está (miss)"| db[("Base de datos")]
    db -.->|"guarda en caché"| cache

    %% 2. Redirect al usuario
    api -->|"2. 301 redirect"| destino["Landing de la campaña"]

    %% 3. Escritura de métricas, asíncrona
    api -.->|"3. evento de clic"| cola["Cola de eventos"]
    cola --> worker["Worker de métricas"]
    worker --> metricas[("Métricas por campaña")]
    dash["Panel de marketing"] --> metricas

    style cache fill:#9dcc4d,stroke:#4d6c20,color:#0f172a
    style metricas fill:#9dcc4d,stroke:#4d6c20,color:#0f172a

Las dos operaciones (la API)

  • POST /acortar → recibe la URL larga y, opcionalmente, un código personalizado y el nombre de la campaña. Devuelve el enlace corto.
  • GET /{codigo} → busca el código, registra el clic y hace un 301/302 redirect a la URL original.

Un ejemplo concreto de cada una:

POST /acortar
{
  "url": "https://codeaseguro.dev/landing/verano",
  "codigo": "verano26",       // opcional
  "campania": "Verano 2026"
}

→ 201 Created
{ "corta": "https://codea.se/verano26" }
GET /verano26

→ 302 Found
Location: https://codeaseguro.dev/landing/verano

Cómo generar el código corto

  • Automático: un contador autoincremental en la base de datos codificado en Base62 (a-z A-Z 0-9) → códigos cortos y únicos (125 → "cb").
  • Personalizado: marketing elige el código (verano26); solo hay que validar que no exista.

Convertir el id numérico a Base62 es sencillo:

const ALFABETO = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';

function aBase62(id: number): string {
  let codigo = '';
  do {
    codigo = ALFABETO[id % 62] + codigo;
    id = Math.floor(id / 62);
  } while (id > 0);
  return codigo;
}

// aBase62(125) === "cb"

Con solo 6 caracteres en Base62 hay 62⁶ ≈ 56.800 millones de combinaciones: más que suficiente para cualquier volumen de campañas.

Guardar los datos

Dos cosas separadas: los enlaces y los clics.

  • Tabla enlaces: codigo (PK) | url_original | campania | fecha_creacion.
  • Los clics se cuentan aparte: un contador por enlace (o un registro por clic con fecha/canal si marketing quiere desglose).
CREATE TABLE enlaces (
  codigo        VARCHAR(12) PRIMARY KEY,   -- "verano26"
  url_original  TEXT NOT NULL,
  campania      VARCHAR(120),
  fecha_creacion TIMESTAMP DEFAULT now()
);

-- Un registro por clic → permite desglosar por fecha y canal.
CREATE TABLE clics (
  id       BIGSERIAL PRIMARY KEY,
  codigo   VARCHAR(12) REFERENCES enlaces(codigo),
  canal    VARCHAR(60),               -- de ?utm_source=instagram
  fecha    TIMESTAMP DEFAULT now()
);

La búsqueda de un redirect es un SELECT url_original FROM enlaces WHERE codigo = $1 por clave primaria: rapidísima.

El punto clave: no frenar el redirect

El usuario tiene que ser redirigido rápido. Registrar la métrica no debería hacerlo esperar.

TIP

Registrá el clic de forma asíncrona: respondé el redirect enseguida y mandá el evento de “clic” a una cola para procesarlo después. Así las métricas nunca frenan al usuario.

Y como los redirects son la operación más frecuente, se agrega una caché (codigo → url) delante de la base de datos, para que la mayoría de los clics ni la toquen.

CAUTION

Si contás los clics escribiendo en la base de datos en cada redirect, en un pico de campaña esas escrituras pueden saturarla. Por eso conviene la cola + caché en lugar de escribir directo.

Qué evaluamos en el candidato

  • Que pregunte los requisitos antes de diseñar.
  • Que separe escritura (crear enlace) de lectura (redirigir), y note que es read-heavy.
  • Que entienda que registrar métricas no debe bloquear el redirect (async / cola).
  • Bonus: que proponga desglosar clics por canal usando parámetros UTM (?utm_source=instagram).