Backend Architecture Series — Jocoso.cl eCommerce · #05

Integrating with MercadoLibre: Architecture to Not Depend on a Giant

148M active users, one single domain model. ML as a channel, not as the source of truth.


MercadoLibre: The Opportunity and the Risk

MercadoLibre is the largest marketplace in Latin America with over 148 million active users. For an ecommerce in Chile, publishing on ML means immediate access to millions of buyers. The risk: many systems build their architecture around ML, turning the giant into their source of truth. When ML changes its API, the business grinds to a halt.

■ Technical Decision

ML is a sales channel, not the source of truth. Jocoso.cl is the source of truth. ML receives data from Jocoso.cl, never the other way around. A product exists in the system before being published on ML. Stock is managed in Jocoso.cl and synchronized toward ML.


The Data Model: Bridges to ML

MercadoLibre identifiers are stored as optional fields in existing models. A product can exist without being published on ML:

prisma
model Product {
  id        String  @id @default(uuid())
  name      String
  mlItemId  String? // null = not published on ML
  // ... other fields
}

model ProductVariant {
  id            String  @id @default(uuid())
  productId     String
  stock         Int     @default(0)
  mlVariationId String? // null = variant not synced
  // ... other fields
}

-- Partial indexes: uniqueness ONLY when the field is present
CREATE UNIQUE INDEX unique_ml_item_id
  ON products (mlItemId) WHERE mlItemId IS NOT NULL;

CREATE UNIQUE INDEX unique_ml_variation_id
  ON product_variants (mlVariationId) WHERE mlVariationId IS NOT NULL;

Why Partial Indexes Instead of Simple @unique

A simple @unique index on a nullable field in PostgreSQL already allows multiple NULLs (NULL != NULL in SQL), but using a partial index is the correct and explicit solution: it communicates business intent to the team, and allows adding additional validations at the index level in the future without destructive migration.


Logical Consistency: mlVariationId → mlItemId Rule

If a variant has mlVariationId, its parent product must have mlItemId. A variant cannot be on ML if its product is not published. This rule is enforced in the domain layer:

typescript
// domain/products/services/product.domain.service.ts
validateMlMapping(product: Product, variant: ProductVariant): void {
  if (variant.getMlVariationId() && !product.getMlItemId()) {
    throw new DomainException(
      'A variant cannot have mlVariationId without the product having mlItemId'
    );
  }
}

Stock Synchronization: Jocoso.cl → ML

When stock changes in Jocoso.cl (sale, adjustment, return), an async job with BullMQ synchronizes the new stock to ML. The design is async by design: if ML has latency or fails, web channel sales continue without interruption.

typescript
// Sync flow
// 1. StockMovement created (web channel)
// 2. SyncStockToMLJob queued in BullMQ
// 3. Worker calls PUT /items/{mlItemId}/variations/{mlVariationId} with new stock
// 4. If ML fails → exponential retry (3 attempts, backoff 2^n sec)
// 5. If ML keeps failing → alert but web channel is not interrupted

Sales from ML: Webhook → Domain

When a user buys on MercadoLibre, ML notifies via webhook. The architecture processes the sale like any other — no special treatment:

typescript
// ML sale flow
// 1. ML Webhook: { orderId, items: [{ mlVariationId, qty }] }
// 2. MLOrderWebhookHandler looks up variant by mlVariationId
// 3. CreateOrder + DecreaseStock (FOR UPDATE, idempotent with externalId)
// 4. StockMovement with source: 'ML_SALE', referenceType: 'ORDER'
// 5. If externalId already exists → 200 OK without processing (idempotency)

ML OAuth2: Tokens That Expire

The MercadoLibre API uses OAuth2 with access tokens that expire every 6 hours. The system maintains the ML access token and refresh token in DB, and an HTTP interceptor automatically renews them before each request:

  • ML access token stored encrypted in DB (AES-256)
  • Automatic refresh if token expires in less than 30 minutes
  • Automatic retry of the original request after token renewal

Resilience as a Design Principle

The system is designed to work even when ML is down. Web sales process without depending on the ML API. Synchronization is eventual — when ML comes back, the job retries. This decision protects the business: MercadoLibre had 6 major incidents in 2024, and in each one merchants with tightly coupled architectures lost sales.


■ Trade-offs

Eventual synchronization vs strong consistency with ML. With eventual sync there is a window where ML stock can be outdated (seconds/minutes under normal conditions). The alternative — blocking web sales until ML sync is confirmed — creates an external failure point. The chosen trade-off: prefer own channel availability over immediate consistency with the external channel. The overselling risk is mitigated by fast retry and active monitoring.


Extensibility: From ML to Any Marketplace

The design with optional fields and sync jobs applies equally to Amazon Marketplace, Falabella, Paris, or any future channel. To add a new marketplace, only needed:

  • Add optional fields in Product/Variant (amazonAsin, falabellaId, etc.)
  • Implement the HTTP client for the new marketplace
  • Add the corresponding sync job
  • The domain, stock logic, and payments are not modified

Conclusion

Integrating with MercadoLibre is not difficult. Integrating with ML in a way that your business does not depend on its availability, your domain model is not contaminated with its concepts, and you can add other marketplaces without refactoring — that requires architecture. Jocoso.cl treats ML for what it is: a powerful distribution channel, not a source of truth.