Internal State vs External State: Where Should Your Microservice Store Its Data?

Detailed image of a server rack with glowing lights in a modern data center.

Most microservices postmortems that look like network or deployment problems are actually data-ownership problems. Two services quietly sharing a database, or one service reaching into another’s tables “just this once,” and six months later nobody can deploy either one without coordinating with the other team. The question “where should this data live” isn’t a storage decision — it’s the decision that determines whether your services are actually independent or just independently deployed processes sharing a fate.

The core rule: state ownership follows service ownership

A microservice’s internal state is data it owns exclusively — a database (or table set) that only that service’s code ever reads or writes directly. Every other service that needs that data goes through the owning service’s API, not its tables. External state is anything shared: a database multiple services write to, a cache multiple teams populate, a data lake treated as a source of truth by services that didn’t produce the data.

The rule of thumb almost writes itself once you frame it this way: if changing your database schema requires asking another team’s permission, that data isn’t internal to you — it’s external, regardless of which service “technically” created the table.

Why shared mutable state breaks independence

The classic anti-pattern is the shared database: two or three services all pointing at one Postgres instance because it was simpler to stand up once. This looks fine for months. Then Team A needs to add a NOT NULL column, or normalize a table, or change a type from int to bigint because they hit an overflow — and now they’re blocked, because Team B’s service queries that same table directly and the migration will break it. You’ve recreated a monolith’s coupling with none of a monolith’s benefits: no shared transaction boundary, no single deployment unit, no one codebase where the coupling is even visible.

The failure mode isn’t hypothetical or rare — it’s close to the default outcome of a shared database, because SQL makes it trivially easy to reach across a boundary that should be enforced in code. A service’s database schema is an implementation detail; the moment another service depends on it directly, that implementation detail becomes a public API you can’t change without a coordinated release.

Rule of thumb: if two services need the same underlying data, one of them owns it and exposes it through an API or an event stream. Nobody else touches its tables.

Where sharing state is actually fine

None of this means data should never leave a service’s boundary — it means mutable, source-of-truth data shouldn’t be shared directly. Several patterns share data safely because they’re built around read-only, derived copies, not shared write access:

  • Cache-aside with an internal source of truth. A service can put frequently-read data in Redis for latency reasons. The cache is disposable and derived — if it’s wiped, the service rebuilds it from its own database. Other services should still go through the owning service’s API, not read its cache directly, or you’ve just moved the coupling problem into Redis.
  • Event-driven replication (CDC or domain events). The owning service publishes order.updated events; downstream services build their own local, read-only projection of whatever slice of that data they need. This is the difference between “sharing a database” and “sharing facts” — each consumer owns its own copy, evolves its own schema, and isn’t broken by the producer’s internal migrations, because the event contract is explicitly versioned and separate from the internal table structure.
  • A dedicated analytics or reporting store. Feeding a data warehouse from every service’s event stream is a legitimate, common pattern — it’s explicitly not meant to be a live source of truth for other services’ transactional logic, just a read model for humans and BI tools.
  • Shared reference/config data. Feature flags, currency codes, tenant configuration — genuinely read-mostly data with a single clear owner and infrequent, low-risk changes — can reasonably live in a shared store multiple services read from directly, as long as writes still go through one owner.

The pattern across all of these: derived, read-only copies are safe to share; the authoritative, mutable record is not.

What to do instead of a shared database

  • Database per service, even if that means running more database instances than feels efficient at first. The operational cost is real but smaller than the coupling cost it prevents.
  • CQRS with materialized views when a service genuinely needs to query across data it doesn’t own — build a local read model from events instead of joining across service boundaries.
  • Sagas instead of cross-service transactions. If a workflow needs to touch two services’ data consistently, that’s a sequence of local transactions coordinated by events or an orchestrator, not a two-phase commit across two databases you don’t both control.

A real failure pattern

A payments service and a reporting service shared one database because reporting needed “live” numbers and a shared DB felt faster than building a pipeline. Payments later needed to shard a table for scale and changed its primary key structure. Reporting’s queries, written directly against that table, broke in production during the migration window — a service with zero business reason to be affected by a payments-scaling decision went down because of it. The fix was CDC: payments kept its internal schema free to evolve, and reporting built its own projection from a Kafka topic, decoupled from the producer’s internal storage decisions entirely.

The decision, in order

  1. Does this service produce the data, or is it just reading someone else’s? If reading, don’t touch their tables — call their API or consume their events.
  2. Is what’s being shared the mutable source of truth, or a derived/read-only copy? Source of truth stays internal, always.
  3. Does another service need this data for its own local decisions? Publish an event; let them build their own projection instead of querying yours live.
  4. Are you sharing a database because it’s simpler right now? That simplicity is a loan against a future migration you won’t control alone.

The services that stay genuinely independent aren’t the ones with the most databases — they’re the ones where nobody can tell, from outside a service, what its internal schema even looks like.

Leave a Comment

Your email address will not be published. Required fields are marked *