El problema fundamental que aborda esta arquitectura es la inconsistencia de datos y la latencia inherente a los sistemas distribuidos que dependen de la agregación y el caching asíncrono. En el contexto del comercio electrónico, la "desincronización" entre el motor de precios autoritativo y las páginas de cara al cliente conduce a una mala experiencia de usuario y pérdidas de negocio. La solución tradicional de precomputar y cachear todas las permutaciones de precios (el "Permutation Explosion") escala mal en almacenamiento y cómputo, y falla en la frescura de los datos.

La tesis central es que, para datos críticos que requieren frescura y consistencia en tiempo real, las capas de caché intermedias con lógica de sincronización programada son un anti-patrón. En su lugar, un enfoque de "pass-through" sin estado, que consulta la fuente de verdad directamente y transmite los resultados a medida que llegan, es superior. Esto se vuelve viable gracias a tecnologías que permiten el streaming de respuestas y el caching en el borde para solicitudes idempotentes, transformando un problema de consistencia eventual en una solución de consistencia casi inmediata con baja latencia.

Históricamente, la elección entre consistencia y disponibilidad (CAP theorem) ha guiado muchas decisiones de arquitectura. Aquí, el desafío es lograr ambas, o al menos una consistencia fuerte con alta disponibilidad y baja latencia, en un sistema que agrega datos de múltiples fuentes. La solución se inclina hacia la consistencia al eliminar el estado intermedio, y hacia la disponibilidad/latencia mediante el streaming y el caching inteligente en el borde.

Arquitectura del Sistema

La arquitectura legacy de Samsung para la agregación de precios se basaba en un servicio de Data Aggregation (DA) que actuaba como Backend for Frontend (BFF). Este servicio utilizaba un Cron Worker programado para precomputar precios para todo el catálogo de productos y sus permutaciones, almacenándolos en una caché local. Esta aproximación generaba una "explosión de permutaciones" y un "lag de sincronización" de hasta una hora, resultando en precios desactualizados.

La nueva arquitectura, denominada Bulk Arbitration Engine, es una capa de orquestación sin estado. Cuando un cliente solicita precios para múltiples SKUs (hasta 30), una función AWS Lambda recibe la solicitud. Esta Lambda utiliza awslambda.streamifyResponse() para fan-out las 30 solicitudes en paralelo al Pricing Engine autoritativo. A medida que los datos de precios individuales regresan del Pricing Engine, la Lambda los transforma en NDJSON (Newline-Delimited JSON) usando un NDJSONTransform y los comprime con GZIP (Z_BEST_SPEED), transmitiéndolos inmediatamente al cliente sin buffering. Esto reduce el Time-To-First-Byte (TTFB).

Para permitir el caching en el borde, la arquitectura convierte las solicitudes complejas de precios en una cadena de consulta GET comprimida, manteniendo el URI dentro de los límites estándar y haciendo que la solicitud sea idempotente. Amazon CloudFront se configura con una Cache Policy precisa que incluye todas las query strings y headers relevantes, permitiendo un TTL de 5-30 minutos y logrando un 95% de hit ratio. Las conexiones internas de Lambda al Pricing Engine se optimizan con Amazon VPC Peering y Provisioned Concurrency para eliminar latencia de red y cold starts. HTTP/2 multiplexing se utiliza para reutilizar conexiones TCP para las solicitudes paralelas a SKUs, y TLS 1.3 asegura la encriptación en tránsito.

Flujo de precios en tiempo real (Nueva Arquitectura)

  1. 1 Cliente Solicita precios para N SKUs (GET comprimido)
  2. 2 CloudFront Edge Sirve desde caché (95% hit) o reenvía a Lambda
  3. 3 AWS Lambda Recibe solicitud, usa `streamifyResponse()`
  4. 4 AWS Lambda Fan-out N solicitudes paralelas al Pricing Engine (VPC Peering, HTTP/2)
  5. 5 Pricing Engine Devuelve precios individuales
  6. 6 AWS Lambda Transforma a NDJSON, comprime GZIP, streamea a CloudFront
  7. 7 CloudFront Edge Reenvía stream al cliente
  8. 8 Cliente Renderiza precios a medida que llegan (DOM update)
CapaTecnologíaJustificación
compute AWS Lambda Función serverless para orquestación sin estado y streaming de respuestas. Ejecuta la lógica de fan-out y transformación. vs EC2 con caching mejorado (legacy), Lambda tradicional (buffered response) awslambda.streamifyResponse(), Provisioned Concurrency, dentro de Amazon VPC.
networking Amazon CloudFront CDN para caching en el borde y distribución de contenido. Reduce la latencia y la carga en el origen. Cache Policy con query strings y headers, HTTPS-only, TTL 5-30 min, GZIP automático.
networking HTTP/2 Protocolo para multiplexing de solicitudes sobre una única conexión TCP, reduciendo overhead de handshake. vs HTTP/1.1
networking Amazon VPC Peering Conexión de red privada entre VPCs, eliminando el tráfico a través de la red pública para reducir latencia y aumentar seguridad. vs Global VPN (legacy)
data-processing NDJSON (Newline-Delimited JSON) Formato de datos para transmitir objetos JSON de forma incremental, permitiendo al cliente procesar cada objeto a medida que llega. vs JSON completo (buffered) NDJSONTransform en Lambda.
data-processing GZIP Compression Algoritmo de compresión para reducir el tamaño de la respuesta, mejorando la latencia de transferencia. Z_BEST_SPEED (Level 1) para priorizar velocidad sobre ratio de compresión.

Trade-offs

Ganancias
  • ▲▲ Latencia P90
  • Consistencia de precios
  • Costo operacional
  • Complejidad de infraestructura
Costes
  • Complejidad de la lógica del cliente
  • Tamaño de la cadena de consulta GET
const { streamifyResponse } = require('awslambda');
const { pipeline } = require('stream/promises');
const { createGzip } = require('zlib');
const { Z_BEST_SPEED } = require('zlib').constants;

// Custom Transform stream for NDJSON
class NDJSONTransform extends Transform {
  constructor(options) {
    super({ ...options, writableObjectMode: true });
  }
  _transform(chunk, encoding, callback) {
    this.push(JSON.stringify(chunk) + '\n');
    callback();
  }
}

exports.handler = streamifyResponse(async (event, responseStream, context) => {
  // ... parse event, fan-out requests ...

  responseStream.setHeader('Content-Type', 'application/x-ndjson');
  responseStream.setHeader('Content-Encoding', 'gzip');
  responseStream.setHeader('Cache-Control', 'max-age=300, public');

  const pricingDataStream = await fetchAllPricingData(event.queryStringParameters);

  await pipeline(
    pricingDataStream, // Source stream of pricing objects
    new NDJSONTransform(),
    createGzip({ level: Z_BEST_SPEED }),
    responseStream // Destination stream
  );
});
Envoltura del handler de Lambda con `awslambda.streamifyResponse()` para habilitar el streaming, y uso de `pipeline()` para encadenar transformaciones (NDJSON, GZIP) y manejo de errores.
async function fetchPricingStream(skus, onChunkCallback) {
  const compressedQueryString = buildCompressedQueryString(skus);
  const response = await fetch(`/api/pricing?${compressedQueryString}`);

  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`);
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder('utf-8');
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    let newlineIndex;
    while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
      const line = buffer.substring(0, newlineIndex);
      buffer = buffer.substring(newlineIndex + 1);
      if (line.trim()) {
        try {
          const priceData = JSON.parse(line);
          onChunkCallback(priceData); // Update DOM for this price
        } catch (e) {
          console.error('Error parsing JSON chunk:', e);
        }
      }
    }
  }
}

// Example usage on page load
const productSKUs = ['SKU-A', 'SKU-B', 'SKU-C']; // Max 30
fetchPricingStream(productSKUs, (priceInfo) => {
  updateProductDOM(priceInfo.sku, priceInfo.price);
}).catch(error => {
  displayErrorMessage('Could not load all prices.');
});
Función cliente para realizar una solicitud GET a la API de streaming, procesando cada chunk de NDJSON a medida que llega y actualizando el DOM.

Fundamentos Teóricos

El problema de la consistencia de datos en sistemas distribuidos ha sido un tema central en la investigación de la computación desde hace décadas. El teorema CAP (Consistency, Availability, Partition tolerance) de Eric Brewer, formulado a principios de los 2000, establece que un sistema distribuido no puede garantizar simultáneamente las tres propiedades. La arquitectura legacy de Samsung, al optar por un caching asíncrono, priorizaba la disponibilidad y la tolerancia a particiones (al aislar el frontend del motor de precios) a expensas de la consistencia, lo que resultaba en datos desactualizados.

La nueva arquitectura, al eliminar la capa de caché intermedia y consultar la fuente de verdad directamente, busca una consistencia más fuerte. Al combinar esto con el streaming de respuestas y el caching en el borde para solicitudes idempotentes, se acerca a un modelo donde la consistencia es casi inmediata para los datos más recientes, y la disponibilidad y latencia se mantienen bajas a través de la distribución de CloudFront. Este enfoque resuena con los principios de los sistemas de "single source of truth" y la minimización del estado distribuido para simplificar la lógica de consistencia, un concepto explorado en trabajos sobre bases de datos distribuidas y protocolos de consenso como Paxos o Raft, aunque en un nivel de aplicación diferente. La idea de transmitir datos a medida que están disponibles también se relaciona con los principios de los sistemas reactivos y el procesamiento de flujos de datos, donde la latencia es crítica.