⏱ 21 min read
Microservices are often sold with a kind of glossy certainty: split the monolith, give each service autonomy, and everything gets faster, safer, and easier to change. Then reality arrives wearing a badge from finance, operations, or customer service. The teams carve out services, deploy them independently, and quietly keep the same database underneath. At first it feels pragmatic. No painful data migration. No duplicate storage. No hard conversations about ownership. Just a few new services pointing at familiar tables.
That convenience is usually borrowed, not earned.
A shared database between multiple services is the architectural equivalent of several families living in one house with a single kitchen, one fuse box, and a lot of passive-aggressive notes on the refrigerator. Everyone tells themselves they are independent. Nobody really is. One team adds an index and another team’s nightly import job changes behavior. A “harmless” schema change becomes a cross-program negotiation. Service boundaries look clean on slides and collapse under a SQL query.
This is why the shared database anti-pattern keeps showing up in microservices programs, especially in large enterprises. It promises incremental change, but often freezes the most important kind of change: domain evolution. If you want services to own business capabilities, deploy at different speeds, and scale according to business demand, then data ownership is not a side issue. It is the center of the matter.
And yet, the story is not as simple as “never share a database.” Architecture earns its keep in the gray areas. There are cases where a shared database is a temporary migration bridge, a useful transitional compromise, or even the least bad choice. But you should call it what it is: a compromise with consequences.
This article digs into the shared database anti-pattern through a practical enterprise lens: domain-driven design, migration strategy, Kafka-based integration, progressive strangler approaches, reconciliation, operational realities, and the failure modes that make support teams lose sleep. event-driven architecture patterns
Context
Microservices are not merely small deployable units. They are a way of organizing software around business capabilities. That distinction matters. A service should own a cohesive domain concept, make decisions within that boundary, and evolve without requiring synchronized changes across the estate.
In domain-driven design terms, each microservice should align with a bounded context. A bounded context is not a technical partition. It is a semantic one. It defines a consistent model and language for a part of the business. “Customer,” for example, means one thing in billing, another in marketing, and yet another in fraud. If all services share the same CUSTOMER table, the architecture is declaring—whether intentionally or not—that there is one universal truth model for all those contexts.
That declaration is usually false.
Enterprises inherit this anti-pattern in predictable ways:
- A monolith is decomposed, but the original relational schema remains untouched.
- Teams create new APIs while continuing to read and write legacy tables directly.
- Reporting requirements push everyone toward one “golden” operational schema.
- Program timelines prioritize UI or channel separation, leaving the data model centralized.
- Governance groups confuse standardization with reuse and insist on shared master tables for everything.
The result is often called “microservices” because the runtime topology has many deployables. But the behavior remains that of a distributed monolith. The coupling moved from method calls to SQL. That is not progress. It is a more expensive form of the same problem. microservices architecture diagrams
Problem
The shared database anti-pattern occurs when multiple services directly access the same database schema, tables, or persistence structures, creating hidden coupling through shared data.
The obvious issue is schema coupling. If Service A and Service B both depend on the orders table, then a schema change for one is a breaking change for both. Database release coordination becomes mandatory. Independent deployment becomes fiction.
The deeper issue is semantic erosion.
A service boundary without data ownership is a border drawn in pencil. Teams begin by sharing a few tables. Then they share reference tables. Then they join across schemas “for performance.” Then they write convenience views. Soon one service is reading another service’s state because “the API is too slow” or “we just need this for a report.” The data model becomes the real integration layer, and the APIs become decorative.
This creates three kinds of damage.
First, it damages autonomy. Teams cannot move at different speeds because the shared schema is common property. Common property in software suffers the same fate as common property anywhere else: everybody depends on it, nobody fully owns it, and change becomes political.
Second, it damages correctness. Services begin to bypass domain rules embedded in each other’s business logic. A table update that looks syntactically valid may be semantically invalid. The database happily accepts it. The business does not.
Third, it damages observability and recovery. When multiple services write to the same tables, tracing the source of corruption becomes harder. Was the bad state caused by an API defect, a batch load, a compensation job, or a manual SQL fix at 2 a.m.? Shared persistence multiplies ambiguity.
The architecture diagram looks modular. The incident report tells the truth.
Forces
Architects should resist easy sermons here, because the shared database anti-pattern survives for reasons that are often rational in the short term.
Pressure for rapid decomposition
Many organizations need to break apart a monolith under deadline pressure: cloud migration, digital channel growth, regional rollout, M&A integration, or vendor exit. Leaving the database shared appears to reduce risk. Teams can extract services around the edges and defer the hardest part—data separation. cloud architecture guide
Existing investment in relational integrity
Enterprise systems often rely heavily on relational constraints, normalized schemas, and shared reference data. The database has become the institutional memory of the business. Moving to service-owned data feels like surrendering consistency and reporting simplicity.
Reporting and analytics demands
Operational data is frequently mined directly by downstream systems, BI platforms, regulatory reports, and finance reconciliations. Splitting data ownership threatens those consumers. The shared database becomes a political compromise: operationally dubious, institutionally convenient.
Legacy integration gravity
Batch jobs, ETL pipelines, stored procedures, and vendor products may already be bound tightly to the existing schema. Services that diverge from it risk breaking a web of hidden dependencies.
Performance fears
Developers often prefer direct joins over remote calls or event-driven replication because the former feels simple and fast. A single SQL query can retrieve what would otherwise require multiple service interactions. The trap is that local convenience becomes systemic coupling.
Governance culture
Some enterprises are uncomfortable with duplication. They interpret separate copies of data across services as waste or inconsistency. They want a canonical model, one master schema, and centrally governed definitions. That instinct sounds disciplined. In a microservices architecture, it often produces paralysis.
These forces are real. Ignoring them leads to architecture theater. Good architecture acknowledges why teams choose the anti-pattern before explaining why they should eventually leave it behind.
Solution
The primary remedy is straightforward to state and painful to implement: each microservice owns its data.
Ownership means more than having a logical schema. It means one service is the authoritative source for a domain concept within its bounded context, and other services access that information through published interfaces, domain events, or replicated read models—not by reaching into the owner’s tables.
This is where domain-driven design matters. The question is not “how do we split the database?” The question is “what business capabilities deserve their own model, rules, lifecycle, and language?” Once those bounded contexts are identified, data ownership follows.
For example:
- Order Service owns order lifecycle, pricing decisions attached to a sale, and fulfillment state relevant to order management.
- Billing Service owns invoices, payment allocations, account balances, and settlement semantics.
- Customer Profile Service owns profile preferences and consent state.
- Fraud Service may maintain its own risk view of the customer entirely separate from the operational profile.
Each service may store overlapping identifiers or replicated attributes, but not a shared transactional model. “Customer” in one service is not obligated to be the same aggregate as “Customer” in another. That is not inconsistency. That is bounded context discipline.
A common implementation pattern is event-driven integration, often using Kafka. When a service changes significant business state, it publishes domain events. Other services subscribe and build their own local projections, caches, or process state. This introduces eventual consistency, which many teams resist at first. But eventual consistency with clear ownership is usually a healthier trade than immediate consistency with systemic coupling.
The key is to treat replication as intentional, not accidental. Replicated data should be small, explicit, versioned, and traceable to an owning source.
Here is the anti-pattern in simple form:
It looks compact. It also means every service is coupled through the same persistence substrate.
A healthier target is service-owned data with asynchronous integration:
This is not free. It introduces new responsibilities: schema evolution in events, idempotent consumers, replay handling, reconciliation, and explicit consistency boundaries. But those are the honest costs of autonomy.
Architecture
A sound architecture for avoiding the shared database anti-pattern usually combines five ideas.
1. Bounded contexts before technology boundaries
Do not start by assigning teams to tables. Start with domain semantics. Use event storming, capability mapping, and journey analysis to identify where the business language changes meaning.
A useful test is this: if two parts of the system argue about the meaning of the same noun, they probably belong in different bounded contexts. “Account,” “Product,” “Case,” and “Customer” are famous troublemakers.
2. Service-owned write models
Every service should have authority over its own write model. No other service should perform direct inserts, updates, or deletes on that data. If an external process needs to influence state, it does so through an API, command, or event that the owning service interprets according to its own rules.
This protects invariants. A database row is not the business. It is merely one representation of business state.
3. Data products for read access
Cross-service queries are inevitable in enterprise landscapes. The answer is not direct table access. The answer is to create read-optimized views through APIs, materialized projections, search indexes, or analytic pipelines.
This often means accepting denormalization. Purists hate that word. Operations teams usually prefer it to synchronized outages.
4. Event-driven propagation with Kafka
Kafka is useful here because it decouples producers from consumers, supports replay, and handles enterprise-scale throughput. It is not magic. Teams still need event contracts, partitioning strategy, consumer lag monitoring, exactly-once skepticism, and compensation design. But for propagating domain changes between service-owned data stores, it is often a strong fit.
The practical pattern is:
- service commits local transaction
- service emits domain event, usually via transactional outbox
- Kafka transports event
- consumers update local projections or trigger workflows
- reconciliation jobs detect missed or malformed updates
5. Reconciliation as a first-class capability
If you have multiple service-owned data stores, you will eventually need reconciliation. Events can be delayed, consumers can fail, messages can be malformed, schemas can drift, and backfills can go wrong. Mature architectures plan for this from day one.
Reconciliation is not an admission of failure. It is an admission that distributed systems are real.
Typical reconciliation techniques include:
- periodic comparison of source-of-truth records against downstream projections
- replay from Kafka offsets or archived topics
- compensating commands for missed state transitions
- audit trails with causation and correlation IDs
- exception queues for manual investigation
A service landscape without reconciliation is a service landscape that has not yet seen production.
Migration Strategy
The most dangerous instruction an architect can give is “just split the database.” That is how large programs die in steering committees.
Migration away from a shared database must be progressive, observable, and reversible where possible. This is where the strangler pattern earns its reputation. You do not replace the data architecture in one grand move. You carve out bounded contexts one by one and gradually reduce direct dependence on the shared schema.
A practical migration approach looks like this.
Step 1: Map actual data dependencies
Not the documented ones. The real ones.
You need an inventory of:
- services and applications reading each table
- write paths including batch jobs and manual scripts
- stored procedures, triggers, views, and ETL dependencies
- regulatory reports and downstream consumers
- SLA expectations around freshness and consistency
In enterprises, the scariest dependencies are usually outside the main codebase.
Step 2: Identify a viable bounded context
Pick a domain slice with clear business ownership and manageable upstream/downstream dependencies. Product catalog, customer notifications, or case comments are often better first candidates than pricing, settlement, or core ledger functions.
Do not begin with the most politically central table in the company.
Step 3: Introduce an anti-corruption layer
Wrap access to the shared schema through a service boundary. Initially this service may still use the old tables. The goal is not immediate data separation. The goal is to stop new direct consumers from coupling to the schema.
This is a crucial turning point. Once access is mediated, the schema can begin to change without every consumer being in the room.
Step 4: Create a service-owned store
Stand up a new database for the bounded context. Start writing new business state there. Replicate needed legacy data into it, or dual-write through a controlled mechanism during transition. Avoid ad hoc dual writes from random callers; use a transactional outbox or orchestrated migration path.
Step 5: Publish events and build downstream projections
As the new service takes ownership, publish domain events for changes that matter externally. Downstream services and reporting consumers begin switching from the shared database to service APIs, Kafka topics, or local projections.
Step 6: Reconcile aggressively
During migration, expect divergence. Run parallel validation:
- compare counts
- compare critical field values
- compare lifecycle transitions
- audit missing or duplicate events
- monitor lag and replay behavior
Teams often underfund this phase because it looks like temporary plumbing. It is the plumbing that prevents board-level incident reviews.
Step 7: Switch reads, then remove legacy writes
Move consumer by consumer off the shared tables. Once no legitimate external writes remain, lock down access. Eventually archive or retire the old schema area.
The journey is usually iterative, not linear. Some contexts move quickly. Others remain partially shared for longer because the business risk is too high. That is acceptable if the architecture makes the compromise explicit and reduces it over time.
Here is what a progressive strangler migration often looks like:
In the early phase, the new service may still read from the legacy database while building its own store and event stream. Over time, reads and writes are shifted until the legacy dependency can be cut.
Enterprise Example
Consider a large retail bank modernizing its customer servicing platform. The original system was a 15-year-old monolith with a massive Oracle database. Customer profile, address history, account preferences, KYC flags, marketing consent, and complaint markers all lived in a shared schema. The bank introduced microservices for onboarding, consent management, complaints, and notifications. To save time, each service was allowed to read and write the central customer tables.
For six months, the program looked successful. Teams deployed more frequently. APIs were modernized. A Kafka platform was introduced for some events. Leadership declared the monolith “decomposed.”
Then the cracks opened.
The complaints service added a status field to support regulatory escalation paths. The onboarding team reused it differently for incomplete applications. Notification rules began firing off the wrong state transitions because they queried the table directly and could not distinguish the contexts. Meanwhile, an address update from onboarding triggered a marketing consent refresh in another service due to a database trigger nobody had documented. Support staff saw customer records oscillate across values depending on which process ran last.
The bank had achieved service proliferation, not service autonomy.
The recovery strategy was grounded in bounded contexts. The architecture team split “customer” into distinct contexts:
- Customer Identity for core identity and KYC references
- Customer Contact for addresses and communication channels
- Consent for marketing and legal permission state
- Complaints for case-specific customer views
Each service got its own database. Kafka topics were introduced for CustomerContactChanged, ConsentUpdated, and ComplaintOpened events. A transactional outbox pattern was used to avoid losing events between local commits and message publication. A reconciliation process compared the source systems nightly and raised exceptions for mismatches in critical fields.
This did not make the landscape simpler overnight. It made it honest.
Reporting changed too. Instead of direct SQL against the operational schema, the bank created a governed analytics pipeline fed from service events and curated extracts. Finance resisted at first because the shared database had been their comfort blanket. But once the ownership model stabilized, report defects actually dropped because provenance was clearer.
The key lesson was not that relational databases are bad. They were still used heavily inside services. The lesson was that a single operational schema cannot faithfully represent multiple business contexts without eventually becoming a battleground.
Operational Considerations
Once services own their data, operational discipline matters more, not less.
Event delivery and outbox design
If a service updates local state and publishes an event, those two actions must be coordinated. Otherwise you get ghost records: data changed with no event, or event published with no durable state. The transactional outbox remains one of the most practical patterns here.
Observability
You need end-to-end tracing with business identifiers, not just technical request IDs. In distributed data flows, the question is often not “did the API return 200?” but “did Order 847392 become visible in billing, fulfillment, and reporting within SLA?”
Track:
- event production latency
- consumer lag
- projection freshness
- reconciliation mismatch rates
- failed compensations
- dead-letter queue growth
Schema and contract evolution
Database schema evolution is hard enough inside one service. Event contract evolution across many consumers is harder. Version events carefully. Favor additive change. Deprecate slowly. Publish semantic meaning, not internal table structure.
An event called CustomerRowUpdated is a confession of poor design.
Security and access control
Shared databases often grow because they are a convenient access point. Once data is separated, access must be redesigned. Fine-grained authorization, token propagation, row-level controls where necessary, and data classification become more important. This is especially true when services handle PII, financial records, or regulated data.
Backup and recovery
With service-owned databases, recovery becomes federated. That improves blast radius but complicates coordinated restoration. Teams need clear rules for replaying events, restoring projections, and determining source-of-truth precedence after partial outages.
Data retention and lineage
Distributed ownership should not mean distributed confusion. Enterprises still need lineage, retention policies, archival rules, and legal hold support. The difference is that these concerns should be implemented through a coherent data governance model, not by forcing all services into one schema. EA governance checklist
Tradeoffs
There is no serious architecture discussion without tradeoffs.
What you gain
- team autonomy
- independent deployment
- clearer domain boundaries
- reduced hidden coupling
- better blast-radius isolation
- stronger alignment with business capabilities
What you pay
- eventual consistency
- data duplication
- more complex reporting
- harder debugging across asynchronous flows
- need for reconciliation and governance
- more operational moving parts
The central tradeoff is this: shared database optimizes for local convenience; service-owned data optimizes for long-term changeability.
In small systems, local convenience may win. In large enterprises with many teams, long-term changeability usually does.
There is also a cultural tradeoff. Teams used to direct SQL access often feel slower when forced to work through APIs and events. They are slower at first. Then the organization stops having to coordinate every schema change with seven application owners and three support groups. That is when the payoff appears.
Failure Modes
Even teams that avoid the shared database anti-pattern can stumble into adjacent traps.
Fake ownership
A service gets its own schema, but other teams still have read access and rely on internal tables. This is just the old anti-pattern with better branding.
Chatty replacement
Teams ban database sharing and replace it with synchronous API chains for every read. The result is latency, cascading failures, and runtime coupling instead of schema coupling. Use local projections where appropriate.
Event dumping ground
Kafka becomes a firehose of poorly defined integration events derived from table mutations. Consumers become dependent on internal implementation details. Publish domain events, not row change gossip unless CDC is intentionally used for a specific integration style.
Dual-write corruption
During migration, systems write to old and new stores independently without transactional safeguards. Divergence becomes inevitable. If you must bridge states, use controlled patterns and explicit reconciliation.
Ignoring reconciliation
Many teams build event flows and assume reliability will emerge from good intentions. It will not. If there is no replay strategy, no mismatch detection, and no exception handling path, incidents become archaeology.
Premature canonical model
An enterprise architecture group creates a universal business schema and forces all services to map to it. This usually destroys bounded context clarity and recreates the shared database problem in conceptual form.
When Not To Use
It is fashionable to say every microservice needs its own database. Fashion is not architecture.
Do not push hard for strict service-owned databases when:
The system is still a monolith in every meaningful sense
If one team owns the code, deployment is coordinated anyway, and business complexity is modest, a modular monolith with one database may be the better design. Better a clean monolith than a dishonest microservice estate.
The domain has not been understood
If bounded contexts are unclear, splitting the data will simply create random seams. First understand the domain. Then cut.
The operational maturity is low
If the organization cannot yet manage Kafka, event contracts, consumer operations, replay, and reconciliation, introducing service-owned distributed data may create more harm than the shared schema it replaces.
Strong transactional consistency is non-negotiable across a narrow scope
Some domains—core ledgering, certain inventory reservations, settlement engines—may require transactional integrity that spans components. Even there, the answer is not automatically a shared database across many services. Often it means keeping that capability together inside a single bounded context, perhaps as a modular monolith.
The “microservices” are really just technical slices
If services are split by UI, channel, or CRUD resource rather than business capability, separate databases will not save the design. You will simply get more network hops and more data copies around the same broken model.
The point is not doctrinal purity. The point is choosing an architecture that matches domain truth and organizational reality.
Related Patterns
Several patterns work alongside or against the shared database issue.
Database per service
The obvious counter-pattern. Each service owns its persistence. This can still be relational, document, graph, or mixed depending on the domain.
Strangler fig pattern
The preferred migration approach for escaping a shared schema in a live enterprise estate. Replace incrementally, one capability at a time.
Anti-corruption layer
Protects the emerging service model from legacy schema semantics and keeps new consumers from coupling directly to old data structures.
Transactional outbox
Coordinates local state change with event publication. Extremely useful in Kafka-centric architectures.
CQRS
Helpful when write models and read models differ significantly. Especially relevant when replacing direct cross-service joins with local projections or query views.
Saga / process manager
Useful for coordinating long-running business processes across service-owned data stores where distributed transactions are inappropriate.
Change data capture
Sometimes useful during migration, especially for feeding Kafka from legacy databases. But CDC should not become a substitute for domain events forever. It mirrors data change, not business intent.
Summary
The shared database anti-pattern in microservices is seductive because it lets teams postpone the hardest architectural decision: who owns the meaning of the data. That postponement usually comes due with interest.
When multiple services share the same operational database, they are not truly independent. They are tenants in the same semantic building, arguing over walls they do not own. Schema coupling is the visible symptom. Lost autonomy, broken domain rules, and operational ambiguity are the deeper disease.
The remedy is not simplistic dogma. It is disciplined domain-driven design, explicit bounded contexts, service-owned data, event-driven integration where appropriate, and a progressive strangler migration that respects enterprise constraints. Kafka can be a strong enabler. Reconciliation is mandatory. Tradeoffs are real. Some domains should remain together longer. Some organizations are not ready yet.
But the architectural principle remains firm: if a service does not own its data, it does not really own its behavior.
That is the line worth remembering. It is the difference between a distributed monolith and a microservices architecture that can actually change under pressure.
Frequently Asked Questions
What is a service mesh?
A service mesh is an infrastructure layer managing service-to-service communication. It provides mutual TLS, load balancing, circuit breaking, retries, and observability without each service implementing these capabilities. Istio and Linkerd are common implementations.
How do you document microservices architecture for governance?
Use ArchiMate Application Cooperation diagrams for the service landscape, UML Component diagrams for internal structure, UML Sequence diagrams for key flows, and UML Deployment diagrams for Kubernetes topology. All views can coexist in Sparx EA with full traceability.
What is the difference between choreography and orchestration in microservices?
Choreography has services react to events independently — no central coordinator. Orchestration uses a central workflow engine that calls services in sequence. Choreography scales better but is harder to debug; orchestration is easier to reason about but creates a central coupling point.