Loading learning content...
In a monolithic application, data ownership is rarely a conscious decision. All code shares the same database, and any module can read or write any table. This simplicity—while operationally convenient—hides a fundamental architectural tension that explodes when you move to microservices.
The moment you distribute your system, a critical question emerges: Who owns this data? When two services both read and write to the same customer record, which one is authoritative? When they disagree, who wins? When one service updates the record, how does the other find out?
This isn't an abstract concern. In poorly-designed distributed systems, data ownership ambiguity creates cascading failures, corrupted state, and debugging nightmares that can take weeks to resolve. Senior engineers at companies like Amazon, Netflix, and Google have learned—often painfully—that establishing single source of truth is the foundation upon which all other microservices patterns depend.
By the end of this page, you will understand what 'single source of truth' means in microservices, why it's essential for system integrity, how to identify data owners, and the architectural principles that ensure each piece of data has exactly one authoritative source. These concepts form the bedrock of all data management in distributed systems.
Single Source of Truth (SSOT) is an architectural principle stating that for any piece of data, there must be exactly one service or system that is the authoritative owner of that data. The owner is responsible for:
This doesn't mean other services can't have copies of the data—they often must, for performance and autonomy. But those copies are explicitly derivative: they are projections, caches, or local views that are synchronized from the source. When conflicts arise, the source always wins.
Think of SSOT like a master recording in music production. There may be thousands of copies on CDs, streaming services, and personal devices, but when someone asks 'What is the official version of this song?', there's exactly one answer: the master. If the artist re-masters the song, all copies should eventually reflect that change. The same principle applies to data in distributed systems.
The mathematical perspective:
In formal terms, SSOT establishes a partial ordering on truth claims. If service A owns entity X, and services B and C have copies, then:
truth(X) = state_in_A(X)
state_in_B(X) ≤ truth(X) (B's view may be stale)
state_in_C(X) ≤ truth(X) (C's view may be stale)
This ordering is crucial because it means conflicts are resolvable. Without SSOT, you face the fundamental problem of distributed consensus: multiple services claim to own truth, leading to split-brain scenarios where the system can't determine which state is correct.
| Scenario | With SSOT | Without SSOT |
|---|---|---|
| Two services update same entity | Owner service validates; rejects conflicting update | Both updates may succeed; data corruption |
| Query returns different values | Source is canonical; copies are stale | No way to determine which is correct |
| Service failure during write | Owner's state is truth; recovery is deterministic | Unknown what data was actually committed |
| Business rule enforcement | Single service enforces invariants | Rules may be enforced inconsistently |
| Audit trail | Single authoritative history | Fragmented, potentially conflicting logs |
The importance of SSOT becomes viscerally clear when you've debugged a production incident caused by its absence. Understanding these failure modes helps you appreciate why experienced architects insist on ownership clarity.
A major e-commerce company discovered that 0.3% of orders had inconsistent state between their Order and Inventory services—neither service 'owned' reservation data. This affected ~50,000 orders per month, requiring manual reconciliation and costing millions in refunds, customer service, and engineering time. Implementing SSOT took 6 months; they estimated the ambiguity cost was $4M/year.
The deeper principle:
SSOT isn't just about avoiding bugs—it's about making the system reasonably understandable. A system where any component can modify any data becomes cognitively intractable. Engineers can't reason about behavior because effects can come from anywhere.
With SSOT, you can draw a clear mental model: 'To understand customer data, I look at the Customer service. Only the Customer service can change it. Other services consume it.' This locality of reasoning is essential for maintaining complex systems over time.
Determining which service should own which data is one of the most important architectural decisions in microservices design. The answer comes from understanding your domain—the business processes, entities, and rules that your system models.
The Domain-Driven Design Connection:
Domain-Driven Design (DDD) provides the vocabulary for ownership discussions. Each Bounded Context is a distinct model of a subset of the domain. Within a context, entities have clear definitions and ownership. Between contexts, the same real-world concept may have different representations.
For example, 'Customer' means different things to different contexts:
Each context owns its view of the customer. The Sales service owns sales-related customer data. The Billing service owns billing-related customer data. This isn't duplication—it's acknowledging that these are different models serving different purposes.
| Entity | Owner Service | Rationale |
|---|---|---|
| User Account | Identity Service | Creates accounts; manages authentication |
| User Profile | Profile Service | Manages display name, preferences, avatar |
| Order | Order Service | Creates orders; manages order lifecycle |
| Inventory Quantity | Inventory Service | Enforces 'never oversell' invariant |
| Product Catalog | Catalog Service | Manages product information and pricing |
| Shipment | Shipping Service | Creates shipments; tracks delivery |
| Payment | Payments Service | Processes payment; manages transaction state |
| Review | Reviews Service | Manages customer reviews and ratings |
When an entity seems to belong to multiple services equally, you've likely discovered either (a) a missing service that should own it, (b) an entity that should be split into multiple bounded concepts, or (c) a service boundary that's drawn incorrectly. Shared ownership is almost never the right answer.
Establishing SSOT requires more than philosophical agreement—you need concrete architectural patterns that enforce ownership at the system level. Here are the primary patterns used in production systems.
The foundational pattern for SSOT is that each service has its own private database that no other service can access directly. All data access goes through the service's API.
This is the default in modern microservices. Sharing databases across services is considered an anti-pattern precisely because it violates SSOT.
For entities that must be referenced across many services (like Customer or Product), establish a golden record in the owner service. Other services receive copies through events or APIs but treat their local data as a cache.
Golden Record (Owner):
- Customer entity in Customer Service database
- Only Customer Service can write
- Publishes CustomerUpdated events
Derived Records (Consumers):
- Order Service has customer_name, customer_email (for display)
- Shipping Service has customer_address (for shipping)
- Each maintains their own copy, updated from events
- Each knows their copy may be stale
The key discipline: consumers never modify their copies based on local logic. All changes flow from the owner.
When a user action affects data owned by multiple services, route commands to the appropriate owner rather than having one service update another's data.
User Action: 'Update my profile and change my password'
Wrong Approach:
- Frontend calls Profile Service
- Profile Service updates profile AND calls Identity Service to change password
Correct Approach:
- Frontend sends UpdateProfile command to Profile Service
- Frontend sends ChangePassword command to Identity Service
- Each service handles its own data
This keeps ownership clear and prevents services from needing write access to each other's domains.
In event-sourced systems, ownership is determined by which service publishes events for an entity. Only the Customer service publishes CustomerRegistered, CustomerProfileUpdated, CustomerDeactivated events. Other services subscribe but never publish events about customers.
This creates an auditable, append-only log where ownership is explicit in the event stream.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
// ===================================================// OWNERSHIP ENFORCEMENT: Customer Service// ===================================================// The Customer Service is the SINGLE SOURCE OF TRUTH// for all customer profile data. It:// 1. Creates customers (assigns IDs)// 2. Validates all profile updates// 3. Publishes events for consumers// // NO OTHER SERVICE should directly modify customer data// =================================================== interface Customer { id: string; email: string; name: string; createdAt: Date; updatedAt: Date; status: 'active' | 'suspended' | 'deleted';} interface CustomerCreatedEvent { type: 'CustomerCreated'; customerId: string; email: string; name: string; timestamp: Date;} interface CustomerUpdatedEvent { type: 'CustomerUpdated'; customerId: string; changes: Partial<Pick<Customer, 'email' | 'name'>>; timestamp: Date;} class CustomerService { private repository: CustomerRepository; private eventPublisher: EventPublisher; // Only the Customer Service creates customers async createCustomer( email: string, name: string ): Promise<Customer> { // Validate uniqueness (our responsibility as owner) const existing = await this.repository.findByEmail(email); if (existing) { throw new ConflictError('Email already registered'); } // Create with generated ID (we control identity) const customer: Customer = { id: generateId(), email, name, createdAt: new Date(), updatedAt: new Date(), status: 'active', }; await this.repository.save(customer); // Publish event for consumers to sync await this.eventPublisher.publish<CustomerCreatedEvent>({ type: 'CustomerCreated', customerId: customer.id, email: customer.email, name: customer.name, timestamp: customer.createdAt, }); return customer; } // Only the Customer Service updates customers async updateCustomer( customerId: string, updates: Partial<Pick<Customer, 'email' | 'name'>> ): Promise<Customer> { const customer = await this.repository.findById(customerId); if (!customer) { throw new NotFoundError('Customer not found'); } // Validate business rules (our responsibility) if (updates.email) { const existing = await this.repository.findByEmail(updates.email); if (existing && existing.id !== customerId) { throw new ConflictError('Email already in use'); } } // Apply updates Object.assign(customer, updates, { updatedAt: new Date() }); await this.repository.save(customer); // Publish change event await this.eventPublisher.publish<CustomerUpdatedEvent>({ type: 'CustomerUpdated', customerId: customer.id, changes: updates, timestamp: customer.updatedAt, }); return customer; }} // ===================================================// CONSUMER SERVICE: Order Service// ===================================================// The Order Service CONSUMES customer data but does NOT own it.// It maintains a local cache updated from CustomerService events.// =================================================== interface LocalCustomerView { customerId: string; name: string; // Cached for order display email: string; // Cached for notifications lastSyncedAt: Date;} class OrderServiceCustomerSync { private localStore: LocalCustomerViewRepository; // Handle events from Customer Service async handleCustomerCreated(event: CustomerCreatedEvent) { await this.localStore.upsert({ customerId: event.customerId, name: event.name, email: event.email, lastSyncedAt: new Date(), }); } async handleCustomerUpdated(event: CustomerUpdatedEvent) { const existing = await this.localStore.findById(event.customerId); if (!existing) return; // We might not have this customer // Apply only the changes we care about if (event.changes.name) existing.name = event.changes.name; if (event.changes.email) existing.email = event.changes.email; existing.lastSyncedAt = new Date(); await this.localStore.save(existing); } // Note: OrderService NEVER calls CustomerService to update // customer data. It only reads its local view.}Real systems occasionally present scenarios that seem to violate SSOT. Understanding these cases helps you maintain ownership discipline while solving practical problems.
Scenario: Country codes, currency codes, tax rates—reference data used everywhere.
Solution: Reference data either:
The key is consistency: either all services source from one place, or changes are managed through deployment, not runtime updates.
Scenario: An 'Order' seems to include shipping address (Shipping domain), payment info (Billing domain), and product details (Catalog domain).
Solution: The Order Service owns the Order entity, which contains:
The Order Service doesn't own the current state of products or addresses—it owns the order's view of them at the moment of ordering. This is proper SSOT: Order Service owns order data, including what that order looked like when placed.
Scenario: A dashboard needs data from five services. Does the Dashboard Service own the computed metrics?
Solution: The Dashboard Service owns its computed aggregates but not the source data. It:
This is additive ownership: the Dashboard Service adds new owned data (aggregates) without taking ownership from source services.
Scenario: User updates their address, which affects both Profile (name, avatar) and Shipping (delivery address) domains.
Solution: Recognize these as two separate entities:
The UI collects both inputs but sends separate commands to each owner. If they must be atomic, consider a saga pattern coordinated by an orchestrator—but each service still owns its data.
When in doubt, ask: 'If this data is wrong, which team do I call to fix it?' That team's service is the owner. If the answer is 'multiple teams', you have an ownership problem to resolve.
A common concern with strict SSOT is increased coupling: if every service must call the Customer Service for customer data, doesn't that make Customer Service a bottleneck and single point of failure?
This concern is valid but addressable. SSOT doesn't require synchronous dependencies—it requires clear ownership. Here's how to maintain ownership while reducing operational coupling.
The key insight: SSOT is about write authority, not read availability. The Customer Service is the only service that can change customer data, but other services can read their own locally-cached copies.
This separation allows:
Most business processes tolerate slight staleness. If a customer updates their name, it's acceptable if their next order (placed 100ms later) shows the old name. What's not acceptable is permanent inconsistency or data corruption—which SSOT prevents.
Conway's Law states that system architectures mirror organizational structures. This applies powerfully to data ownership: services should be owned by teams, and those teams should own the corresponding data domains.
Misalignment between team structure and data ownership creates constant friction:
Alignment principles:
One team, one service, one domain — The team that owns the Customer Service also owns customer data and customer business logic. They are the experts; they make decisions.
API contracts as team interfaces — Teams interact through service APIs, just as services do. Changing a service contract is a negotiation between teams.
Domain expertise concentrates — Over time, the Customer team becomes the organization's expert on customer data. Other teams defer to their judgment on customer-related questions.
Ownership includes on-call — The owning team is responsible when customer data has issues. This accountability reinforces careful stewardship.
| Aspect | Aligned | Misaligned |
|---|---|---|
| Who adds customer fields? | Customer team | Any team that 'needs' it |
| Who validates customer rules? | Customer team | Caller's responsibility |
| Who pages when data is wrong? | Customer team | Whoever noticed |
| Who decides customer data retention? | Customer team + compliance | Unknown or assumed |
| Who reviews customer data access? | Customer team | Security team (without context) |
If your organization can't agree on which team owns customer data, your services won't be able to agree either. Resolving ownership is first an organizational conversation, then a technical implementation.
Single Source of Truth is the foundational principle for managing data in distributed systems. Without it, systems devolve into chaos—data corruption, inconsistent enforcement, and impossible debugging. With it, you have clear ownership, deterministic behavior, and teams that understand their responsibilities.
What's next:
With single source of truth established, we next address a practical reality: data duplication. Even with clear ownership, services often need copies of data for performance and autonomy. The next page explores the trade-offs of data duplication, when it's appropriate, and how to manage it without violating ownership principles.
You now understand Single Source of Truth—the foundational principle of data ownership in microservices. Every data entity needs exactly one authoritative owner, and this ownership drives system clarity, maintainability, and correctness. Next, we'll explore how to manage the inevitable duplication that arises when services need local copies of owned data.