InterviewHack.ai
Empezar gratis
Blog/System Design: cómo diseñar un sistema de pagos en una entrevista

System Design: cómo diseñar un sistema de pagos en una entrevista

16 de septiembre de 2026

system-designbackend-developer

Guía paso a paso para diseñar un sistema de pagos en una entrevista de system design: idempotencia, consistencia, colas de transacciones, reconciliación, seguridad PCI DSS.

System Design: cómo diseñar un sistema de pagos en una entrevista

Diseñar un sistema de pagos es una de las preguntas más frecuentes —y más temidas— en entrevistas de system design para roles senior y staff. No porque sea imposible, sino porque requiere combinar criterios de correctitud (idempotencia, consistencia), rendimiento (latencia sub-segundo, throughput alto), regulación (PCI DSS, SCA) y operaciones (reconciliación, monitoreo).

Esta guía te lleva paso a paso, con el mismo ritmo que debés usar en la entrevista: primero clarificás, después modelás, después profundizás. Al final encontrás un diagrama de arquitectura completo y los errores que más eliminan candidatos.


Parte 1 — Antes de dibujar: clarificación de requisitos

Nunca arranques a dibujar cuadros hasta entender qué sistema estás diseñando. En pagos hay diferencias enormes entre un sistema de pagos entre pares (P2P), una pasarela de e-commerce, un procesador interno de nómina o un exchange de criptomonedas.

Pregunta 1: ¿Qué tipo de sistema de pagos te piden diseñar?

Por qué importa: "sistema de pagos" puede significar cosas muy distintas. Las respuestas cambian el modelo de datos, la latencia requerida y los actores involucrados.

Ejemplos concretos:

  • PSP (Payment Service Provider): intermediario entre el comercio y los bancos adquirentes. Ejemplos: Stripe, dLocal, Mercado Pago.
  • Wallet interna: saldo dentro de una app, movimientos entre usuarios (tipo PayPal, Mercado Libre, Rappi).
  • Procesador de nómina: pagos en lote, alta consistencia, baja urgencia.
  • Pasarela de checkout: captura de tarjeta y tokenización para un e-commerce.

En la entrevista, si no te especifican, preguntá: *"¿Estamos diseñando la pasarela que expone el comercio, el procesador interno de cuentas, o ambos?"*


Pregunta 2: ¿Cuál es el volumen esperado de transacciones por segundo (TPS)?

Por qué importa: define si necesitás sharding, colas asíncronas y qué motor de base de datos aplica.

Referencias reales:

  • Visa: ~1.700 TPS promedio, pico de ~65.000 TPS.
  • Stripe: no publicado, pero maneja millones de negocios globales.
  • Mercado Pago: pico en Hot Sale Argentina > 10.000 TPS.

Para una entrevista, un número razonable es: 500–5.000 TPS para un sistema regional. Si el entrevistador dice "escala global", diseñás para 50.000 TPS.


Pregunta 3: ¿Cuál es la latencia aceptable para confirmar un pago?

Por qué importa: pagos síncronos (respuesta en <500ms) vs asíncronos (respuesta en segundos o minutos) cambian todo el flujo.

  • Tarjeta de crédito: el usuario espera una respuesta en <2 segundos antes de dudar de que algo salió mal.
  • Transferencia bancaria (ACH, CBU): puede tomar minutos o días.
  • Crypto: confirmaciones pueden tomar 10–60 minutos.

Clarificá: *"¿El pago necesita confirmarse sincrónicamente antes de que el usuario vea 'pago aprobado', o podemos usar un flujo asíncrono con notificación posterior?"*


Pregunta 4: ¿Qué monedas y regiones hay que soportar?

Por qué importa: determina complejidad de conversión, PSPs locales, y regulaciones.

  • Moneda única: modelo simple, sin conversión.
  • Multi-moneda: necesitás tipo de cambio (¿fijado al momento del pago? ¿al momento del settlement?), y manejar riesgo FX.
  • LATAM específicamente: BRL, ARS, COP, MXN, cada uno con su PSP local preferido (Mercado Pago, dLocal, OXXO, Rappi Pay).

Pregunta 5: ¿Qué operaciones básicas tiene que soportar el sistema?

Lista mínima que debés cubrir:

  • charge / authorize + capture
  • refund
  • chargeback
  • payout (pagar al comercio o al usuario)
  • dispute

Pregunta 6: ¿Qué nivel de disponibilidad se espera?

SLA típicos:

  • 99.9% = ~8.7h de downtime al año.
  • 99.99% = ~52 minutos al año.
  • 99.999% = ~5 minutos al año.

Para sistemas de pagos productivos, el target mínimo es 99.99%. Esto implica diseñar con redundancia, failover automático y circuit breakers.


Parte 2 — Modelo de datos

Un modelo de datos mal pensado es el error más difícil de corregir en producción. En pagos, hay tres abstracciones centrales.

Pregunta 7: ¿Cuál es la diferencia entre una cuenta, una transacción y un ledger?

  • Cuenta (Account): representa a un actor (usuario, comercio, sistema). Tiene un balance calculado o materializado.
  • Transacción (Transaction): evento atómico de movimiento de dinero. Es inmutable: nunca se modifica, solo se cancela con otra transacción inversa.
  • Ledger (libro mayor): registro de todos los asientos contables. Implementa contabilidad de doble entrada.
sql
-- Tabla de cuentas
CREATE TABLE accounts (
  id          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  owner_id    UUID NOT NULL REFERENCES users(id),
  currency    CHAR(3) NOT NULL,  -- ISO 4217: ARS, USD, BRL
  type        TEXT NOT NULL,     -- 'user', 'merchant', 'system'
  created_at  TIMESTAMPTZ DEFAULT NOW()
);

-- Tabla de transacciones (inmutable)
CREATE TABLE transactions (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  idempotency_key TEXT UNIQUE NOT NULL,
  status          TEXT NOT NULL DEFAULT 'pending',
  -- 'pending' | 'processing' | 'completed' | 'failed' | 'reversed'
  amount          BIGINT NOT NULL,  -- en centavos/unidad mínima
  currency        CHAR(3) NOT NULL,
  from_account_id UUID REFERENCES accounts(id),
  to_account_id   UUID REFERENCES accounts(id),
  type            TEXT NOT NULL,  -- 'charge' | 'refund' | 'payout' | 'fee'
  metadata        JSONB,
  created_at      TIMESTAMPTZ DEFAULT NOW(),
  updated_at      TIMESTAMPTZ DEFAULT NOW()
);

-- Ledger (doble entrada — jamás se modifica, solo inserta)
CREATE TABLE ledger_entries (
  id              UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  transaction_id  UUID NOT NULL REFERENCES transactions(id),
  account_id      UUID NOT NULL REFERENCES accounts(id),
  amount          BIGINT NOT NULL,  -- positivo = crédito, negativo = débito
  balance_after   BIGINT NOT NULL,  -- snapshot del balance en ese momento
  created_at      TIMESTAMPTZ DEFAULT NOW()
);

Pregunta 8: ¿Por qué el monto se guarda en centavos (enteros) y no en decimales?

Porque los tipos FLOAT y DOUBLE tienen errores de redondeo en base 2. 0.1 + 0.2 en floats no es exactamente 0.3. En pagos eso es inaceptable.

Regla: siempre almacená en la unidad mínima de la moneda:

  • USD, ARS, EUR: centavos (1 USD = 100 unidades).
  • JPY: la unidad mínima es el yen (sin decimales).
  • BTC: satoshis (1 BTC = 100.000.000 satoshis).
python
# Mal — nunca hagas esto
amount = 10.99  # float

# Bien
amount_cents = 1099  # int, siempre

# Conversión al mostrar
def format_amount(cents: int, currency: str) -> str:
    if currency in ("JPY", "KRW"):
        return f"{cents}"
    return f"{cents / 100:.2f}"

Pregunta 9: ¿Qué es la contabilidad de doble entrada y por qué es obligatoria en pagos?

En contabilidad de doble entrada, cada movimiento de dinero genera dos asientos: uno que debita una cuenta y otro que acredita otra. La suma de todos los asientos siempre es cero. Esto permite detectar inconsistencias: si los debits no igualan los crédits, hay un bug o fraude.

sql
-- Un pago de $100 del usuario A al comercio B
-- genera dos asientos:
INSERT INTO ledger_entries (transaction_id, account_id, amount, balance_after)
VALUES
  (txn_id, account_user_a,    -10000, 50000),  -- débito: le salen $100
  (txn_id, account_merchant_b, 10000, 20000);  -- crédito: le entran $100

-- Invariante: SUM(amount) OVER (txn_id) = 0
SELECT SUM(amount) FROM ledger_entries WHERE transaction_id = txn_id;
-- debe retornar 0

Parte 3 — Idempotencia

Esta es la parte donde más candidatos se caen. La idempotencia no es un nice-to-have: es el mecanismo que evita que le cobrés dos veces a un usuario porque la red falló y el cliente reintentó.

Pregunta 10: ¿Qué es idempotencia en el contexto de pagos?

Una operación es idempotente si ejecutarla una vez o N veces produce el mismo resultado. En pagos: si el cliente envía la misma solicitud de cobro dos veces (por timeout, retry, bug), el sistema debe ejecutar el cobro una sola vez.

Pregunta 11: ¿Cómo implementás idempotencia en una API de pagos?

El mecanismo estándar es una idempotency key: un UUID que el cliente genera y envía en el header. El servidor lo persiste junto con la respuesta. Si recibe la misma key otra vez, devuelve la respuesta cacheada sin ejecutar nada.

python
# Cliente: genera la key ANTES de enviar
import uuid

idempotency_key = str(uuid.uuid4())  # se genera una sola vez para este intento

response = requests.post(
    "https://api.payments.com/v1/charges",
    headers={"Idempotency-Key": idempotency_key},
    json={"amount": 5000, "currency": "ARS", "source": "tok_123"}
)
python
# Servidor: lógica de idempotencia
from db import get_idempotency_record, save_idempotency_record
from fastapi import Header, HTTPException

async def create_charge(
    payload: ChargeRequest,
    idempotency_key: str = Header(...)
):
    # 1. Buscar si ya existe
    existing = await get_idempotency_record(idempotency_key)
    if existing:
        return existing.response  # devolvés la misma respuesta, sin reejecutar

    # 2. Ejecutar el cobro (dentro de una transacción DB)
    async with db.transaction():
        charge = await process_charge(payload)
        # 3. Guardar key + respuesta (atómicamente con el cobro)
        await save_idempotency_record(
            key=idempotency_key,
            response=charge.to_dict(),
            expires_at=datetime.now() + timedelta(days=7)
        )

    return charge
sql
-- Tabla de idempotencia
CREATE TABLE idempotency_records (
  key         TEXT PRIMARY KEY,
  response    JSONB NOT NULL,
  created_at  TIMESTAMPTZ DEFAULT NOW(),
  expires_at  TIMESTAMPTZ NOT NULL
);

-- El UNIQUE en key garantiza que dos requests concurrentes
-- con la misma key no creen dos registros

Pregunta 12: ¿Qué pasa si dos requests concurrentes usan la misma idempotency key?

Es el caso de race condition. Se resuelve con un lock a nivel base de datos o con INSERT ... ON CONFLICT DO NOTHING:

sql
-- INSERT atómico: si ya existe, no hace nada
INSERT INTO idempotency_records (key, response, expires_at)
VALUES ($1, $2, $3)
ON CONFLICT (key) DO NOTHING
RETURNING *;

-- Si RETURNING no devuelve filas, es que ya existía → fetch y devolvé

Parte 4 — Flujo de una transacción

Pregunta 13: ¿Cuál es el flujo completo de un pago con tarjeta de crédito?

Cliente → API Gateway → Payment Service
                              │
                    ┌─────────▼──────────┐
                    │  Validar & crear   │
                    │  transacción       │
                    │  (status=pending)  │
                    └─────────┬──────────┘
                              │
                    ┌─────────▼──────────┐
                    │  Publicar evento   │
                    │  en Kafka          │
                    └─────────┬──────────┘
                              │
                    ┌─────────▼──────────┐
                    │  Payment Worker    │
                    │  consume el evento │
                    └─────────┬──────────┘
                              │
                    ┌─────────▼──────────┐
                    │  Llamar al PSP     │
                    │  (Stripe / dLocal) │
                    └──┬──────────────┬──┘
                       │              │
                  Éxito │         Fallo│
                       │              │
              status=  │    status=   │
              completed │    failed   │
                       │              │
                    ┌──▼──────────────▼──┐
                    │  Actualizar ledger  │
                    │  Notificar webhook  │
                    └────────────────────┘

Pregunta 14: ¿Cuál es la diferencia entre `authorize` y `capture`?

  • Authorize: reserva los fondos en la tarjeta del usuario. El banco "bloquea" el monto. El dinero todavía no se mueve.
  • Capture: ejecuta el movimiento real. Generalmente hasta 7 días después de la autorización.

Esto es útil en e-commerce: autorizás cuando el usuario confirma la compra, capturás cuando el producto se despacha.

python
# Paso 1: autorizar (reservar fondos)
auth = stripe.PaymentIntent.create(
    amount=5000,
    currency="ars",
    capture_method="manual",  # no captura automáticamente
    payment_method="pm_card_visa",
    confirm=True
)
# auth.status == "requires_capture"

# Paso 2: capturar (cuando despachás el pedido)
stripe.PaymentIntent.capture(auth.id)

Parte 5 — Colas asíncronas con Kafka

Pregunta 15: ¿Por qué usás Kafka en un sistema de pagos y no llamadas síncronas al PSP?

Tres razones principales:

  1. 1Desacoplamiento de latencia: la llamada al PSP puede tardar 200ms–2s. Si la hacés síncrona, bloqueás el hilo del servidor. Con Kafka, el API responde en <50ms y el worker procesa en background.
  1. 2Reintentos con backoff: si el PSP falla, Kafka retiene el mensaje. Podés reintentar sin perder la transacción.
  1. 3Throughput: Kafka maneja millones de mensajes por segundo. El PSP tiene sus propios rate limits; Kafka actúa como buffer.

Pregunta 16: ¿Cómo estructurás los topics de Kafka para pagos?

payments.transactions.created     → payment-workers (procesan con PSP)
payments.transactions.completed   → ledger-workers (asientan en ledger)
payments.transactions.failed      → retry-workers / alerting
payments.webhooks.outbound        → webhook-dispatcher (notifica al comercio)
payments.reconciliation.daily     → reconciliation-worker
python
# Productor: cuando se crea la transacción
from kafka import KafkaProducer
import json

producer = KafkaProducer(
    bootstrap_servers="kafka:9092",
    value_serializer=lambda v: json.dumps(v).encode("utf-8")
)

producer.send(
    "payments.transactions.created",
    key=transaction_id.encode(),  # key = partition determinista por txn
    value={
        "transaction_id": str(transaction_id),
        "amount": 5000,
        "currency": "ARS",
        "idempotency_key": idempotency_key,
        "payment_method_id": payment_method_id,
        "created_at": created_at.isoformat()
    }
)
python
# Consumidor: worker que llama al PSP
from kafka import KafkaConsumer

consumer = KafkaConsumer(
    "payments.transactions.created",
    group_id="payment-workers",
    bootstrap_servers="kafka:9092",
    enable_auto_commit=False  # commit manual después de procesar
)

for message in consumer:
    txn = json.loads(message.value)
    try:
        result = call_psp(txn)
        update_transaction_status(txn["transaction_id"], "completed", result)
        consumer.commit()  # solo comiteás si todo salió bien
    except PSPError as e:
        if e.retryable:
            # No commitear → Kafka va a redeliverar
            pass
        else:
            update_transaction_status(txn["transaction_id"], "failed", str(e))
            consumer.commit()

Pregunta 17: ¿Qué pasa si el worker procesa el mensaje pero falla antes de hacer commit?

Kafka va a redeliverar el mensaje. Por eso el procesamiento debe ser idempotente: si el PSP ya cobró y el worker recibe el mismo mensaje de nuevo, tiene que detectar que ya existe y no cobrar dos veces.

python
def process_transaction(txn: dict):
    # Verificar si ya fue procesada (idempotencia a nivel worker)
    existing = db.query(
        "SELECT status FROM transactions WHERE id = %s",
        txn["transaction_id"]
    )
    if existing.status in ("completed", "failed"):
        return  # ya procesada, ignorar

    # Procesar solo si está en pending
    result = psp_client.charge(
        amount=txn["amount"],
        idempotency_key=txn["idempotency_key"]  # pasamos la key al PSP también
    )
    update_transaction_status(txn["transaction_id"], "completed", result)

Parte 6 — Consistencia

Pregunta 18: ¿Cuándo usás consistencia eventual vs consistencia fuerte en pagos?

Esta es una pregunta de trade-off que los entrevistadores hacen para ver si entendés los límites del sistema.

Consistencia fuerte (Strong Consistency):

  • Requerida cuando el dato afecta directamente la autorización del pago.
  • Ejemplo: el balance disponible del usuario. Si permitís consistencia eventual, podés autorizar pagos por más del saldo disponible (overdraft no intencional).
  • Implementación: transacciones ACID en PostgreSQL con SELECT ... FOR UPDATE.

Consistencia eventual (Eventual Consistency):

  • Aceptable para datos secundarios o de reporting.
  • Ejemplo: el dashboard de analytics del comercio, los totales del día, las notificaciones de webhook.
  • Implementación: Kafka + procesamiento asíncrono + vistas materializadas.
sql
-- Ejemplo de débito con consistencia fuerte
-- Usa FOR UPDATE para lockear la fila durante la transacción
BEGIN;

SELECT balance
FROM accounts
WHERE id = $1
FOR UPDATE;  -- lock exclusivo

-- Verificar saldo suficiente
-- Si balance < amount → ROLLBACK y retornar error

UPDATE accounts
SET balance = balance - $amount
WHERE id = $1;

INSERT INTO ledger_entries (transaction_id, account_id, amount, balance_after)
VALUES ($txn_id, $1, -$amount, balance - $amount);

COMMIT;

Pregunta 19: ¿Qué es el problema del "double-spend" y cómo lo prevenís?

El double-spend ocurre cuando el mismo saldo se usa dos veces en transacciones concurrentes. Sin locking, dos transacciones simultáneas pueden leer el mismo balance y ambas aprobar el gasto.

sql
-- Sin protección (MAL):
-- Thread A lee balance = 100
-- Thread B lee balance = 100
-- Thread A escribe balance = 0 (cobró 100)
-- Thread B escribe balance = 0 (cobró 100 también — ¡pero ya no había!)

-- Con SELECT FOR UPDATE (BIEN):
BEGIN;
SELECT balance FROM accounts WHERE id = $user_id FOR UPDATE;
-- Thread B queda bloqueado hasta que Thread A haga COMMIT o ROLLBACK
UPDATE accounts SET balance = balance - $amount
WHERE id = $user_id AND balance >= $amount;
-- Verificar affected rows = 1, si no → ROLLBACK
COMMIT;

Parte 7 — Integración con PSPs

Pregunta 20: ¿Cómo diseñás la capa de integración con múltiples PSPs?

Definís una abstracción (PaymentGateway) y una implementación por PSP. El selector elige el PSP según moneda, país, método de pago o fallback.

python
from abc import ABC, abstractmethod
from dataclasses import dataclass

@dataclass
class ChargeResult:
    psp_transaction_id: str
    status: str  # 'succeeded' | 'failed' | 'pending'
    error_code: str | None = None

class PaymentGateway(ABC):
    @abstractmethod
    async def charge(
        self,
        amount: int,
        currency: str,
        payment_method_token: str,
        idempotency_key: str
    ) -> ChargeResult:
        ...

    @abstractmethod
    async def refund(self, psp_transaction_id: str, amount: int) -> ChargeResult:
        ...

class StripeGateway(PaymentGateway):
    async def charge(self, amount, currency, payment_method_token, idempotency_key):
        import stripe
        intent = stripe.PaymentIntent.create(
            amount=amount,
            currency=currency.lower(),
            payment_method=payment_method_token,
            confirm=True,
            idempotency_key=idempotency_key
        )
        return ChargeResult(
            psp_transaction_id=intent.id,
            status="succeeded" if intent.status == "succeeded" else "failed"
        )

class DLocalGateway(PaymentGateway):
    """Para LATAM: Brasil, México, Argentina, Colombia, etc."""
    async def charge(self, amount, currency, payment_method_token, idempotency_key):
        # dLocal maneja métodos locales: OXXO, Boleto, PSE, Rapipago
        response = await self.client.post(
            "/secure_payments",
            json={
                "amount": amount / 100,  # dLocal usa decimales
                "currency": currency,
                "payment_method_id": payment_method_token,
                "order_id": idempotency_key
            }
        )
        return ChargeResult(
            psp_transaction_id=response["id"],
            status=response["status"]
        )

class MercadoPagoGateway(PaymentGateway):
    """Para Argentina y Brasil principalmente"""
    async def charge(self, amount, currency, payment_method_token, idempotency_key):
        response = await self.client.post(
            "/v1/payments",
            headers={"X-Idempotency-Key": idempotency_key},
            json={
                "transaction_amount": amount / 100,
                "token": payment_method_token,
                "installments": 1
            }
        )
        return ChargeResult(
            psp_transaction_id=str(response["id"]),
            status="succeeded" if response["status"] == "approved" else "failed"
        )

# Selector de PSP
class PSPRouter:
    def select(self, currency: str, country: str, method: str) -> PaymentGateway:
        if country in ("AR", "BR", "MX", "CO") and method in ("oxxo", "boleto", "pse"):
            return DLocalGateway()
        if country == "AR" and method == "mercadopago":
            return MercadoPagoGateway()
        return StripeGateway()  # fallback para tarjetas internacionales

Pregunta 21: ¿Cómo manejás los webhooks que recibís del PSP?

Los PSPs notifican eventos asíncronamente (pago completado, disputa abierta, chargeback). Estas notificaciones pueden llegar fuera de orden o duplicadas.

python
# Endpoint de webhook
from fastapi import Request, HTTPException
import hmac, hashlib

async def stripe_webhook(request: Request):
    payload = await request.body()
    sig_header = request.headers.get("Stripe-Signature")

    # 1. Verificar firma (evitar webhooks falsos)
    try:
        event = stripe.Webhook.construct_event(
            payload, sig_header, STRIPE_WEBHOOK_SECRET
        )
    except stripe.error.SignatureVerificationError:
        raise HTTPException(400, "Invalid signature")

    # 2. Idempotencia: ignorar eventos ya procesados
    if await event_already_processed(event["id"]):
        return {"status": "already_processed"}

    # 3. Publicar en Kafka para procesamiento asíncrono
    # (no procesamos directamente en el endpoint — queremos responder rápido al PSP)
    await kafka_producer.send(
        "payments.psp.events",
        key=event["id"].encode(),
        value={
            "event_id": event["id"],
            "type": event["type"],
            "data": event["data"],
            "received_at": datetime.now().isoformat()
        }
    )

    # 4. Responder 200 rápido (Stripe reintenta si no recibe 200 en <30s)
    return {"status": "queued"}

Parte 8 — Reintentos y manejo de fallos

Pregunta 22: ¿Cómo diseñás la lógica de reintentos para fallos del PSP?

Diferenciá entre errores retryable y no-retryable antes de reintentar ciegamente.

python
class PSPError(Exception):
    def __init__(self, code: str, message: str, retryable: bool):
        self.code = code
        self.retryable = retryable
        super().__init__(message)

# Errores NO retryable (reintentar no va a ayudar)
NON_RETRYABLE_CODES = {
    "card_declined",           # el banco rechazó la tarjeta
    "insufficient_funds",      # no tiene saldo
    "card_expired",            # tarjeta vencida
    "invalid_card_number",     # número inválido
    "do_not_honor",            # el banco dice que no
}

# Errores retryable (pueden resolverse solos)
RETRYABLE_CODES = {
    "rate_limited",            # PSP con rate limit
    "timeout",                 # timeout de red
    "service_unavailable",     # PSP caído transitoriamente
    "processing_error",        # error interno del PSP
}

async def charge_with_retry(
    gateway: PaymentGateway,
    payload: dict,
    max_attempts: int = 3
) -> ChargeResult:
    base_delay = 1.0  # segundos

    for attempt in range(max_attempts):
        try:
            return await gateway.charge(**payload)
        except PSPError as e:
            if not e.retryable:
                raise  # no reintentar

            if attempt == max_attempts - 1:
                raise  # agotamos reintentos

            # Exponential backoff con jitter
            delay = base_delay * (2 ** attempt) + random.uniform(0, 0.5)
            await asyncio.sleep(delay)

Pregunta 23: ¿Qué es el patrón Outbox y por qué es importante en pagos?

El problema del Outbox resuelve el caso donde necesitás actualizar la DB y publicar en Kafka de forma atómica. Sin Outbox, podés tener:

  • Guardar en DB ✓ → publicar en Kafka ✗ → transacción queda en pending para siempre
  • Guardar en DB ✗ → publicar en Kafka ✓ → hay un evento en Kafka para una transacción que no existe
sql
-- Tabla de outbox (misma base de datos que las transacciones)
CREATE TABLE outbox_messages (
  id           UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  topic        TEXT NOT NULL,
  key          TEXT,
  payload      JSONB NOT NULL,
  created_at   TIMESTAMPTZ DEFAULT NOW(),
  published_at TIMESTAMPTZ  -- null = pendiente de publicar
);
python
# En la misma transacción de DB, insertás en outbox
async def create_charge(payload: dict):
    async with db.transaction():
        # 1. Crear la transacción
        txn = await db.execute(
            "INSERT INTO transactions (...) VALUES (...) RETURNING *",
            payload
        )
        # 2. Insertar en outbox (mismo TX — o ambos persisten o ninguno)
        await db.execute(
            "INSERT INTO outbox_messages (topic, key, payload) VALUES ($1, $2, $3)",
            "payments.transactions.created",
            str(txn.id),
            txn.to_dict()
        )
    # Después del commit, un poller lee el outbox y publica en Kafka

# Poller que lee outbox y publica (corre en background)
async def outbox_poller():
    while True:
        messages = await db.fetchall(
            "SELECT * FROM outbox_messages WHERE published_at IS NULL LIMIT 100"
        )
        for msg in messages:
            await kafka_producer.send(msg.topic, key=msg.key, value=msg.payload)
            await db.execute(
                "UPDATE outbox_messages SET published_at = NOW() WHERE id = $1",
                msg.id
            )
        await asyncio.sleep(0.1)

Parte 9 — Reconciliación

Pregunta 24: ¿Qué es la reconciliación y cuándo la necesitás?

La reconciliación es el proceso de comparar los registros internos del sistema con los del PSP para detectar discrepancias. Es obligatoria en cualquier sistema de pagos productivo.

Escenarios que la reconciliación detecta:

  • Pago que el PSP marcó como completado pero que en el sistema interno quedó en pending.
  • Refund procesado por el PSP sin que el sistema lo haya solicitado.
  • Fee cobrado por el PSP que no coincide con lo esperado.
python
# Reconciliation worker (corre una vez al día)
async def daily_reconciliation(date: date):
    # 1. Obtener todos los pagos del día desde el PSP
    psp_transactions = await stripe_client.list_charges(
        created_from=datetime.combine(date, time.min),
        created_to=datetime.combine(date, time.max)
    )

    # 2. Obtener los registros internos del mismo período
    internal_transactions = await db.fetchall(
        """SELECT id, psp_transaction_id, amount, status
           FROM transactions
           WHERE created_at::date = $1""",
        date
    )

    internal_map = {t.psp_transaction_id: t for t in internal_transactions}

    discrepancies = []

    for psp_txn in psp_transactions:
        internal = internal_map.get(psp_txn.id)

        if not internal:
            discrepancies.append({
                "type": "missing_internal",
                "psp_transaction_id": psp_txn.id,
                "amount": psp_txn.amount
            })
            continue

        if internal.amount != psp_txn.amount:
            discrepancies.append({
                "type": "amount_mismatch",
                "internal_id": str(internal.id),
                "psp_transaction_id": psp_txn.id,
                "internal_amount": internal.amount,
                "psp_amount": psp_txn.amount
            })

        if internal.status == "pending" and psp_txn.status == "succeeded":
            discrepancies.append({
                "type": "status_mismatch",
                "internal_id": str(internal.id),
                "action": "mark_completed"
            })

    # 3. Persistir discrepancias para revisión manual o auto-resolución
    if discrepancies:
        await save_reconciliation_report(date, discrepancies)
        await alert_ops_team(f"{len(discrepancies)} discrepancias en {date}")

Parte 10 — Seguridad y PCI DSS

Pregunta 25: ¿Qué es PCI DSS y cómo afecta el diseño?

PCI DSS (Payment Card Industry Data Security Standard) es el conjunto de requisitos de seguridad para cualquier sistema que procese, almacene o transmita datos de tarjetas de crédito.

Niveles de PCI DSS:

  • SAQ A: el sistema nunca toca datos de tarjetas (usa iframes del PSP). El más fácil.
  • SAQ D: el sistema toca datos de tarjetas. Requiere auditoría anual por un QSA certificado.

Diseño para minimizar el scope PCI:

El objetivo es que tu sistema nunca toque el PAN (Primary Account Number — el número de la tarjeta). Para eso:

[Cliente]
    │  (tarjeta ingresada en iframe del PSP)
    │
[PSP (Stripe/dLocal)]
    │  (devuelve un token, ej: "tok_1AbcDEF...")
    │
[Tu sistema]
    │  (solo almacena el token, nunca el PAN)
    ▼
[Base de datos — solo tokens]
python
# NUNCA hagas esto
payment_method = {
    "card_number": "4242424242424242",  # ¡NUNCA almacenes esto!
    "cvv": "123",                        # ¡NUNCA!
    "expiry": "12/26"                    # ¡NUNCA!
}

# Siempre trabaja con tokens del PSP
payment_method = {
    "token": "pm_1AbcDEF23GhIJKL",  # token opaco del PSP — esto sí
    "last4": "4242",                  # solo los últimos 4 para display
    "brand": "visa",
    "exp_month": 12,
    "exp_year": 2026
}

Pregunta 26: ¿Qué otras medidas de seguridad implementás?

  1. 1Encriptación en tránsito: TLS 1.2+ en todos los endpoints.
  2. 2Encriptación en reposo: datos sensibles en DB con AES-256 (manejado por el cloud provider o Vault de HashiCorp).
  3. 3Autenticación de webhooks: siempre verificar la firma del PSP antes de procesar.
  4. 4Rate limiting: limitar intentos de pago por usuario/IP para prevenir carding attacks.
  5. 5Fraud detection: reglas básicas (monto inusual, múltiples países en corto tiempo) o integración con herramientas como Stripe Radar, Sift, Kount.
python
# Rate limiting básico para prevenir ataques de carding
from redis import Redis

redis = Redis()

async def check_rate_limit(user_id: str, ip: str) -> bool:
    # Máximo 5 intentos de pago por IP por hora
    ip_key = f"payment_attempts:ip:{ip}"
    ip_count = redis.incr(ip_key)
    if ip_count == 1:
        redis.expire(ip_key, 3600)
    if ip_count > 5:
        return False  # bloquear

    # Máximo 3 tarjetas distintas por usuario por día
    card_key = f"payment_attempts:user:{user_id}:date:{date.today()}"
    user_count = redis.incr(card_key)
    if user_count == 1:
        redis.expire(card_key, 86400)
    if user_count > 10:
        return False

    return True

Parte 11 — Diagrama de arquitectura completo

Pregunta 27: ¿Podés dibujar el diagrama de arquitectura del sistema?

┌──────────────────────────────────────────────────────────┐
│                      CLIENTE / SDK                        │
│   (iframe PSP para captura de tarjeta — fuera del scope) │
└────────────────────────┬─────────────────────────────────┘
                         │ HTTPS + Idempotency-Key
                         ▼
┌──────────────────────────────────────────────────────────┐
│                    API GATEWAY / BFF                      │
│   Rate Limiting · Auth (JWT) · TLS termination           │
└────────────────────────┬─────────────────────────────────┘
                         │
          ┌──────────────┼──────────────┐
          ▼              ▼              ▼
┌─────────────┐  ┌─────────────┐  ┌──────────────┐
│  Payments   │  │  Accounts   │  │  Webhooks    │
│  Service    │  │  Service    │  │  Service     │
└──────┬──────┘  └──────┬──────┘  └──────┬───────┘
       │                │                │
       └────────────────┼────────────────┘
                        │
                        ▼
       ┌────────────────────────────────┐
       │         Kafka Cluster          │
       │  payments.transactions.created  │
       │  payments.transactions.completed│
       │  payments.psp.events           │
       │  payments.webhooks.outbound    │
       └────────────────┬───────────────┘
                        │
          ┌─────────────┼─────────────┐
          ▼             ▼             ▼
┌──────────────┐ ┌────────────┐ ┌──────────────┐
│  Payment     │ │  Ledger    │ │  Webhook     │
│  Workers     │ │  Workers   │ │  Dispatcher  │
│  (x10)       │ │  (x5)      │ │  (x3)        │
└──────┬───────┘ └──────┬─────┘ └──────────────┘
       │                │
       ▼                ▼
┌──────────────────────────────────────┐
│           PSP Router                 │
│  ┌─────────┐ ┌────────┐ ┌─────────┐ │
│  │ Stripe  │ │ dLocal │ │MercPago │ │
│  └─────────┘ └────────┘ └─────────┘ │
└──────────────────────────────────────┘
       │
       ▼
┌──────────────────────────────────────┐
│          Databases / Storage         │
│  ┌────────────────┐ ┌─────────────┐  │
│  │ PostgreSQL (RW)│ │ Redis Cache │  │
│  │ Primary + 2    │ │ (idempotency│  │
│  │ Read Replicas  │ │  / sessions)│  │
│  └────────────────┘ └─────────────┘  │
│  ┌─────────────────────────────────┐  │
│  │ ClickHouse / BigQuery (analytics│  │
│  │ + reconciliation reports)       │  │
│  └─────────────────────────────────┘  │
└──────────────────────────────────────┘
       │
       ▼
┌──────────────────────────────────────┐
│          Monitoring & Alerting        │
│  Prometheus · Grafana · PagerDuty    │
│  Distributed Tracing (Jaeger/OTEL)   │
└──────────────────────────────────────┘

Parte 12 — Monitoreo y escalabilidad

Pregunta 28: ¿Qué métricas monitoreas en un sistema de pagos?

Métricas de negocio (las más importantes):

  • payment_success_rate — % de pagos que terminan en completed. Si baja del 95%, hay un problema.
  • payment_volume_per_minute — volumen en dinero. Pico inusual puede ser fraude.
  • average_payment_latency_p99 — percentil 99 de latencia. Los outliers importan.
  • chargeback_rate — % de pagos revertidos. Umbral de Visa/Mastercard: <1%.

Métricas técnicas:

  • kafka_consumer_lag — si el lag crece, los workers no alcanzan a procesar.
  • db_lock_wait_time — si aumenta, hay contención en las transacciones.
  • psp_error_rate_by_code — qué tipos de error vienen del PSP.
python
# Instrumentación con Prometheus
from prometheus_client import Counter, Histogram, Gauge

payment_attempts = Counter(
    "payment_attempts_total",
    "Total de intentos de pago",
    ["psp", "currency", "status"]
)

payment_latency = Histogram(
    "payment_processing_seconds",
    "Latencia de procesamiento de pagos",
    ["psp"],
    buckets=[0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0]
)

kafka_consumer_lag = Gauge(
    "kafka_consumer_lag",
    "Mensajes pendientes en Kafka",
    ["topic", "consumer_group"]
)

# Uso en el código
async def process_payment(txn: dict):
    psp_name = get_psp_for_transaction(txn)

    with payment_latency.labels(psp=psp_name).time():
        try:
            result = await charge(txn)
            payment_attempts.labels(
                psp=psp_name,
                currency=txn["currency"],
                status="success"
            ).inc()
        except PSPError as e:
            payment_attempts.labels(
                psp=psp_name,
                currency=txn["currency"],
                status=e.code
            ).inc()
            raise

Pregunta 29: ¿Cómo escalás el sistema para manejar picos de tráfico?

Escalado horizontal de los workers:

  • Los payment workers son stateless, pueden escalar a N instancias.
  • Kafka redistribuye las particiones automáticamente.
  • Regla práctica: 1 worker por partición de Kafka.

Sharding de la base de datos:

  • Sharding por account_id o por región.
  • Cada shard es un PostgreSQL independiente con sus réplicas.
python
# Selección de shard por account_id
def get_shard_for_account(account_id: str) -> str:
    # Consistent hashing o módulo simple
    shard_index = int(account_id.replace("-", ""), 16) % NUM_SHARDS
    return f"db-shard-{shard_index}"

Caching:

  • Datos de cuenta leídos frecuentemente → Redis con TTL corto (30s).
  • Resultados de idempotencia → Redis con TTL de 7 días.

Pregunta 30: ¿Cómo manejás el settlement y los payouts al comercio?

El settlement es el proceso por el cual el PSP transfiere el dinero capturado a la cuenta bancaria del comercio. Normalmente ocurre en T+1 o T+2 (un o dos días hábiles después).

python
# Job de payout nocturno
async def process_daily_settlements(settlement_date: date):
    # 1. Calcular el monto neto por comercio
    # (ventas - refunds - fees del día anterior)
    settlements = await db.fetchall("""
        SELECT
            merchant_id,
            SUM(CASE WHEN type = 'charge' THEN amount ELSE 0 END) as gross_volume,
            SUM(CASE WHEN type = 'refund' THEN amount ELSE 0 END) as refunds,
            SUM(CASE WHEN type = 'fee' THEN amount ELSE 0 END) as fees
        FROM transactions
        WHERE created_at::date = $1 AND status = 'completed'
        GROUP BY merchant_id
    """, settlement_date - timedelta(days=1))

    for s in settlements:
        net_amount = s.gross_volume - s.refunds - s.fees
        if net_amount > 0:
            await create_payout(
                merchant_id=s.merchant_id,
                amount=net_amount,
                settlement_date=settlement_date
            )

Parte 13 — Preguntas adicionales de profundidad

Pregunta 31: ¿Cómo implementás chargebacks?

Un chargeback es una reversión iniciada por el banco del usuario, no por el comercio. El dinero se devuelve al usuario y el comercio pierde el monto más una penalidad.

El sistema debe:

  1. 1Recibir el webhook del PSP notificando la disputa.
  2. 2Suspender el monto del balance del comercio (no está disponible para payout).
  3. 3Notificar al comercio para que presente evidencia.
  4. 4Registrar el resultado (ganó/perdió la disputa).
  5. 5Ajustar el balance según el resultado.

Pregunta 32: ¿Qué diferencia hay entre un refund y un chargeback?

| | Refund | Chargeback |

|---|---|---|

| Iniciado por | El comercio | El banco/usuario |

| Tiempo | Inmediato | Puede tardar semanas |

| Penalidad al comercio | Ninguna | $15–$100 por disputa |

| Controlado por | El comercio | El banco |

| Reversible | No | Sí (el comercio puede presentar evidencia) |

Pregunta 33: ¿Cómo diseñás el manejo de multi-moneda?

python
@dataclass
class ExchangeRate:
    from_currency: str
    to_currency: str
    rate: Decimal
    fetched_at: datetime
    source: str  # 'fixer.io' | 'xe.com' | 'central_bank'

# Siempre almacenar el tipo de cambio en el momento del pago
async def create_cross_currency_charge(
    amount: int,           # en moneda del usuario
    user_currency: str,    # ej: "ARS"
    settlement_currency: str  # ej: "USD" — en qué moneda se asienta
) -> dict:
    rate = await get_exchange_rate(user_currency, settlement_currency)

    # Convertir
    settled_amount = int(amount / rate.rate)  # en settlement_currency

    # Guardar ambos montos y el rate — crucial para auditoría
    return {
        "charged_amount": amount,
        "charged_currency": user_currency,
        "settled_amount": settled_amount,
        "settled_currency": settlement_currency,
        "exchange_rate": float(rate.rate),
        "exchange_rate_at": rate.fetched_at.isoformat()
    }

Pregunta 34: ¿Qué pasa si el PSP devuelve un estado ambiguo?

A veces el PSP devuelve status: "pending" o directamente un timeout sin respuesta. Esto se llama "estado desconocido" y es uno de los casos más delicados.

Estrategia:

  1. 1Guardar el estado como processing (distinto de pending y completed).
  2. 2Publicar un evento para que un worker consulte el estado al PSP después de N segundos.
  3. 3Si después de K intentos sigue ambiguo, escalar a revisión manual.
python
async def handle_unknown_psp_status(transaction_id: str):
    """Consulta el estado real de la transacción al PSP"""
    txn = await get_transaction(transaction_id)

    # Consultar al PSP directamente
    psp_status = await psp_client.retrieve_charge(txn.psp_transaction_id)

    if psp_status == "succeeded":
        await finalize_transaction(transaction_id, "completed")
    elif psp_status == "failed":
        await finalize_transaction(transaction_id, "failed")
    else:
        # Todavía pendiente — reencolar para revisar más tarde
        delay_seconds = 30 * (2 ** txn.status_check_count)  # backoff
        await schedule_status_check(transaction_id, delay=delay_seconds)

Pregunta 35: ¿Cómo diseñás el sistema para cumplir con SCA (Strong Customer Authentication)?

SCA es un requisito regulatorio europeo (PSD2) que exige autenticación de dos factores para pagos online. En América Latina no es obligatorio aún, pero los PSPs globales lo están adoptando.

El flujo con SCA (3DS2):

  1. 1Tu sistema crea un PaymentIntent con el PSP.
  2. 2Si el PSP requiere SCA, devuelve status: "requires_action".
  3. 3El cliente completa el challenge (ej: código SMS del banco).
  4. 4El PSP confirma y devuelve status: "succeeded".
python
# Manejo de flujo 3DS2
intent = stripe.PaymentIntent.create(
    amount=5000,
    currency="eur",
    payment_method="pm_card_threeDSecure2Required",
    confirm=True,
    return_url="https://tuapp.com/payment/return"
)

if intent.status == "requires_action":
    # Redirigir al usuario para completar el 3DS challenge
    redirect_url = intent.next_action.redirect_to_url.url
    return {"action_required": True, "redirect_url": redirect_url}
elif intent.status == "succeeded":
    return {"success": True}

Errores comunes que eliminan candidatos

Error 1: No mencionar idempotencia desde el principio.

Es el error más grave. Un entrevistador senior va a preguntar "¿qué pasa si el cliente reintenta?" y si no tenés una respuesta clara, es señal de que no diseñaste sistemas de pagos reales.

Error 2: Usar floats para montos.

Mencionarlo activamente: "todos los montos se almacenan en la unidad mínima de la moneda como enteros, nunca floats".

Error 3: Ignorar el estado ambiguo del PSP.

La red falla. Los PSPs tienen downtime. ¿Qué pasa si no recibís respuesta? Tener un plan para estados processing y consulta posterior demuestra experiencia real.

Error 4: No distinguir consistencia fuerte de eventual.

Si usás consistencia eventual para el balance, podés tener overdraft. Si usás consistencia fuerte para todo, el sistema no escala. El entrevistador quiere ver que sabés cuándo aplicar cada una.

Error 5: Diseñar sin reconciliación.

La reconciliación no es un detalle operativo, es parte del diseño. Mencionala aunque no tengas tiempo para desarrollarla: "tendría un job nocturno que compara los registros internos con el reporte del PSP".

Error 6: No mencionar PCI DSS.

En cuanto diseñás un sistema que toca pagos con tarjeta, decir "usaría iframes del PSP para nunca tocar el PAN y quedar fuera del scope de PCI DSS" es una señal clara de que sabés del tema.


Tips para la entrevista

  1. 1Empezá siempre clarificando. Preguntá tipo de sistema, volumen, latencia, monedas antes de dibujar nada.
  1. 2Nombrá las complejidades aunque no las resolvas completamente. "La reconciliación es compleja; en el tiempo que tenemos la menciono pero nos focalizamos en X" muestra que sabés qué existe.
  1. 3Usá números reales. "500 TPS es razonable para un sistema regional; Visa maneja 65k en pico" muestra que estudiaste el dominio.
  1. 4Pensá en voz alta. Los entrevistadores evalúan el proceso tanto como el resultado. Si dudás entre dos enfoques, decilo: "podría usar locks a nivel DB o consistencia eventual; elijo locks porque el monto es crítico para la autorización".
  1. 5Preguntá back. "¿Tiene sentido este trade-off para el caso de uso que estás pensando?" muestra que es una conversación, no un monólogo.
  1. 6Diferenciá el happy path del failure path. Primero describís el flujo cuando todo sale bien, después hablás de fallos. Nunca al revés.

Practicá esta pregunta antes de la entrevista

El sistema de pagos es un tema que requiere práctica en voz alta, no solo leer. Intentá explicar el flujo completo —desde que el usuario hace clic en "Pagar" hasta que el comercio recibe el dinero— sin mirar estas notas. Si te trabás en idempotencia, consistencia o reconciliación, ese es el nodo que más necesitás reforzar.

Un buen ejercicio: explicale el diagrama de arquitectura a alguien que no sea técnico. Si podás hacerlo en términos simples, lo entendés bien. Si solo podés explicarlo con jerga, hay partes que todavía son automáticas, no comprendidas.

FAQ

¿Qué es la idempotencia en pagos y cómo se implementa?+

La idempotencia garantiza que si un cliente envía la misma solicitud de pago dos veces (por timeout o retry), el cobro se ejecuta una sola vez. Se implementa con una 'idempotency key': un UUID que el cliente genera y envía en el header de cada request. El servidor guarda esa key junto con la respuesta en la base de datos. Si recibe la misma key otra vez, devuelve la respuesta almacenada sin volver a ejecutar el cobro.

¿Qué es PCI DSS y cómo afecta el diseño de un sistema de pagos?+

PCI DSS es el estándar de seguridad para sistemas que procesan datos de tarjetas. La forma más simple de cumplirlo es diseñar el sistema para que nunca toque el número de tarjeta (PAN): se usan iframes o SDKs del PSP que capturan la tarjeta directamente y devuelven solo un token opaco. Así el sistema queda fuera del scope de auditoría de PCI DSS.

¿Por qué se usa Kafka en sistemas de pagos?+

Kafka desacopla el API que recibe el pago de los workers que lo procesan. Esto permite: responder al cliente en menos de 50ms mientras el cobro real ocurre en background; reintentar automáticamente si el PSP falla sin perder la transacción; escalar los workers horizontalmente sin cambiar el API; y usar el patrón Outbox para garantizar que los eventos se publiquen atómicamente con los cambios en la base de datos.

¿Cómo se evitan los pagos duplicados en un sistema distribuido?+

Con tres capas de protección: 1) Idempotency key en el API para que el cliente pueda reintentar sin duplicar; 2) SELECT FOR UPDATE en la base de datos para prevenir race conditions cuando dos requests concurrentes intentan procesar el mismo pago; 3) Idempotency key pasada también al PSP, para que si el worker reintenta la llamada al PSP tampoco haya un cobro doble del lado del proveedor.

¿Qué es la reconciliación en pagos y por qué es necesaria?+

La reconciliación es el proceso de comparar los registros internos del sistema con los reportes del PSP para detectar discrepancias: pagos que el PSP marcó como completados pero quedaron en 'pending' internamente, refunds no registrados, o fees incorrectos. Sin reconciliación, el sistema acumula inconsistencias que se convierten en pérdidas financieras o en disputas con los comercios. Se implementa como un job nocturno que compara ambas fuentes y genera reportes de diferencias.

¿Cuándo conviene usar consistencia eventual vs consistencia fuerte en pagos?+

Usás consistencia fuerte para datos que afectan directamente la autorización: el balance disponible del usuario debe ser consistente antes de aprobar un pago, de lo contrario podés autorizar más del saldo disponible. Usás consistencia eventual para datos secundarios: dashboards de analytics del comercio, notificaciones de webhook, reportes de volumen. La regla práctica: si la inconsistencia puede resultar en pérdida de dinero, necesitás consistencia fuerte.

Artículos relacionados

Preguntas de entrevista de diseño de sistemas distribuidos (35+)

Complete system design interview article in Spanish rioplatense with 40 questions, detailed answers, real code

Preguntas de entrevista de GraphQL — 35 con código y respuestas

35 preguntas de GraphQL para entrevistas backend y full-stack: queries, mutations, subscriptions, resolvers, DataLoader, N+1 problem, schema design. Con código.

Preguntas de entrevista de Redis — 35 con código y respuestas reales

35 preguntas de Redis para entrevistas backend y system design: estructuras de datos, pub/sub, persistencia, clustering, patrones de caché. Con ejemplos de código.

Preguntas de entrevista de Microservicios — 35 respuestas de arquitecto

35 preguntas de microservicios para roles backend y arquitecto: descomposición de servicios, saga pattern, event sourcing, circuit breaker, observabilidad. Respuestas de nivel senior.

Preparate para tu entrevista real

Pegá el link de tu vacante: investigamos quién te entrevista y te ensayamos en vivo.

Empezar gratis →

¿Tenés entrevista próxima? Instalá el copiloto en vivo →

InterviewHack.ai

Preparate para la entrevista exacta: quién te entrevista, tu CV a medida y coach real.

Producto

VacantesRevisar CV (ATS) gratis¿Cómo suena tu inglés?¿Te pagan bien?Reporte de sueldos LATAMCursos gratisBlogCV a medidaPráctica habladaEs gratis

Empleos remotos

ReactPythonFull-StackLATAMArgentinaMéxicoVer todas →

Preparate

Práctica habladaFrontendBackendAI EngineerPor empresaVendete con tu CV

Empresa

Buscás talentoAcerca deContactoPrivacidadTérminos

© 2026 InterviewHack.ai · Tu CV es tuyo. Nunca se usa para entrenar nada. · Un producto de IA-PTY