Skip to main content
Skip to main content
Back to Blog
System ArchitectureFeatured

Inside CMDM: How We Cut Customer-Data Chaos Across 15 Nissan Regions

A technical deep-dive into the Customer Master Data Management platform we built at Nissan: Kafka event streaming, a golden-record conflict resolution engine, and the CQRS split that made it fast enough for global operations.

Thakur Ganeshsingh
January 2, 2025
16 min read
Share this post:
Inside CMDM: How We Cut Customer-Data Chaos Across 15 Nissan Regions

This is Part 2 of the Web App Evolution series. Part 1 covered the decade-long shift from servlets to agentic AI — this post zooms into a single system from that timeline and takes it apart.

💡

The System: Customer Master Data Management (CMDM)

From September 2018 to November 2020, I led a team of 12 engineers at Nissan Digital India, working across 9 global Sales Finance Business Units (SFBUs). The hardest problem on our plate wasn't a feature — it was a foundation: every regional system had its own version of "the customer."

CMDM was our answer. It became the single source of truth for customer data across 15 regional operations, cut data inconsistencies by 85%, and saved Nissan a substantial amount annually in operational overhead. This post is about how it was actually built.

The Problem: 15 Regions, 15 Different Truths#

Nissan's regional business units — APAC, EMEA, Americas, and others — had each grown their own customer systems over years, sometimes decades. A customer who bought a car in one region and later moved, leased a second vehicle, or contacted support in another region didn't have one record. They had fragments:

  • A sales record in the CRM used by the dealership network
  • A service history entry in a regional after-sales system
  • A finance profile in whatever the local Sales Finance Business Unit had standardized on
  • Marketing preferences in yet another system, often with a different name spelling or address format

None of these systems agreed with each other, and none of them were designed to. Each had its own schema, its own validation rules, its own idea of what a "valid" phone number or address looked like. Multiply that by 15 regions and you get a customer service team that couldn't answer a simple question — "what does this customer actually have with us?" — without phoning three different departments.

The State of Things Before CMDM

target
15+
Regional Systems
Each with its own schema and validation rules
zap
None
Source of Truth
No canonical customer record existed anywhere
clock
Manual
Resolution Path
Cross-referencing systems by phone, one region at a time

Buying bigger regional databases wasn't the fix — the problem wasn't capacity, it was disagreement. We needed an architecture that could hold 15 regions' worth of conflicting data and still produce one answer per customer, in near real time, without forcing every regional team to rip out and replace their existing systems (which was politically and operationally not going to happen).

The Shape of the Solution#

We landed on an event-driven microservices architecture with Apache Kafka as the backbone, a golden record master data repository as the single source of truth, and a CQRS split so that the write-heavy conflict resolution path never competed with the read-heavy customer lookup path.

📊Mermaid Chart
Rendering chart...

At a glance, four architectural decisions did most of the work:

  1. Event-driven ingestion instead of polling regional databases
  2. A golden record model instead of trying to make 15 systems agree on write
  3. CQRS instead of one database serving both writes and dashboard-style reads
  4. Business-rule-driven conflict resolution instead of "last write wins"

Let's go through each one, because each was a genuine trade-off, not a default.

Decision 1: Event-Driven Ingestion Over Polling#

Our first instinct — and our first mistake in a proof-of-concept — was a nightly batch ETL job pulling from each regional database. It worked, technically. It also meant a customer's data could be up to 24 hours stale, and a failed job on a Tuesday meant EMEA's numbers didn't show up until Wednesday's retry. For a customer service rep on the phone, "check back tomorrow" is not an acceptable answer.

We moved to change data capture (CDC) on each regional system, publishing change events onto Kafka topics partitioned by region and entity type:

Java
@Component
public class CustomerChangeEventConsumer {

    private final GoldenRecordService goldenRecordService;
    private final ConflictResolutionEngine conflictEngine;

    @KafkaListener(
        topics = "customer-change-events",
        groupId = "cmdm-integration-layer",
        concurrency = "6"
    )
    public void onCustomerChanged(ConsumerRecord<String, CustomerChangeEvent> record) {
        CustomerChangeEvent event = record.value();

        // Immutable, append-only log: we never mutate the incoming event,
        // only append downstream decisions derived from it.
        CustomerRecordCandidate candidate = CustomerRecordCandidate.fromEvent(event);

        Optional<GoldenRecord> existing =
            goldenRecordService.findByCustomerKey(candidate.getCustomerKey());

        GoldenRecord resolved = existing.isPresent()
            ? conflictEngine.merge(existing.get(), candidate)
            : GoldenRecord.fromCandidate(candidate);

        goldenRecordService.upsert(resolved);
    }
}

Each regional system kept writing to its own database exactly as before — nobody had to change their application. A lightweight CDC connector watched the transaction log and emitted an event per change. That single decision is what made the migration politically viable: regional teams didn't have to trust a new system with their writes, only let us listen.

The trade-off was consistency model: we accepted eventual consistency with a reconciliation process, rather than trying for synchronous, strongly consistent writes across 15 regions (which would have meant every regional write blocking on a cross-continent round trip — a latency and availability problem waiting to happen).

Decision 2: The Golden Record, Not a Sync Protocol#

Early design conversations kept drifting toward "let's build bidirectional sync between all the regional systems." We killed that idea quickly once we sketched it: 15 systems syncing pairwise is 105 relationships to maintain, each with its own conflict semantics. It doesn't scale, and it doesn't actually answer "what is the truth" — it just multiplies the places where two systems can disagree.

Instead, CMDM's master data repository became the golden record: the one place where a customerKey resolves to a single, versioned, audited profile. Regional systems remained systems of entry; CMDM became the system of record.

Java
@Entity
@Table(name = "golden_customer_record",
       indexes = @Index(name = "idx_customer_key_region", columnList = "customer_key, region"))
public class GoldenRecord {

    @Id
    private String customerKey;          // deterministic identity, not a regional PK

    private String canonicalName;
    private String canonicalEmail;
    private String canonicalPhone;

    @ElementCollection
    private Map<String, FieldProvenance> fieldSources; // which region "won" each field, and why

    @Version
    private Long version;                // optimistic locking for concurrent merges

    private Instant lastReconciledAt;
}

The fieldSources map matters more than it looks. Every field on the golden record remembers which region's data it came from and why it won — because "single source of truth" without an audit trail is just a black box that compliance and support teams won't trust the first time it looks wrong.

Decision 3: Conflict Resolution Is a Business Problem, Not a Technical One#

This was the part that took the longest to get right, and it's the part generic "master data management" tutorials skip. "Last write wins" is trivial to implement and wrong often enough to erode trust in the platform within a week.

Here's roughly the shape of the rules engine we ended up with, after two rounds of getting it wrong:

Java
@Component
public class ConflictResolutionEngine {

    private final List<FieldMergeRule> rules; // ordered, most-authoritative-first

    public GoldenRecord merge(GoldenRecord existing, CustomerRecordCandidate incoming) {
        GoldenRecord.Builder merged = existing.toBuilder();

        for (FieldMergeRule rule : rules) {
            FieldDecision decision = rule.resolve(existing, incoming);
            if (decision.shouldOverride()) {
                merged.setField(rule.getFieldName(), decision.getValue());
                merged.recordProvenance(rule.getFieldName(), decision.getSourceRegion(), decision.getReason());
            }
        }

        return merged.build();
    }
}

// Example rule: the region that legally "owns" a customer's finance
// relationship is authoritative for contact details, regardless of
// which region's event arrived most recently.
@Component
public class SfbuOwnershipRule implements FieldMergeRule {
    @Override
    public FieldDecision resolve(GoldenRecord existing, CustomerRecordCandidate incoming) {
        boolean incomingIsOwningSfbu = incoming.getRegion().equals(existing.getOwningSfbu());
        return incomingIsOwningSfbu
            ? FieldDecision.override(incoming.getContactDetails(), incoming.getRegion(), "owning-SFBU")
            : FieldDecision.keep();
    }
}

The key realization: recency, completeness, and business ownership all had to be weighed differently per field. A customer's legal owning SFBU was authoritative for finance and contact details even if a different region's event arrived more recently. But vehicle service history was authoritative from whichever region's service center actually performed the work, full stop — recency didn't matter there at all, ownership did. Encoding that as an ordered set of rules, instead of one generic "newest wins" function, is what got us from a system nobody trusted to one that reached 99.9% consistency in practice.

The lesson that mattered most

The technical mechanism for merging records is the easy 20%. The hard 80% is sitting down with each SFBU and agreeing, field by field, on who is allowed to be right. Skip that conversation and no amount of Kafka or CQRS saves you.

Decision 4: CQRS to Stop Writes and Reads From Fighting Each Other#

Once the golden record was reliably reconciling, we hit a second problem: customer service dashboards ran expensive aggregate queries against the same tables the conflict engine was writing to. Under regional load spikes (month-end finance close was the worst offender), read latency for support reps would spike right when they needed it least.

We split the model:

  • Write side: the conflict resolution engine and golden record repository, optimized for correctness and auditability, partitioned by region for write locality.
  • Read side: denormalized, cached projections rebuilt from the same Kafka event stream, optimized for the actual query patterns customer service and the mobile app used — "give me everything about customer X" in one round trip.
Java
@KafkaListener(topics = "golden-record-updated", groupId = "cmdm-read-model-projector")
public void onGoldenRecordUpdated(GoldenRecordUpdatedEvent event) {
    CustomerReadView view = CustomerReadView.projectFrom(event);
    redisTemplate.opsForValue().set(
        "customer:" + event.getCustomerKey(),
        view,
        Duration.ofHours(6)
    );
    readViewRepository.save(view); // durable copy behind the cache
}

Reads never touched the write-side tables again. That single change is where most of the 60% faster customer service resolution came from — not a faster database, but the right database for each side of the workload.

We didn't get CQRS right on the first attempt — our first "read model" was just a read replica of the write database, which meant a bad write-side query could still starve reads. It wasn't until the read model was rebuilt from Kafka events independently, with its own schema, that the two workloads actually stopped competing.

A
A lesson from the rollout
Lead Software EngineerNissan Digital India

Where the Complexity Actually Lived#

Technical Challenges & Solutions

🟢
0
Low
🟡
0
Medium
🟠
1
High
🔴
1
Critical

Global Data Synchronization at Scale

Scalability🔴Critical
Challenge

Maintaining real-time data consistency across 15+ regional systems with different data formats, validation rules, and network latencies, while guaranteeing no data loss during regional outages.

Solution

Event-driven architecture on Apache Kafka with durable, replayable topics, plus a reconciliation job that re-walks unresolved conflicts on a schedule.

Approach
Designed an event-sourcing pattern with immutable, append-only event logs per region
Built the conflict resolution engine with business-rule ordering rather than timestamp-only logic
Accepted eventual consistency with scheduled reconciliation instead of synchronous cross-region writes
Built dashboards tracking synchronization lag per region so a stalled consumer group was visible within minutes, not days
Outcome

99.9% data consistency across all 15 regions with sub-second propagation for the common case

Business Impact

Enabled a genuinely unified customer view instead of a best-effort approximation

Lessons Learned
💡Event-driven architecture is close to mandatory once you're past 3-4 independent regional systems
💡Conflict resolution rules must be owned by the business, encoded by engineering — not the other way around
💡You cannot operate a system like this without per-region lag monitoring from day one

Performance at Millions of Records Without Starving Writes

Technical🟠High
Challenge

Supporting millions of customer records with continuous write-side updates while still returning sub-second responses for customer service lookups.

Solution

CQRS with an independently-rebuildable read model, Redis-backed caching for hot lookups, and geographic partitioning on the write side.

Approach
Partitioned the golden record repository by region for write locality
Rebuilt read-side projections from the Kafka event stream rather than replicating write-side tables
Cached the most frequently accessed customer views with a 6-hour TTL backed by a durable store
Tuned indexes around actual customer-service query patterns, not the write schema's natural keys
Outcome

Sub-second customer lookups sustained even during month-end regional load spikes

Business Impact

60% improvement in customer service resolution time

Lessons Learned
💡A read replica of your write database is not a read model — it still shares the write schema's bottlenecks
💡Caching strategy has to be built around how support teams actually query, not how the data is stored
💡Partitioning decisions made for write throughput and query patterns needed for reads are two different design problems

Key Takeaways

Global data management is a business-alignment problem before it's a technical one
Event-driven architecture buys you decoupling, but you pay for it in observability requirements
CQRS earns its complexity the moment reads and writes have genuinely different performance profiles

We also had to build a compliance layer — GDPR and regional data-protection rules meant the audit trail on every golden-record field (that fieldSources provenance map) wasn't a nice-to-have, it was what let us answer "why does this record say what it says" during an actual audit. That's a governance story more than an architecture one, so I'm not going to stretch this post to cover it in depth — but it's worth knowing it rode on the same event log and versioning we'd already built for conflict resolution, rather than needing a separate system.

The Architecture Decisions, Side by Side#

Architectural Decisions & Design Patterns

Architectural Principles

Event-driven architecture for real-time-enough consistency without coupling regional systems
Golden record as single source of truth, regional systems as systems of entry
Business-rule-driven conflict resolution, never generic last-write-wins
CQRS wherever read and write workloads have genuinely different shapes

Design Patterns Used

Event SourcingCQRSChange Data CaptureGolden RecordCircuit Breaker

Event-Driven Microservices Over Polling ETL

Problem

Need real-time-enough data consistency across 15+ regional systems without forcing regional teams to change how they write data

Solution

Change data capture on each regional system publishing to Kafka, consumed by an independent integration layer

Rationale

Decouples regional systems from CMDM entirely — they keep writing exactly as before, we only listen. Failure in the integration layer never blocks a regional write.

Trade-offs
Eventual consistency instead of strong consistency
Debugging a distributed event flow is harder than debugging a single database
Impact

Sub-second propagation for the common case, 99.9% consistency after reconciliation

Golden Record Over Pairwise Sync

Problem

15 regional systems each held a partial, sometimes contradictory view of the same customer

Solution

A single master data repository holding one authoritative, versioned, audited record per customer, with regional systems remaining systems of entry

Rationale

Pairwise synchronization between N systems grows as N², and still doesn't answer 'what's true' — it just adds more places to disagree

Trade-offs
Migration required buy-in from every regional team to accept a new system of record
Field-level provenance tracking added storage and query overhead
Impact

85% reduction in data inconsistencies across all touchpoints

CQRS for Read/Write Separation

Problem

Write-heavy conflict resolution and read-heavy customer service dashboards were contending for the same database, and it showed worst exactly when it mattered most

Solution

Independent read-side projections rebuilt from the Kafka event stream, cached separately from the write-side golden record store

Rationale

Reads and writes had fundamentally different performance profiles and query shapes; forcing one schema to serve both was the actual bottleneck, not raw hardware

Trade-offs
Read model can lag the write model by the time it takes to project an event
Two schemas to reason about instead of one
Impact

60% improvement in customer service resolution time

Leading the Build#

Team Leadership & Management

Team Size & Structure

12 engineers across backend, data, and DevOps specializations

Backend Engineers4
Data Engineers3
DevOps Engineers2
QA Engineers2
Technical Lead1

Leadership Approach

Technical architect and lead engineer, hands-on across the conflict resolution engine and Kafka integration layer
Cross-functional coordination across Nissan's 9 global Sales Finance Business Units to agree on field-level ownership rules
Direct collaboration with compliance and legal teams on the audit-trail and data-retention requirements
Ran the team on 3-week sprint cycles with continuous delivery to production, region by region

Management Methodologies

AgileEvent-Driven DesignMicroservices ArchitectureDevOps

Leadership Achievements

85% fewer data inconsistencies

Across all 15 regional touchpoints after the golden-record rollout

Considerable annual savings
99.9% data consistency

Sustained across global operations after reconciliation

Sub-second propagation for the common case
60% faster customer service resolution

From the CQRS read-model split relieving contention on the write path

Leadership Outcomes

Delivered the platform across all 15 regional operations
Reached 99.9% data consistency across global operations
Established conflict-resolution and CQRS patterns later reused by other data platform teams at Nissan

What I'd Tell a Team Starting This Today#

Start with the event log, not the database schema. Every hard problem we solved — conflict resolution, read/write separation, compliance auditing — turned out to be easier once "what happened and when" was captured as an immutable event stream. The database schema is a projection of that log, not the source of truth.

Treat conflict resolution as a business-rules problem you'll revisit constantly. We shipped, then had to go back and rework field-ownership rules more than once as SFBUs raised edge cases we hadn't anticipated. Build the rules engine to be changed, not just to be correct on day one.

Don't build CQRS until the contention is real. We didn't split read and write models until dashboard queries were visibly starving the conflict resolution engine under load. Building it earlier would have been complexity we paid for before we needed it.

Observability is not optional in an event-driven system. The single biggest operational risk in this architecture is a stalled or lagging consumer that nobody notices for hours. Per-region synchronization lag needs to be a first-class metric, not an afterthought.


Next in this series: a look at the developer-experience side of scaling a platform team — how the "First 100" onboarding strategy and Super User Activation Process grew Freshworks' marketplace developer base from zero to 10,000+.

Have you built or maintained a master data platform across multiple regions? I'd be glad to compare notes — reach out on LinkedIn.

Thakur Ganeshsingh
Thakur Ganeshsingh
Engineering Manager - Developer Relations at Freshworks