Skip to main content

The Translator Framework - Static Services Over Factories

· 4 min read
Coded Ventures Team
Engineering Team

We just shipped the Translator Framework — a registry-driven system for converting Engagements into external formats like cXML, EDI, and UBL. What started as a factory pattern spec evolved into something more aligned with our existing patterns.

The Problem

B2B commerce means talking to dozens of external systems, each with their own format:

  • Ariba wants cXML
  • Government portals want UBL
  • ERPs want custom XML or EDI
  • Partners want specific JSON schemas

Without a framework, you end up with scattered transformation logic, no way to override field mappings per client, and breaking changes whenever your internal models evolve.

The Solution

We built a three-part system:

  1. Config — Declarative field mappings stored in code or MongoDB
  2. Transformer — TypeScript classes that extract and format data
  3. Template — Output structure with placeholder interpolation

The key decision? Static service class instead of instantiated factories.

Why Static?

The original spec called for a factory pattern:

const factory = new TranslatorFactory();
factory.register('cxml.punchout.orderMessage', definition);

We implemented it as a static service:

TranslatorService.register('cxml.punchout.orderMessage', definition);

Why? Consistency. Our pricing module already uses this pattern. One less thing to instantiate, one less dependency to inject, same mental model across the codebase.

Config-Driven Field Mapping

Here's where it gets interesting. Each translator has a default config:

export const orderMessageConfig = {
mappings: {
"buyerCookie": "actors[0].attributes.accountId",
"shipTo.name": "actors[2].attributes.shipping_address.first_name"
},
defaults: {
"currency": "USD",
"uom": "piece"
}
};

But clients can override these mappings via MongoDB. Change field paths without touching code. Deploy mapping changes independently.

This is configuration as infrastructure — the system adapts to client needs without custom code.

Template Engine Simplicity

We kept the template engine deliberately simple:

  • Placeholder replacement: {{field}}
  • Nested properties: {{user.address.city}}
  • Safe null handling

No loops yet. For templates with iterations, transformers implement custom rendering functions. We'll add loops when we need them, not before.

This is YAGNI in practice — ship what works, evolve based on real requirements.

Hard Imports, No File I/O

Another deviation from the spec: we use hard imports instead of runtime file reading.

Spec approach:

const template = readFileSync('template.xml');

Implemented:

import { template } from './template';

Benefits:

  • Compile-time validation
  • Bundle-friendly
  • No runtime file system dependencies
  • Works in any environment

Bridge Integration

Translators are exposed through the Bridge as a property:

const bridge = new meshBridgeModel('my-service');
const xml = bridge.translators.run('cxml.punchout.orderMessage', engagement);

This follows our Bridge as abstraction layer pattern — everything goes through the Bridge, nothing talks directly to underlying services.

MongoDB Config Storage

We added a MongoDB module for storing and retrieving translator configs:

await bridge.integrations.mongoDb.module_translator
.translator_getConfigByName('client_custom_mappings');

This wasn't in the original spec, but it emerged naturally from the config-driven design. Store client-specific mappings in the database, merge them at runtime.

Evolution over specification — we let implementation details guide us toward better patterns.

Admin Testing Interface

We also built admin API endpoints for testing:

  • List registered translators
  • Get config structure
  • Run translator with real engagement data

This provides a testing interface without writing code — essential for validating client-specific mappings.

Performance Characteristics

The benchmarks are promising:

  • Translation: 1-2ms per operation
  • Registry lookup: O(1) via Map
  • No runtime file I/O
  • Stateless transformers

Fast by design — simple lookups, minimal overhead, no I/O bottlenecks.

What We Learned

1. Match Existing Patterns

The static service pattern works because it matches what we already do elsewhere. Consistency matters more than theoretical purity.

2. Config as First-Class

Making field mappings configurable from day one means clients can adapt without custom code. This scales.

3. Hard Imports Win

Compile-time imports are cleaner, safer, and more portable than runtime file reading. Choose explicitness.

4. Ship Simple, Evolve Based on Need

We could have built a full template engine with loops and conditionals. Instead, we shipped placeholders and custom functions. That's enough for now.

5. Let Implementation Guide Design

The MongoDB config storage emerged during implementation. The admin API emerged from testing needs. Let reality inform your design.

What's Next

The translator framework is production-ready and handling cXML punchout orders. Next up:

  • Additional cXML message types (invoices, order confirmations)
  • UBL format support for government procurement
  • Template engine enhancements (loops, conditionals)
  • Translator versioning system

But we'll add these when clients need them, not because we think they might be useful.

The Principle

Build systems that adapt to client needs through configuration, not customization.

The Translator Framework embodies this — same code, different configs, diverse outputs. That's how you scale without fracturing your codebase.


Translator Framework: One model, infinite formats.