Database per Service: Patterns and Pitfalls

Khimananda Oli 6 min read Virtualization
Database per Service: Patterns and Pitfalls

By Khimananda Oli | Last reviewed: August 2026

Implementing Database per Service: Patterns and Pitfalls is the defining architectural challenge when moving from monoliths to distributed systems. While isolating data stores prevents tight coupling and enables independent scaling, it introduces significant complexity regarding distributed transactions and cross-service queries. Before you provision separate RDS instances or MongoDB clusters, review this guide on RDS versus self-managed database trade-offs to understand the operational baseline.

Monolith (Shared DB)Order ModuleCustomer ModuleInventory ModuleSingle Shared DBMicroservices (Isolated)Order SvcOrders DBUser SvcUsers DBStock SvcStock DBAPI / EventKey Difference: No Foreign Keys Across ServicesData Integrity Enforced via Application Logic & Events
Database per Service architecture contrasts isolated microservice datastores against traditional shared monolithic schemas

How do you enforce data isolation in Database per Service?

The core tenet of Database per Service: Patterns and Pitfalls is strict ownership. A service must never expose its internal schema directly, nor should it allow other services to query its tables. In practice, this means creating separate logical databases or schemas even if you share physical infrastructure initially. For teams using PostgreSQL, creating distinct schemas within a single cluster can reduce costs while maintaining logical boundaries, but you must revoke all cross-schema privileges.

Practical Schema Isolation Steps

  1. Create a dedicated database user for each service with permissions restricted to its specific schema.
  2. Revoke PUBLIC access and explicit cross-schema grants during provisioning.
  3. Use Infrastructure as Code to manage these permissions declaratively; manual GRANT statements drift quickly. See Terraform for practical IaC to automate this securely.
  4. Implement network-level segmentation where possible, especially in multi-tenant environments or regulated sectors like fintech in Nepal.
-- Example: Strict isolation for Order Service in PostgreSQL
CREATE SCHEMA orders_schema;
CREATE ROLE order_svc_user WITH LOGIN PASSWORD 'secure_pass';

-- Grant usage ONLY on the specific schema
GRANT USAGE ON SCHEMA orders_schema TO order_svc_user;
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA orders_schema TO order_svc_user;

-- Explicitly prevent access to other schemas
REVOKE ALL ON SCHEMA public FROM order_svc_user;
REVOKE ALL ON SCHEMA users_schema FROM order_svc_user;

This level of isolation feels restrictive at first, but it prevents the "distributed monolith" anti-pattern where services are coupled through hidden database dependencies. When an audit requires proving data segregation for SOC 2 compliance, these explicit permission boundaries serve as primary evidence.

How do you handle distributed transactions without 2PC?

Without shared ACID transactions, maintaining consistency across services is the most common failure point in Database per Service: Patterns and Pitfalls. Two-Phase Commit (2PC) is generally avoided in modern cloud-native architectures due to blocking behavior and poor availability. Instead, the Saga pattern provides eventual consistency through compensating transactions.

Order ServicePayment ServiceInventory Service1. CreateOrder2. ReserveStock3. StockReserved4. ProcessPayment5. PaymentFailed6. Compensate: ReleaseStock7. StockReleased8. CancelOrder
Saga pattern choreography handling payment failure with compensating transactions to maintain eventual consistency

Choreography vs Orchestration

Choreography relies on events: Service A publishes OrderCreated, Service B reacts with PaymentProcessed. This decouples services but makes debugging difficult as flows grow. Orchestration uses a central coordinator (like AWS Step Functions or Temporal) to direct each step. For most production systems beyond three services, orchestration is safer because the state machine is explicit and observable.

A common mistake is implementing Sagas without idempotency. Network retries will happen. Every command handler and compensation logic must safely handle duplicate requests. Store processed message IDs in a deduplication table or use Redis to track completion status before executing business logic.

How do you query data across multiple microservices efficiently?

You cannot JOIN across databases. When your UI needs order details plus customer names, you have two primary options in Database per Service: Patterns and Pitfalls: API Composition or CQRS.

CriteriaAPI CompositionCQRS (Read Models)
ComplexityLow initial setupHigh (event pipelines, sync lag)
LatencyHigher (multiple round trips)Low (single optimized query)
Data FreshnessAlways consistentEventually consistent
Best ForAdmin panels, low-traffic viewsHigh-read dashboards, search
Failure ModePartial results if one service failsStale data during outages

Implementing API Composition Safely

If choosing API composition, implement it in a dedicated gateway or BFF (Backend for Frontend) layer, not in the frontend browser. Use parallel async calls and set aggressive timeouts. Always define fallback behavior: if the Recommendation Service is down, return an empty list rather than failing the entire product page. For Laravel teams building BFFs, understanding performance optimization techniques is critical to avoid making the composer itself a bottleneck.

When to Adopt CQRS

CQRS adds significant operational overhead. Only adopt it when read latency directly impacts revenue or user experience. The read model should be denormalized specifically for the query it serves. If you find yourself building generic read models that mirror your write models, you are adding complexity without benefit. Start simple with API composition; migrate to CQRS only after profiling proves it necessary.

What are the operational pitfalls of polyglot persistence?

Choosing the right database for each service sounds ideal but creates massive operational tax. Managing PostgreSQL, MongoDB, Redis, and Elasticsearch simultaneously requires diverse expertise. Backup strategies, monitoring dashboards, and security patches differ for each. In smaller teams or startups in Nepal, this fragmentation often slows delivery more than the architectural benefits accelerate it.

New Service Data NeedDoes team have ops expertise?NoYesUse Default(PostgreSQL)Access Pattern?RelationalDocument/KVPostgreSQLMongoDB / RedisRule: Justify non-default DB with concrete benchmark or feature gapDefault to managed PostgreSQL unless proven insufficient
Decision framework for polyglot persistence balancing technical requirements against operational team capacity

Mitigating Polyglot Overhead

  • Standardize on one primary store: Use PostgreSQL as the default for 80% of services. Only introduce specialized stores when you hit specific limitations.
  • Use managed services: Offload patching, backups, and replication to cloud providers. The premium cost is usually lower than hiring a dedicated DBA.
  • Unified observability: Ensure all datastores export metrics to the same Prometheus/Grafana stack. You cannot troubleshoot what you cannot see in context.
  • Automate provisioning: Never create databases manually. Use Terraform modules that enforce encryption, backup policies, and tagging standards consistently.

In my experience helping Nepali companies scale, teams that resisted polyglot persistence until they absolutely needed it shipped features 3x faster than those who adopted exotic databases prematurely. Operational simplicity is a feature.

Conclusion

Mastering Database per Service: Patterns and Pitfalls requires accepting that distributed data is fundamentally harder than centralized data. Start with strict isolation and simple API composition. Introduce Sagas and CQRS only when consistency or performance demands justify the complexity. Resist polyglot persistence until benchmarks prove necessity. If your team lacks bandwidth to manage this complexity, consider whether a modular monolith better serves your current stage. For tailored architecture reviews or migration planning, reach out to discuss your specific infrastructure challenges.

Frequently Asked Questions

It assigns a dedicated database instance or schema to each microservice, enforcing strict data ownership and preventing direct cross-service table access. This isolation enables independent scaling, technology choices, and deployments while eliminating shared-schema coupling that typically blocks team autonomy in distributed systems.

Shared databases allow multiple services to read and write the same tables, creating tight coupling and deployment bottlenecks. Database per service enforces boundaries via APIs only, meaning schema changes remain local and teams deploy independently without coordinating migrations across unrelated domains or risking cascading failures.

Avoid it for small monoliths, tightly coupled domains with frequent cross-entity transactions, or teams lacking DevOps maturity. The operational overhead of managing multiple databases outweighs benefits when service boundaries are unclear or transactional consistency requirements cannot be satisfied through eventual consistency patterns.

Use API composition, CQRS read models, or event-driven denormalization to aggregate data across service boundaries. Each approach trades latency and complexity for decoupling, so choose based on query frequency, consistency tolerance, and whether the consuming service owns the derived data or merely displays it.

Distributed transactions, complex joins across services, increased infrastructure costs, and operational overhead top the list. Teams often underestimate the difficulty of maintaining referential integrity and debugging data inconsistencies when foreign keys no longer span service boundaries, leading to fragile integration logic.

Implement the Saga pattern using choreography or orchestration to coordinate compensating actions instead of two-phase commits. Each service publishes events upon state changes, and downstream services react accordingly, accepting eventual consistency as the trade-off for avoiding blocking locks across network boundaries.

Yes, provisioning separate instances multiplies compute, storage, and backup expenses compared to shared databases. Mitigate this by using managed serverless options like Aurora Serverless v2 or Cloud SQL, right-sizing instances per workload, and consolidating low-traffic services into isolated schemas rather than full instances.

Yes, polyglot persistence is a core benefit allowing each service to select the optimal engine. A catalog service might use PostgreSQL for relational integrity while a search service uses Elasticsearch, provided teams accept the operational complexity of maintaining diverse backup, monitoring, and migration toolchains.

Rely on application-level validation and domain events to maintain consistency since database constraints cannot span services. Services validate referenced IDs exist via API calls before writes, and publish deletion events so dependent services can cascade soft deletes or orphan records intentionally.

Treat each database schema as an independent artifact versioned alongside its service code using tools like Flyway or Liquibase. Never run cross-service migrations; instead, coordinate breaking changes through API versioning and deprecation windows, ensuring backward compatibility during rolling deployments across the fleet.

Pipelines must provision ephemeral test databases per service, run isolated migrations, and execute integration tests against contract-defined interfaces rather than shared fixtures. This increases pipeline duration but prevents false positives from schema drift and ensures each service validates only against its owned data model.

Yes, Laravel supports multiple database connections natively, making it viable for bounded contexts within a modular monolith transitioning to microservices. However, Eloquent relationships cannot cross connection boundaries, requiring repositories or service clients to fetch related data and sacrificing ORM convenience for architectural isolation.

Never expose database ports between services; communicate exclusively through authenticated APIs with mTLS or OAuth2 tokens. Each database should have unique credentials rotated via secrets managers like Vault, and network policies must restrict access to only the owning service's pod or VM identity.

Track per-database metrics including connection pool saturation, replication lag, slow queries, and storage growth independently. Correlate these with service-level indicators to detect whether performance degradation stems from local schema issues or upstream dependency failures, enabling precise alerting without noisy shared-database dashboards.

There is no universal limit, but exceeding ten active databases per engineer signals insufficient automation or overly granular service decomposition. Invest in platform engineering to standardize provisioning, backups, and observability; if operational burden remains high, consider merging closely related services to reduce cognitive load.