The Database-per-Service Fallacy
For many engineering teams, the "database-per-service" pattern has become the de facto gold standard for microservices architecture. We are taught that to truly decouple services, we must enforce physical data isolation. If a service owns its data, it should own its database.
However, for a 15-person engineering team, this "best practice" often becomes a massive anchor. You find yourself spending more time managing distributed transactions and Kafka topics than building features that actually deliver value to your users.
If you are struggling with complex Saga orchestrators just to join a User table with an Order table, you have likely fallen into a common architectural trap: premature optimization.
The Cost of Premature Isolation
When startups and mid-sized teams rush to split their databases, they often create a "distributed monolith"—a system that suffers from the complexity of microservices without gaining any of the benefits of scalability.
By forcing physical isolation too early, you inherit several significant operational burdens:
- Distributed Consistency Nightmares: Without ACID-compliant transactions across services, you are forced to implement complex patterns like Sagas or distributed locks.
- Latency Spikes: Simple read operations that could have been a single SQL join now require multiple network hops between services.
- Operational Overkill: You end up maintaining complex event-driven pipelines simply to keep data synchronized across boundaries.
I recall a project where we migrated a billing service to its own database. Suddenly, a simple check to see if a client was active required a network call from the subscription service. We spent weeks debugging race conditions and implementing retry queues and outbox patterns. It was a massive waste of engineering cycles that could have been spent on product growth.
The Pragmatic Alternative: Logical Schemas
The alternative is not to abandon modularity, but to rethink where you enforce it. You can achieve clear ownership boundaries without the overhead of multiple database instances by using logical schemas.
In a system like PostgreSQL, you can isolate your data using namespaces (schemas) within a single physical instance.
Example: Implementing Logical Isolation in Postgres
Instead of creating two separate databases, organize your tables into logical schemas. This allows you to maintain clean code boundaries while keeping the data accessible.
-- Create distinct schemas for service ownership
CREATE SCHEMA users;
CREATE SCHEMA billing;
-- Define tables within their respective schemas
CREATE TABLE users.profiles (
id UUID PRIMARY KEY,
email TEXT NOT NULL,
status TEXT
);
CREATE TABLE billing.invoices (
id UUID PRIMARY KEY,
user_id UUID REFERENCES users.profiles(id),
amount DECIMAL
);
-- You can still perform efficient, ACID-compliant joins
SELECT i.id, u.email
FROM billing.invoices i
JOIN users.profiles u ON i.user_id = u.id
WHERE u.status = 'active';
Why Logical Schemas Win
Using logical schemas provides the best of both worlds:
- Clear Ownership: Your application code connects to the database with a search path restricted to its own schema, enforcing logical separation.
- Future-Proofing: If a specific service grows to the point where it truly needs its own hardware, moving a schema to a separate database instance is a straightforward migration.
- Pragmatism: When you need to perform cross-service reporting or complex queries, you aren't fighting against your own infrastructure. You have the full power of SQL at your fingertips.
Conclusion: Build for Your Current Scale
A recent industry report noted that 34% of practitioners admit that multiple microservices end up managing the same tables anyway. We must stop paying the "distributed systems tax" before we have even reached product-market fit.
Keep your stack simple. Build clean boundaries within a single database, and only reach for physical isolation when your team size, traffic, and data volume actually demand it. Your future self—and your velocity—will thank you.
Top comments (0)