Saltar a contenido

Messaging Mapper

1. Nombre del Patrón

  • Nombre oficial: Messaging Mapper
  • Categoría: Messaging Endpoints (Endpoints de Mensajería)
  • Traducción contextual: Mapeador de Mensajería

2. Resumen Ejecutivo

Messaging Mapper es un patrón que proporciona la conversión bidireccional entre objetos de dominio de la aplicación y mensajes del sistema de mensajería, manteniendo ambos mundos independientes entre sí. El mapper conoce tanto el modelo de dominio como el formato del mensaje, pero ni los objetos de dominio saben que serán convertidos a mensajes, ni los mensajes conocen la estructura de los objetos de dominio.

El problema que resuelve es la impedancia entre dos representaciones fundamentalmente diferentes de la misma información. Un objeto de dominio Policy en una aseguradora tiene herencia, relaciones bidireccionales, comportamiento encapsulado, lazy loading y un ciclo de vida gestionado por un ORM. Un mensaje PolicyCreated en un topic de Kafka es un blob serializado (JSON, Avro, Protobuf) que debe ser autocontenido, inmutable, versionable y deserializable por consumidores que no comparten el modelo de dominio del productor.

Aparece en toda aplicación que usa mensajería y tiene un modelo de dominio no trivial. Sin este patrón, la serialización se dispersa por la lógica de negocio, los objetos de dominio se contaminan con anotaciones de serialización, y los cambios en el modelo de dominio rompen contratos de mensajería sin control.


3. Definición Detallada

Propósito

El propósito de Messaging Mapper es aislar la lógica de conversión entre el modelo de dominio de la aplicación y el formato wire de los mensajes en un componente dedicado. Este componente traduce en ambas direcciones: de dominio a mensaje (cuando se publica) y de mensaje a dominio (cuando se consume).

Lógica Arquitectónica

Messaging Mapper implementa el mismo principio que los Data Mappers de Fowler aplicados a persistencia, pero en el contexto de mensajería. Así como un ORM mapper traduce entre objetos de dominio y filas de base de datos, el Messaging Mapper traduce entre objetos de dominio y mensajes.

La necesidad de este mapper surge porque las dos representaciones tienen concerns fundamentalmente diferentes:

  • Objetos de dominio: encapsulan comportamiento, tienen identidad, relaciones, estado mutable, invariantes y un ciclo de vida gestionado por la aplicación.
  • Mensajes: son DTOs inmutables, autocontenidos, serializados en un formato wire (JSON, Avro, Protobuf), versionados por schema, y diseñados para ser deserializados por consumidores que no comparten el modelo del productor.

Principio de Diseño Subyacente

El principio es separación de representación: el modelo interno de la aplicación y el modelo externo de comunicación son representaciones distintas de la misma información y deben poder evolucionar independientemente. El mapper es el componente que absorbe las diferencias entre ambas representaciones.

Problema Estructural que Resuelve

Sin un Messaging Mapper explícito, la conversión entre dominio y mensaje se realiza de una de estas formas problemáticas:

  • Anotaciones en el dominio: el objeto de dominio se anota con @JsonProperty, @AvroSchema, etc., acoplando el modelo de dominio al formato de serialización.
  • Serialización directa: se serializa el objeto de dominio directamente a JSON/Avro, exponiendo la estructura interna de la aplicación como contrato público de mensajería.
  • Conversión inline: la lógica de conversión se escribe directamente en el punto de envío o recepción, dispersa y duplicada.

Contexto en el que Emerge

Messaging Mapper emerge cuando el equipo reconoce que el modelo de dominio y el modelo de mensaje son (o deberán ser) diferentes. Típicamente emerge cuando un cambio en el modelo de dominio (añadir un campo, renombrar una propiedad, reestructurar una jerarquía) rompe consumidores downstream porque el mensaje cambió implícitamente.

Por Qué No Es Trivial

La conversión entre dominio y mensaje parece simple pero involucra decisiones no triviales:

  • Granularidad del mensaje: ¿el mensaje contiene toda la información del aggregate root, o solo los campos relevantes para el evento? Incluir todo genera mensajes grandes y acoplamiento; incluir poco puede ser insuficiente para los consumidores.
  • Flattening de relaciones: un objeto de dominio con relaciones anidadas (Policy → Coverage → Clause) debe aplanarse para el mensaje. ¿Hasta qué profundidad? ¿Se incluyen IDs de referencia o datos completos?
  • Campos calculados: ¿se incluyen en el mensaje campos derivados que el consumidor podría necesitar pero que no son parte del estado persistido?
  • Schema evolution: cuando el modelo de dominio cambia, ¿cómo se versiona el mensaje? ¿El mapper soporta múltiples versiones simultáneas?
  • Null handling: ¿cómo se representan campos opcionales en el mensaje? ¿Null, ausencia del campo, o valor por defecto?

Relación con Sistemas Distribuidos y Mensajería

En sistemas distribuidos, el formato del mensaje es un contrato público entre productor y consumidor. A diferencia de una llamada a método local (donde ambas partes compilan juntas y el compilador verifica la compatibilidad), productor y consumidor de mensajes se despliegan independientemente. El Messaging Mapper es el componente que gestiona este contrato y aísla al modelo de dominio de las restricciones del contrato público.

En la práctica:

  • En Kafka con Avro/Schema Registry, el mapper traduce entre objetos de dominio y GenericRecord o clases generadas por Avro. El Schema Registry gestiona la compatibilidad entre versiones.
  • En RabbitMQ con JSON, el mapper traduce entre objetos de dominio y payload JSON, gestionando la evolución del schema sin soporte de registry.
  • En gRPC/Protobuf, el mapper traduce entre objetos de dominio y clases generadas por Protobuf.

4. Problema que Resuelve

El Problema Antes del Patrón

Sin Messaging Mapper, las alternativas más comunes son:

  1. Serialización directa del dominio: objectMapper.writeValueAsString(policy). Esto funciona inicialmente pero acopla el contrato de mensajería a la estructura interna del modelo de dominio. Cualquier refactoring del modelo (renombrar un campo, mover una propiedad a otro objeto) rompe el contrato de mensajería.

  2. Anotaciones de serialización en el dominio: anotar Policy con @JsonProperty("policy_number") o @AvroField(name="policyNumber"). Esto contamina el modelo de dominio con concerns de serialización y crea una dependencia bidireccional: cambiar el dominio rompe el mensaje y cambiar el formato del mensaje requiere modificar el dominio.

  3. Conversión ad-hoc en cada punto de envío: cada clase que envía un mensaje contiene su propia lógica de conversión, frecuentemente duplicada e inconsistente entre diferentes puntos de envío.

Síntomas del Problema

  • Cambios en el modelo de dominio rompen consumidores de mensajes sin que nadie lo anticipe.
  • El modelo de dominio tiene anotaciones de serialización (@JsonProperty, @XmlElement, @AvroField) mezcladas con anotaciones de persistencia (@Entity, @Column).
  • El formato del mensaje expone detalles internos de la aplicación (nombres de campos internos, estructura de herencia, IDs técnicos).
  • Diferentes partes de la aplicación serializan el mismo objeto de dominio de formas inconsistentes.
  • Los tests del modelo de dominio fallan por errores de serialización.

Impacto Operativo y Arquitectónico

Sin Messaging Mapper:

  • El modelo de dominio se rigidiza porque cada cambio tiene impacto externo en los mensajes publicados.
  • Los consumidores se acoplan a la estructura interna del productor, violando el principio de encapsulación entre servicios.
  • La evolución del schema de mensajes se vuelve incontrolada porque no hay un punto explícito donde se gestiona el formato del mensaje.
  • Los bugs de serialización son difíciles de diagnosticar porque la lógica de conversión está dispersa.

Riesgos Si No Se Implementa Correctamente

  • Mapper anémico: un mapper que simplemente copia campo por campo sin transformación ni validación. Existe pero no añade valor diferencial sobre serialización directa.
  • Mapper acoplado al framework: un mapper que depende de Jackson, Avro o Protobuf en su interfaz, haciendo imposible cambiar el formato de serialización sin cambiar la interfaz.
  • Mapper bidireccional inconsistente: toMessage() y fromMessage() que no son inversas correctas, produciendo pérdida de información en round-trips.

Ejemplos Reales

  • Seguros: un mapper que convierte Policy (con su árbol de coberturas, cláusulas, asegurados y beneficiarios) en un mensaje PolicyCreated plano con los datos esenciales para los consumidores downstream (sistema de facturación, reaseguro, compliance).
  • Banca: un mapper que convierte Account (con historial de movimientos, límites, bloqueos) en un mensaje AccountOpened con solo los datos necesarios para notificaciones y compliance.
  • Healthcare: un mapper que convierte una entidad Patient (con historial clínico completo) en un mensaje PatientAdmitted con solo datos demográficos y de admisión, excluyendo información clínica sensible.

5. Contexto de Aplicación

Cuándo Usarlo

  • Cuando el modelo de dominio es significativamente diferente del formato del mensaje (diferente estructura, diferente granularidad, diferentes campos).
  • Cuando el modelo de dominio debe poder evolucionar sin romper el contrato de mensajería.
  • Cuando múltiples tipos de mensajes se derivan del mismo objeto de dominio (un Policy genera PolicyCreated, PolicyAmended, PolicyCancelled, cada uno con campos diferentes).
  • Cuando se necesita control explícito sobre qué información se incluye en el mensaje (exclusión de datos sensibles, campos internos o información redundante).
  • Cuando se usan schemas tipados (Avro, Protobuf) donde las clases generadas son diferentes de las clases de dominio.

Cuándo No Usarlo

  • Cuando el modelo de dominio es tan simple que coincide exactamente con el formato del mensaje y no se espera que diverga.
  • Cuando se usa un enfoque de "domain events as first-class citizens" donde los objetos de evento ya están diseñados como mensajes (no son entities del dominio).
  • En prototipos donde la velocidad de desarrollo prima sobre la separación de concerns.

Precondiciones

  • Existe un modelo de dominio con objetos que tienen comportamiento y estado.
  • Existe un formato de mensaje definido (schema Avro, schema JSON, .proto, etc.).
  • Existe un Messaging Gateway o mecanismo de envío que necesita la conversión.

Restricciones

  • El mapper debe ser capaz de manejar la evolución del schema del mensaje (nuevos campos, campos deprecados) sin romper consumidores existentes.
  • El mapper debe ser determinista: el mismo objeto de dominio debe producir el mismo mensaje siempre.

Dependencias

  • Modelo de dominio de la aplicación.
  • Definición del schema del mensaje (Avro schema, Protobuf definition, JSON Schema, etc.).
  • Librería de serialización (Jackson, Avro, Protobuf, etc.).

Supuestos Arquitectónicos

  • El modelo de dominio y el modelo de mensaje son (o serán) representaciones diferentes de la misma información.
  • Los consumidores del mensaje no comparten el modelo de dominio del productor.
  • El formato del mensaje es un contrato público que debe gestionarse explícitamente.

Tipo de Sistemas Donde Aparece con Más Frecuencia

  • Microservicios con domain models ricos (DDD).
  • Aplicaciones de seguros con modelos de póliza complejos.
  • Sistemas bancarios con modelos de cuenta y transacción elaborados.
  • Sistemas de salud con modelos de paciente y episodio clínico extensos.
  • Cualquier aplicación con clean architecture o hexagonal architecture.

6. Fuerzas Arquitectónicas

Acoplamiento vs. Flexibilidad

El Messaging Mapper desacopla el modelo de dominio del contrato de mensajería. Esto proporciona flexibilidad para evolucionar ambos independientemente. El costo es la existencia de un componente adicional (el mapper) que debe mantenerse sincronizado con ambos modelos.

Simplicidad vs. Robustez

Serializar directamente el dominio es simple pero frágil. Un Messaging Mapper explícito es más robusto (controla exactamente qué se incluye en el mensaje y en qué formato) pero requiere escribir y mantener código de conversión.

Fidelidad vs. Encapsulación

Incluir más información en el mensaje aumenta la utilidad para los consumidores pero expone más del modelo interno del productor. Incluir menos preserva la encapsulación pero puede ser insuficiente. El mapper es el componente que toma esta decisión explícitamente.

Performance vs. Claridad

Mappers con muchas transformaciones (flattening, enrichment, cálculo de campos derivados) pueden impactar la latencia de envío. Sin embargo, priorizar performance eliminando el mapper y serializando directamente sacrifica claridad y mantenibilidad.

Consistencia vs. Autonomía de Equipos

Un mapper centralizado garantiza que todos los mensajes de un tipo se crean de la misma forma. Pero si múltiples equipos necesitan crear mensajes del mismo tipo, el mapper puede convertirse en un punto de contención. La solución es ownership claro del mapper por parte del equipo que posee el dominio.

Evolución del Schema vs. Estabilidad del Contrato

Los schemas de mensajes deben evolucionar (nuevos campos, campos deprecados) sin romper consumidores. El mapper es el componente que gestiona esta evolución, traduciendo entre la versión actual del dominio y las versiones del schema del mensaje.


7. Estructura Conceptual del Patrón

Actores o Componentes Involucrados

  1. Objeto de Dominio: la entidad o aggregate del modelo de negocio (ej: Policy, Claim, Coverage).
  2. Messaging Mapper: el componente que contiene la lógica de conversión bidireccional.
  3. Mensaje (DTO/Event): el objeto que representa el mensaje en formato wire (ej: PolicyCreatedEvent, ClaimSubmittedEvent).
  4. Serializer: el componente que convierte el DTO del mensaje a bytes (JSON, Avro, Protobuf). El mapper produce/consume el DTO; el serializer produce/consume bytes.
  5. Messaging Gateway: el componente que usa el mapper para convertir antes de enviar o después de recibir.

Flujo Lógico

flowchart TD
    subgraph Envío ["Envío (Dominio a Mensaje)"]
        A1([Lógica de Negocio]) -->|Objeto de dominio| B1[Gateway]
        B1 -->|mapper.toMessage| C1[Mapper: Extrae campos del dominio]
        C1 -->|Transforma y enriquece| D1[Mapper: Construye DTO del mensaje]
        D1 -->|DTO| E1[Serializer: Serializa a JSON/Avro/Protobuf]
        E1 -->|Bytes| F1[(Broker)]
    end

    subgraph Recepción ["Recepción (Mensaje a Dominio)"]
        A2[(Broker)] -->|Bytes| B2[Serializer: Deserializa a DTO]
        B2 -->|DTO| C2[Gateway invoca mapper.fromMessage]
        C2 -->|Extrae campos del DTO| D2[Mapper: Construye objeto de dominio]
        D2 -->|Objeto de dominio| E2([Lógica de Negocio])
    end

Responsabilidades

Componente Responsabilidad
Objeto de Dominio Representar el estado y comportamiento del negocio
Messaging Mapper Convertir entre dominio y DTO de mensaje, manejar diferencias de estructura
Mensaje (DTO) Representar el contrato público del mensaje, versionable y serializable
Serializer Convertir DTO a bytes y viceversa
Gateway Orquestar el flujo de conversión y envío/recepción

Interacciones

  • Gateway → Mapper: invocación de toMessage() o fromMessage().
  • Mapper → Domain Object: lectura de propiedades para construir el DTO.
  • Mapper → Message DTO: construcción del DTO con los datos extraídos y transformados.
  • Gateway → Serializer: serialización del DTO a formato wire.

Contratos Implícitos

  • El mapper conoce la estructura de ambos modelos (dominio y mensaje).
  • El DTO del mensaje es el contrato público; su estructura se gestiona como un API contract.
  • El mapper garantiza que toda la información necesaria para el consumidor está presente en el mensaje.

Decisiones de Diseño Clave

  1. DTO explícito vs. mapa genérico: usar una clase DTO tipada (PolicyCreatedEvent) vs. un Map<String, Object>. El DTO tipado es más seguro y documentable; el mapa genérico es más flexible pero menos seguro.
  2. Mapper manual vs. generado: escribir el mapper manualmente vs. usar herramientas de generación (MapStruct, ModelMapper). Manual es más explícito y controlable; generado es más rápido de implementar pero puede ocultar transformaciones.
  3. Granularidad del mensaje: qué campos del dominio incluir en el mensaje. Decisión crítica que afecta acoplamiento y utilidad.
  4. Manejo de versiones: cómo el mapper maneja múltiples versiones del schema del mensaje.

8. Ejemplo Arquitectónico Detallado

Dominio: Seguros — Mapeo entre Policy y PolicyCreated

Contexto del Negocio

Una aseguradora emite aproximadamente 50,000 pólizas diarias a través de múltiples canales (agentes, portal web, banca-seguros). Cada póliza emitida genera un evento PolicyCreated que deben consumir: facturación (para generar el primer cobro), reaseguro (para calcular cesión), compliance (para verificar reglas regulatorias), y analytics (para reportes de producción).

Necesidad de Integración

El modelo de dominio Policy es un aggregate root complejo con relaciones profundas. El evento PolicyCreated debe contener la información necesaria para todos los consumidores sin exponer la complejidad interna del modelo de dominio.

Sistemas Involucrados

  1. Policy Service: microservicio que gestiona el ciclo de vida de pólizas. Produce el evento PolicyCreated.
  2. Apache Kafka con Schema Registry: infraestructura de mensajería con schemas Avro.
  3. Billing Service: consume PolicyCreated para generar la primera factura.
  4. Reinsurance Service: consume PolicyCreated para calcular la cesión al reasegurador.
  5. Compliance Service: consume PolicyCreated para verificación regulatoria.
  6. Analytics Platform: consume PolicyCreated para dashboards de producción.

Modelo de Dominio (Simplificado)

public class Policy {
    private PolicyId id;
    private PolicyNumber number;
    private PolicyType type;
    private PolicyStatus status;
    private LocalDate effectiveDate;
    private LocalDate expirationDate;
    private Policyholder policyholder;     // Relación con entidad Policyholder
    private List<Coverage> coverages;       // Colección de coberturas
    private List<Endorsement> endorsements; // Historial de endosos
    private UnderwritingResult underwriting; // Resultado de suscripción
    private Premium premium;                // Prima calculada
    private Agent sellingAgent;             // Agente vendedor
    private Channel channel;                // Canal de venta
    private LocalDateTime createdAt;
    private AuditTrail auditTrail;          // Trazabilidad de cambios
    // ... métodos de dominio, invariantes, validaciones
}

Modelo del Mensaje (Schema Avro)

{
  "type": "record",
  "name": "PolicyCreatedEvent",
  "namespace": "com.insurance.events.policy",
  "fields": [
    {"name": "eventId", "type": "string"},
    {"name": "eventTimestamp", "type": "long", "logicalType": "timestamp-millis"},
    {"name": "policyId", "type": "string"},
    {"name": "policyNumber", "type": "string"},
    {"name": "policyType", "type": {"type": "enum", "name": "PolicyType", "symbols": ["AUTO", "HOME", "LIFE", "HEALTH"]}},
    {"name": "effectiveDate", "type": "int", "logicalType": "date"},
    {"name": "expirationDate", "type": "int", "logicalType": "date"},
    {"name": "policyholderId", "type": "string"},
    {"name": "policyholderName", "type": "string"},
    {"name": "policyholderTaxId", "type": "string"},
    {"name": "totalPremium", "type": {"type": "bytes", "logicalType": "decimal", "precision": 12, "scale": 2}},
    {"name": "currency", "type": "string"},
    {"name": "coverageSummaries", "type": {"type": "array", "items": "CoverageSummary"}},
    {"name": "channelCode", "type": "string"},
    {"name": "agentCode", "type": ["null", "string"], "default": null},
    {"name": "riskScore", "type": ["null", "int"], "default": null}
  ]
}

Restricciones Técnicas

  • El modelo de dominio Policy tiene 40+ campos y relaciones anidadas a 3 niveles de profundidad. El mensaje debe ser significativamente más plano y contener solo 15-20 campos.
  • El schema Avro debe ser backward-compatible con versiones anteriores.
  • Datos sensibles del policyholder (dirección completa, teléfono, email) no deben incluirse en el evento por política de privacidad.
  • El riskScore es un campo calculado por el motor de suscripción que no se persiste en el modelo de dominio pero sí debe incluirse en el evento.

9. Desarrollo Paso a Paso del Ejemplo

Paso 1: Definición del DTO del Mensaje

Se crea la clase que representa el mensaje, independiente del modelo de dominio:

public class PolicyCreatedEvent {
    private String eventId;
    private Instant eventTimestamp;
    private String policyId;
    private String policyNumber;
    private PolicyType policyType;
    private LocalDate effectiveDate;
    private LocalDate expirationDate;
    private String policyholderId;
    private String policyholderName;
    private String policyholderTaxId;
    private BigDecimal totalPremium;
    private String currency;
    private List<CoverageSummary> coverageSummaries;
    private String channelCode;
    private String agentCode;        // nullable
    private Integer riskScore;       // nullable, campo calculado
}

Paso 2: Implementación del Mapper

@Component
public class PolicyEventMapper {

    public PolicyCreatedEvent toPolicyCreatedEvent(Policy policy) {
        return PolicyCreatedEvent.builder()
            .eventId(UUID.randomUUID().toString())
            .eventTimestamp(Instant.now())
            .policyId(policy.getId().value())
            .policyNumber(policy.getNumber().value())
            .policyType(policy.getType())
            .effectiveDate(policy.getEffectiveDate())
            .expirationDate(policy.getExpirationDate())
            .policyholderId(policy.getPolicyholder().getId().value())
            .policyholderName(policy.getPolicyholder().getFullName())
            .policyholderTaxId(policy.getPolicyholder().getTaxId().masked())
            .totalPremium(policy.getPremium().totalAmount())
            .currency(policy.getPremium().currency().getCode())
            .coverageSummaries(mapCoverages(policy.getCoverages()))
            .channelCode(policy.getChannel().code())
            .agentCode(policy.getSellingAgent() != null
                ? policy.getSellingAgent().code() : null)
            .riskScore(policy.getUnderwriting() != null
                ? policy.getUnderwriting().riskScore() : null)
            .build();
    }

    private List<CoverageSummary> mapCoverages(List<Coverage> coverages) {
        return coverages.stream()
            .map(c -> CoverageSummary.builder()
                .coverageCode(c.getCode())
                .coverageName(c.getName())
                .insuredAmount(c.getInsuredAmount().value())
                .premiumAmount(c.getPremiumAmount().value())
                .build())
            .toList();
    }
}

Paso 3: Observar las Transformaciones

El mapper realiza varias transformaciones explícitas:

  1. Generación de metadata: eventId y eventTimestamp se generan en el mapper (no existen en el dominio).
  2. Unwrapping de Value Objects: policy.getId().value() extrae el valor primitivo del Value Object PolicyId.
  3. Flattening de relaciones: policy.getPolicyholder().getFullName() aplana la relación Policy → Policyholder a campos planos en el mensaje.
  4. Masking de datos sensibles: policy.getPolicyholder().getTaxId().masked() aplica masking parcial al número de identificación fiscal.
  5. Cálculo de campos derivados: policy.getPremium().totalAmount() calcula el total a partir de la estructura de prima.
  6. Mapping de colecciones: las coberturas completas (con cláusulas, exclusiones, deducibles) se resumen como CoverageSummary (solo código, nombre, montos).
  7. Null handling: agentCode y riskScore pueden ser null (pólizas vendidas por portal sin agente, pólizas sin scoring).

Paso 4: Integración con el Gateway

El gateway usa el mapper para convertir antes de enviar:

@Component
public class KafkaPolicyEventGateway implements PolicyEventGateway {
    private final KafkaTemplate<String, PolicyCreatedEvent> kafkaTemplate;
    private final PolicyEventMapper mapper;

    @Override
    public CompletableFuture<EventPublishResult> publishPolicyCreated(Policy policy) {
        PolicyCreatedEvent event = mapper.toPolicyCreatedEvent(policy);
        return kafkaTemplate.send("insurance.policies.events",
                event.getPolicyId(), event)
            .thenApply(r -> EventPublishResult.success(r.getRecordMetadata().offset()))
            .exceptionally(ex -> EventPublishResult.failure(ex.getMessage()));
    }
}

Paso 5: Mapper del Lado del Consumidor

El Billing Service tiene su propio mapper que convierte del mensaje a su modelo de dominio:

@Component
public class BillingPolicyMapper {
    public BillablePolicy fromPolicyCreatedEvent(PolicyCreatedEvent event) {
        return BillablePolicy.builder()
            .policyId(event.getPolicyId())
            .policyNumber(event.getPolicyNumber())
            .policyholderId(event.getPolicyholderId())
            .policyholderName(event.getPolicyholderName())
            .totalPremium(event.getTotalPremium())
            .currency(event.getCurrency())
            .effectiveDate(event.getEffectiveDate())
            .paymentSchedule(calculateSchedule(event))
            .build();
    }
}

Note que el consumidor crea su propio modelo (BillablePolicy), no intenta reconstruir el Policy original. Cada consumidor mapea el mensaje a su propio modelo de dominio.

Manejo de Errores

  • Campo faltante en el dominio: si un campo requerido del mensaje no está disponible en el dominio, el mapper lanza una excepción descriptiva en tiempo de mapeo.
  • Schema incompatible: si el consumidor recibe un mensaje con un schema version diferente, el deserializer de Avro + Schema Registry maneja la compatibilidad.
  • Datos inválidos: el mapper valida que los datos extraídos del dominio cumplen las restricciones del schema del mensaje antes de construir el DTO.

10. Diagrama Técnico del Patrón

Código Python con diagrams

Diagrama General

Diagrama AWS

Diagrama Azure

Ver / Copiar código de los diagramas
from diagrams import Diagram, Cluster, Edge
from diagrams.programming.language import Java
from diagrams.onprem.queue import Kafka
from diagrams.generic.compute import Rack

with Diagram("Messaging Mapper - Insurance Policy Events", show=False, direction="LR"):

    with Cluster("Policy Service (Producer)"):
        domain_obj = Java("Policy\n(Domain Object)")
        mapper_out = Java("PolicyEvent\nMapper")
        dto_out = Java("PolicyCreated\nEvent (DTO)")
        serializer_out = Rack("Avro\nSerializer")
        gateway = Java("PolicyEvent\nGateway")

    kafka = Kafka("Kafka\n+ Schema Registry")

    with Cluster("Billing Service (Consumer)"):
        deserializer_in = Rack("Avro\nDeserializer")
        dto_in = Java("PolicyCreated\nEvent (DTO)")
        mapper_in = Java("BillingPolicy\nMapper")
        billing_obj = Java("BillablePolicy\n(Domain Object)")

    with Cluster("Reinsurance Service (Consumer)"):
        mapper_reins = Java("ReinsurancePolicy\nMapper")
        reins_obj = Java("CedablePolicy\n(Domain Object)")

    domain_obj >> Edge(label="extract") >> mapper_out
    mapper_out >> Edge(label="build") >> dto_out
    dto_out >> Edge(label="serialize") >> serializer_out
    serializer_out >> Edge(label="publish") >> gateway
    gateway >> kafka

    kafka >> Edge(label="consume") >> deserializer_in
    deserializer_in >> dto_in
    dto_in >> Edge(label="map") >> mapper_in
    mapper_in >> Edge(label="build") >> billing_obj

    kafka >> Edge(label="consume") >> mapper_reins
    mapper_reins >> Edge(label="build") >> reins_obj
from diagrams import Diagram, Cluster, Edge
from diagrams.aws.compute import Lambda
from diagrams.aws.integration import Eventbridge, SNS, SQS


with Diagram("Messaging Mapper - Insurance Policy Events (AWS)", show=False, direction="LR"):

    with Cluster("Policy Service (Producer)"):
        domain_obj = Lambda("Policy\n(Domain Object)")
        mapper_out = Lambda("PolicyEvent\nMapper")
        gateway = Lambda("PolicyEvent\nGateway")

    with Cluster("EventBridge"):
        event_bus = Eventbridge("Policy Events\nBus")
        input_transform = Eventbridge("InputTransformer\n(Schema)")

    topic = SNS("policy-events\nTopic")

    with Cluster("Billing Service (Consumer)"):
        billing_q = SQS("Billing\nQueue")
        mapper_in = Lambda("BillingPolicy\nMapper")
        billing_obj = Lambda("BillablePolicy\n(Domain Object)")

    with Cluster("Reinsurance Service (Consumer)"):
        reins_q = SQS("Reinsurance\nQueue")
        mapper_reins = Lambda("ReinsurancePolicy\nMapper")
        reins_obj = Lambda("CedablePolicy\n(Domain Object)")

    domain_obj >> Edge(label="extract") >> mapper_out
    mapper_out >> Edge(label="put event") >> gateway
    gateway >> event_bus
    event_bus >> Edge(label="transform") >> input_transform
    input_transform >> topic

    topic >> Edge(label="fan-out") >> billing_q
    billing_q >> Edge(label="map") >> mapper_in
    mapper_in >> Edge(label="build") >> billing_obj

    topic >> Edge(label="fan-out") >> reins_q
    reins_q >> mapper_reins
    mapper_reins >> Edge(label="build") >> reins_obj
from diagrams import Diagram, Cluster, Edge
from diagrams.azure.compute import FunctionApps
from diagrams.azure.integration import ServiceBus

with Diagram("Messaging Mapper - Insurance Policy Events (Azure)", show=False, direction="LR"):

    with Cluster("Policy Function (Producer)"):
        domain_obj = FunctionApps("Policy\n(Domain Object)")
        mapper_out = FunctionApps("PolicyEvent\nMapper\n(Output Binding)")
        dto_out = FunctionApps("PolicyCreated\nEvent (DTO)")

    topic = ServiceBus("policy-events\nTopic")

    with Cluster("Billing Function (Consumer)"):
        billing_func = FunctionApps("Billing\nFunction\n(Input Binding)")
        mapper_in = FunctionApps("BillingPolicy\nMapper")
        billing_obj = FunctionApps("BillablePolicy\n(Domain Object)")

    with Cluster("Reinsurance Function (Consumer)"):
        reins_func = FunctionApps("Reinsurance\nFunction\n(Input Binding)")
        mapper_reins = FunctionApps("ReinsurancePolicy\nMapper")
        reins_obj = FunctionApps("CedablePolicy\n(Domain Object)")

    domain_obj >> Edge(label="extract") >> mapper_out
    mapper_out >> Edge(label="output binding\nserializes") >> dto_out
    dto_out >> Edge(label="publish") >> topic

    topic >> Edge(label="subscription\nbilling-sub") >> billing_func
    billing_func >> Edge(label="input binding\ndeserializes") >> mapper_in
    mapper_in >> Edge(label="build") >> billing_obj

    topic >> Edge(label="subscription\nreinsurance-sub") >> reins_func
    reins_func >> Edge(label="map") >> mapper_reins
    mapper_reins >> Edge(label="build") >> reins_obj

Explicación del Diagrama

El diagrama muestra el flujo completo del Messaging Mapper en ambas direcciones:

  1. Producer side: el Policy domain object pasa por el PolicyEventMapper que extrae y transforma los datos necesarios, construye el DTO PolicyCreatedEvent, que luego se serializa a Avro y se publica en Kafka.
  2. Consumer side (Billing): el mensaje llega de Kafka, se deserializa a PolicyCreatedEvent, y el BillingPolicyMapper lo transforma al modelo de dominio del Billing Service (BillablePolicy).
  3. Consumer side (Reinsurance): el mismo mensaje se transforma al modelo del Reinsurance Service (CedablePolicy) por un mapper diferente.

Correspondencia Patrón ↔ Diagrama

Concepto del Patrón Componente del Diagrama
Domain Object (productor) Policy
Messaging Mapper (salida) PolicyEventMapper
Mensaje (DTO) PolicyCreatedEvent
Serializer Avro Serializer/Deserializer
Domain Object (consumidor) BillablePolicy, CedablePolicy
Messaging Mapper (entrada) BillingPolicyMapper, ReinsurancePolicyMapper

11. Beneficios

Impacto Técnico

  • Independencia entre modelo de dominio y contrato de mensaje: el modelo de dominio puede evolucionar (refactoring, nuevas relaciones, cambios de estructura) sin romper el contrato de mensajería, y viceversa.
  • Control explícito del contrato: el mapper define explícitamente qué información se incluye en el mensaje, evitando la exposición accidental de datos internos o sensibles.
  • Reducción del tamaño del mensaje: el mapper selecciona solo los campos necesarios, produciendo mensajes más ligeros que la serialización completa del aggregate.
  • Type safety: los DTOs del mensaje son clases tipadas con validación, eliminando errores de serialización en runtime.

Impacto Organizacional

  • Contrato documentable: el DTO del mensaje y el schema Avro/Protobuf constituyen documentación formal del contrato de integración.
  • Autonomía de equipos: el equipo productor puede refactorizar su modelo de dominio sin impactar a los consumidores, siempre que el mapper mantenga el contrato del mensaje.
  • Onboarding: un nuevo desarrollador entiende qué datos se publican leyendo el mapper, sin necesidad de entender todo el modelo de dominio.

Impacto Operacional

  • Debugging de mensajes: cuando un mensaje tiene datos incorrectos, el mapper es el único lugar donde buscar el error de conversión.
  • Validación pre-envío: el mapper puede validar que los datos cumplen las restricciones del schema antes de intentar serializar.
  • Masking de datos sensibles: el mapper es el punto natural para aplicar masking o exclusión de PII antes de publicar.

Beneficios de Mantenibilidad y Evolución

  • Schema evolution controlada: cuando se necesita un nuevo campo en el mensaje, se modifica el mapper y el DTO, no el modelo de dominio.
  • Backward compatibility: el mapper puede generar mensajes compatibles con múltiples versiones del schema.
  • Testing aislado: el mapper se testea unitariamente con objetos de dominio de prueba, verificando que la conversión produce el mensaje esperado.

12. Desventajas y Riesgos

Complejidad Añadida

  • Código adicional: cada tipo de mensaje requiere un mapper con lógica de conversión. En un sistema con 50 tipos de eventos, son 50 mappers.
  • Sincronización triple: el modelo de dominio, el DTO del mensaje y el mapper deben mantenerse sincronizados. Un campo añadido al dominio que debería reflejarse en el mensaje requiere cambios en tres lugares.
  • Mappers complejos: cuando el modelo de dominio es muy diferente del mensaje (flattening profundo, cálculos complejos, enriquecimiento con datos de otros servicios), el mapper puede volverse complejo y difícil de mantener.

Riesgos de Mal Uso

  • Mapper como transformador de negocio: el mapper debe convertir, no transformar con lógica de negocio. Si el mapper calcula descuentos, aplica reglas o toma decisiones de negocio, está asumiendo responsabilidades que no le corresponden.
  • Mapper que lee de base de datos: un mapper que hace queries a la base de datos para enriquecer el mensaje está mezclando concerns de persistencia con concerns de mapeo. El enriquecimiento debe ocurrir antes del mapeo.
  • Compartir mappers entre servicios: publicar el mapper como biblioteca compartida crea acoplamiento de deployment entre servicios.

Sobreingeniería

  • Crear un framework genérico de mapping con reflection, configuración XML y auto-discovery cuando mappers simples con código explícito serían suficientes.
  • Usar herramientas de mapping automático (ModelMapper, Dozer) que ocultan las transformaciones y hacen difícil entender qué se incluye en el mensaje.

Anti-Patterns Relacionados

  • Domain Object as Message: serializar directamente el objeto de dominio como mensaje, sin mapper ni DTO intermedio. Funciona inicialmente pero acopla el contrato público a la estructura interna.
  • Message as Domain Object: usar el DTO del mensaje como modelo de dominio en el consumidor, sin mapper de entrada. El consumidor queda acoplado al schema del productor.
  • Bidirectional Mapper: un mapper que convierte en ambas direcciones entre el mismo par de clases, creando la ilusión de que el dominio y el mensaje son equivalentes.

13. Relación con Otros Patrones

Patrones Complementarios

  • Messaging Gateway: el gateway usa el mapper para convertir entre dominio y mensaje. Son colaboradores naturales.
  • Canonical Data Model: cuando múltiples productores generan mensajes sobre el mismo concepto (Policy), un modelo canónico compartido evita que cada productor defina su propio formato. El mapper traduce del dominio específico al modelo canónico.
  • Message Translator: si un consumidor recibe un mensaje en un formato y necesita otro, Message Translator realiza la conversión entre formatos de mensaje. Messaging Mapper convierte entre dominio y mensaje; Message Translator convierte entre mensaje y mensaje.

Patrones que Suelen Aparecer Antes o Después

  • Content Enricher: a veces el mapper necesita datos que no están en el objeto de dominio (ej: nombre del agente cuando el dominio solo tiene el ID). Content Enricher se aplica antes del mapping para proporcionar los datos necesarios.
  • Content Filter: inversamente, el mapper puede actuar como Content Filter al excluir campos del dominio que no deben publicarse.

Combinaciones Comunes

  • Messaging Mapper + Schema Registry: el mapper produce DTOs que se serializan contra schemas registrados en Schema Registry, proporcionando validación de compatibilidad en tiempo de serialización.
  • Messaging Mapper + Messaging Gateway + Transactional Client: la combinación completa para publicación transaccional de eventos de dominio.

Diferencias con Patrones Similares

  • vs. Message Translator: Message Translator convierte entre dos formatos de mensaje. Messaging Mapper convierte entre un objeto de dominio y un mensaje. El translator opera en el mundo del messaging; el mapper opera en la frontera entre el mundo de la aplicación y el mundo del messaging.
  • vs. Envelope Wrapper: Envelope Wrapper añade y remueve headers y metadata del mensaje sin cambiar el payload. Messaging Mapper transforma el payload mismo.

Encaje en un Flujo Mayor de Integración

Messaging Mapper se sitúa en la frontera de cada aplicación, como parte del Messaging Gateway. En un flujo de integración completo, los datos pasan por: dominio del productor → mapper de salida → mensaje → broker → mensaje → mapper de entrada → dominio del consumidor. El mapper aparece dos veces: en la salida del productor y en la entrada del consumidor.


14. Relevancia Actual del Patrón

Evaluación: Relevancia Alta

Argumentación

Messaging Mapper es absolutamente relevante en la arquitectura moderna. La separación entre modelo de dominio y contrato de mensaje es un principio fundamental de diseño de microservicios.

A favor de la vigencia:

  • Los microservicios modernos siguen DDD, con modelos de dominio ricos que son significativamente diferentes de los DTOs de comunicación.
  • Los schema registries (Confluent Schema Registry, AWS Glue Schema Registry, Apicurio) formalizan el contrato del mensaje como un artefacto gestionado, reforzando la necesidad de separar dominio de mensaje.
  • La adopción de Avro, Protobuf y JSON Schema como formatos tipados genera automáticamente clases DTO que son diferentes de las clases de dominio, requiriendo mappers explícitos.
  • Las regulaciones de privacidad (GDPR, PCI-DSS) exigen control explícito sobre qué datos se incluyen en mensajes que cruzan fronteras de servicio.

Evolución moderna:

  • Herramientas como MapStruct generan mappers en tiempo de compilación, reduciendo el boilerplate.
  • Los frameworks de Event Sourcing (Axon, EventStoreDB) tienen mecanismos nativos de mapping entre domain events y formatos de persistencia/publicación.

Cómo Se Implementa Hoy

  • MapStruct: genera mappers type-safe en compile time para Java/Kotlin.
  • AutoMapper (.NET): mapping por convención con configuración declarativa.
  • Avro/Protobuf code generation: genera DTOs del schema que se usan como el "lado mensaje" del mapper.
  • Custom mappers: en muchas organizaciones, los mappers se escriben manualmente para mantener control total sobre la transformación.

15. Implementación en Arquitecturas Modernas

Apache Kafka con Schema Registry

El mapper produce DTOs que se serializan con KafkaAvroSerializer contra schemas registrados en Schema Registry. El registry valida que el schema del mensaje es compatible con versiones anteriores. Cuando el schema evoluciona (nuevo campo con default), el mapper del productor se actualiza para incluir el nuevo campo, y los consumidores existentes siguen funcionando sin cambios gracias a la backward compatibility de Avro.

Spring Boot / Spring Kafka

Spring Kafka no proporciona mapping automático entre dominio y mensaje. El mapper se implementa como un @Component Spring que el gateway inyecta. MapStruct se integra naturalmente con Spring para generar implementaciones de mappers con inyección de dependencias.

MassTransit / NServiceBus (.NET)

Ambos frameworks esperan que los mensajes sean DTOs simples. El mapping se hace explícitamente antes de Publish() o Send(). AutoMapper es la herramienta estándar para el mapping en el ecosistema .NET.

gRPC / Protobuf

Los archivos .proto generan clases de mensaje que son el DTO. El mapper convierte entre los objetos de dominio y los builders de Protobuf. La generación de código Protobuf produce builders inmutables que se prestan bien para mappers.

Azure Service Bus / AWS SQS

Los mensajes son JSON o binary. El mapper produce un DTO que luego se serializa a JSON. Sin schema registry nativo, la compatibilidad de versiones se gestiona mediante convenciones (campos opcionales, versionado en headers).

Apache Camel / MuleSoft

Camel proporciona TypeConverter y DataFormat como mecanismos de mapping integrados en las rutas de integración. MuleSoft usa DataWeave como lenguaje de transformación declarativo que puede actuar como mapper entre formatos.


16. Consideraciones de Gobierno y Operación

Observabilidad

  • Métricas: mensajes mapeados/segundo, errores de mapping/segundo, latencia de mapping (p50, p95).
  • Logging: registrar el tipo de evento mapeado, el ID del aggregate, y la versión del schema. No registrar el contenido completo del mensaje (puede contener PII).
  • Alertas: aumento de errores de mapping (indica incompatibilidad entre dominio y schema).

Tracing

  • El mapper debe preservar y propagar el trace ID del contexto de ejecución al mensaje (como header).
  • Los campos de trazabilidad del mensaje (eventId, correlationId, causationId) se generan en el mapper.

Versionado

  • Los schemas de mensajes se versionan en Schema Registry con políticas de compatibilidad (BACKWARD, FORWARD, FULL).
  • Cuando se añade un campo al schema, el mapper se actualiza para incluirlo. Los consumidores con schemas anteriores ignoran el campo nuevo (forward compatibility).
  • Cuando se elimina un campo del schema, se mantiene con un default en el schema (backward compatibility) y el mapper deja de proporcionarlo gradualmente.

Seguridad

  • El mapper es el punto donde se aplica masking de PII (números de documento parciales, emails ofuscados, direcciones omitidas).
  • Los campos clasificados como "internal only" se excluyen explícitamente en el mapper.
  • Auditoría: registro de qué datos se incluyeron y excluyeron del mensaje, para compliance con políticas de datos.

Manejo de Errores

  • Campo requerido null: el mapper lanza excepción si un campo requerido del mensaje no está disponible en el dominio.
  • Tipo incompatible: el mapper valida tipos antes de asignar (ej: si el dominio tiene un BigDecimal negativo y el schema requiere positivo).
  • Colección vacía: el mapper decide si una colección vacía se mapea como [] o como ausencia del campo.

Idempotencia

  • El mapper debe ser determinista: la misma entrada (mismo objeto de dominio con el mismo estado) debe producir el mismo mensaje.
  • Los campos no deterministas (eventId, eventTimestamp) se generan fuera del mapper o se documentan como excepciones.

Performance

  • Los mappers deben ser ligeros y sin side effects (no I/O, no queries, no network calls).
  • Para objetos de dominio con colecciones grandes, el mapper puede paginar o resumir en lugar de mapear toda la colección.
  • MapStruct genera código de mapeo directo (sin reflection) que es tan rápido como mapeo manual.

17. Errores Comunes

Serializar Directamente el Dominio

El error más frecuente es serializar el aggregate root directamente como mensaje, exponiendo toda la estructura interna como contrato público. Cuando el equipo refactoriza el modelo de dominio (renombra un campo, mueve una propiedad), los consumidores se rompen sin previo aviso.

Contaminar el Dominio con Anotaciones de Serialización

Añadir @JsonProperty, @AvroField, @XmlElement al modelo de dominio mezcla concerns de representación interna y contrato externo. El modelo de dominio debe definirse por las necesidades del negocio, no por las restricciones del formato de serialización.

Mapper que Hace Lógica de Negocio

Un mapper que calcula descuentos, aplica reglas de negocio o toma decisiones basadas en estado es un mapper que ha sobrepasado su responsabilidad. El mapper convierte, no decide. La lógica de negocio debe ejecutarse antes del mapping.

Mapper que Lee de Base de Datos

Un mapper que ejecuta queries SQL para obtener datos adicionales (ej: obtener el nombre del agente a partir del agentId) está mezclando concerns de persistencia con concerns de mapping. Los datos necesarios deben estar disponibles en el objeto de dominio antes de invocar el mapper. Si no están, es un problema de diseño del aggregate.

No Testear el Mapper

Los mappers son código de conversión que puede tener bugs sutiles: campos olvidados, transformaciones incorrectas, null pointer exceptions. Cada mapper necesita tests unitarios que verifiquen que un objeto de dominio conocido produce el mensaje esperado.

Compartir DTOs de Mensaje entre Productor y Consumidor

Publicar el DTO del mensaje como una biblioteca compartida (JAR, NuGet package) crea acoplamiento de deployment entre productor y consumidores. Cada consumidor debe tener su propia copia del DTO, generada a partir del schema compartido (Avro, Protobuf), no del código del productor.


18. Conclusión Técnica

Messaging Mapper es un patrón esencial para cualquier sistema que tenga un modelo de dominio no trivial y publique o consuma mensajes. Su valor reside en la separación explícita entre la representación interna de la aplicación y el contrato público de comunicación.

Para un arquitecto de sistemas modernos, las directrices son:

  • Siempre separar dominio de mensaje: incluso si el DTO del mensaje parece idéntico al objeto de dominio hoy, la divergencia futura es prácticamente inevitable. Es más barato crear un mapper simple hoy que refactorizar docenas de consumidores mañana.
  • El mapper es el guardián del contrato: controla explícitamente qué información cruza la frontera del servicio. Es el punto donde se aplica masking de PII, exclusión de datos internos y transformación de formatos.
  • Un mapper por dirección por consumidor: el productor tiene su mapper de salida; cada consumidor tiene su mapper de entrada. No se comparten mappers entre servicios.
  • Testing riguroso: cada mapper necesita tests que verifiquen que la conversión produce el resultado esperado, incluyendo edge cases (nulls, colecciones vacías, valores límite).
  • Schema como contrato: el DTO del mensaje y su schema (Avro, Protobuf, JSON Schema) son el contrato público. El mapper es la implementación privada que cumple ese contrato.

En el contexto asegurador del ejemplo, el PolicyEventMapper permite que el equipo del Policy Service evolucione su modelo de dominio Policy (añadir campos de reaseguro, reestructurar coberturas, cambiar la jerarquía de pólizas) sin impactar a los cuatro servicios consumidores. El mapper absorbe las diferencias entre la complejidad interna del dominio de seguros y la simplicidad necesaria del contrato público de eventos.