Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Database per Service: Patterns and Pitfalls

By Kokil Thapa | Last reviewed: September 2026

Database per Service: Patterns and Pitfalls matter the moment you split a monolith into deployable services. One shared MySQL schema feels fast at first. Every query is a JOIN away. Then two teams ship on Friday night and the same orders table becomes a bottleneck. I've seen this on production Laravel applications where booking, payments, and CRM logic lived in one database until growth forced a split. This guide walks through ownership rules, cross-service reads, migration steps, and the failures that actually show up in 2026—not textbook diagrams alone. If you are planning a split, start with our database sharding guide and enterprise application development approach before you rename schemas on a live server.

What is the database per service pattern in microservices?

The pattern gives each bounded context its own database—or at least its own schema with strict access control. The Orders service writes to orders_db. Inventory writes to inventory_db. No foreign keys span service boundaries. Other teams consume data through HTTP APIs, gRPC, or domain events—not SQL against your tables.

That rule sounds strict because it is. Shared databases are the most common hidden monolith. Two services reading the same table still couple their release cycles. A column rename in Inventory breaks Checkout at 2 a.m. Database per service pushes coupling to explicit contracts: OpenAPI specs, event schemas, and versioned payloads.

Monolith vs Database Per ServiceShared Database MonolithAll App ModulesOne MySQL Schemaorders + users + stockTight coupling, one deployDatabase Per ServiceOrders SvcInventoryorders_dbstock_dbAPI onlyIndependent deploys
Database per service replaces cross-module JOINs with service-owned stores and explicit API contracts.

On a legal-tech portal I built, client documents, appointments, and payments started in one Laravel app with one PostgreSQL instance. That was correct for year one. Once document workflows needed different backup policies than public marketing content, separate stores made sense. The pattern is not "microservices for microservices' sake." It is about independent change velocity and blast-radius control.

Core ownership rules

  • Only the owning service runs migrations against its database.
  • Other services store foreign references as opaque IDs, not enforced FK constraints.
  • Schema changes are backward compatible or versioned through dual-write periods.
  • Read models for reporting may duplicate data—that is intentional denormalization.

Microsoft's microservices architecture guidance describes this as "private datastore per service" with no direct cross-database access. That matches what I enforce on client projects: if you need a row, call the owner.

When should you adopt database per service?

Adopt the pattern when team boundaries, scaling profiles, or compliance requirements diverge—not because a conference slide said you should. Three green signals appear repeatedly in production.

  1. Different scaling needs: Your catalog is read-heavy; orders are write-heavy. One connection pool cannot serve both fairly. See connection pooling fundamentals for why mixed workloads fight over the same pool.
  2. Independent release cadence: Inventory ships daily; billing ships monthly. Shared migrations block the faster team.
  3. Regulatory isolation: Payment card data or legal documents need encryption, retention, or audit rules that marketing tables do not.

Three red signals suggest you should wait. Your team is under five engineers. Your domain model is still shifting weekly. You lack observability for distributed traces. Database per service adds operational cost: more backups, more restore drills, more failure modes. Our database restore testing guide applies per store once you split.

SignalStay monolith / modular monolithSplit to database per service
Team size1–8 engineers, one backlogMultiple autonomous teams with separate roadmaps
Query styleHeavy cross-entity JOINs in one requestMost reads scoped to one aggregate root
Consistency needsStrong ACID across entities requiredEventual consistency acceptable with compensations
Ops maturitySingle backup job, one on-call rotationAutomated provisioning, per-service SLOs
Tech fitOne Laravel 13 app on PHP 8.3+ is enoughPolyglot stores help (Redis 8.10 cache, Postgres 18 analytics)

For many Nepal SMB projects I work on, a well-structured modular monolith on Laravel 12 or 13 with clear module boundaries beats premature splitting. Budget and ops headcount are real constraints. Rs 15,000–40,000/month (~USD 110–295) hosting cannot easily absorb six managed databases without a business case.

How do you query across services without shared databases?

You do not run JOINs across service boundaries. You choose one of four patterns based on latency, consistency, and who owns the UX.

1. Synchronous API composition

The Checkout service calls Inventory's REST API before confirming a cart. Simple, familiar, easy to debug. Downside: latency stacks and cascading failures appear if Inventory is slow. Pair this with timeouts and circuit breakers from our circuit breaker patterns article.

// Checkout service (Laravel 13) — never query inventory_db directly
$response = Http::timeout(2)
    ->retry(2, 100)
    ->get("https://inventory.internal/api/v1/stock/{$sku}");

if ($response->failed()) {
    throw new InventoryUnavailableException($sku);
}

$available = $response->json('quantity');

2. Event-driven replication

Inventory publishes StockAdjusted events. Search maintains its own read-optimized copy. This powers faceted product search on eCommerce builds like multi-currency florist stores where catalog reads dwarf writes. Accept lag of seconds to minutes. Document it in SLAs.

3. CQRS read models

Command side stays in the owning service. A reporting projector builds tables tailored to dashboards. Useful when admins need cross-service views without hammering live OLTP schemas. See CQRS in Laravel when it helps for bounded cases—not every CRUD screen needs it.

4. API gateway aggregation

The gateway calls Orders and Users, merges JSON for the mobile app. Keeps clients dumb and services focused. Watch payload size and cache headers carefully.

Cross-Service Data PatternsOrders Serviceorders_dbInventory Svcinventory_dbSync API CallGET /stock/{sku}Domain EventsStockAdjustedCQRS Read Modelsearch_indexAnti-PatternCross-DB SQL JOINNever query another service database directly
Cross-service data flows through APIs, events, or dedicated read models—not shared SQL access.

Amazon's microservices best practices document stresses "each service's data is private." That privacy is what lets you swap MySQL 8.4 for PostgreSQL 18 in one service without rewriting the fleet. Our MySQL to PostgreSQL migration guide covers single-service moves; multiply the checklist per database once you split.

How do you keep distributed writes consistent?

Single-database transactions disappear across boundaries. You replace them with sagas, outbox patterns, and idempotent consumers. On booking systems like trek management platforms, a reservation may touch availability, payment, and supplier notification. All three must succeed or roll back cleanly.

Choreography vs orchestration

Choreography lets services react to events without a central coordinator. Orchestration uses a saga manager that tracks steps. I prefer orchestration when business rules are complex and you need a visible state machine. Choreography works for simple pipelines with few compensating actions.

// Saga step with outbox (Laravel queue + MySQL 9.7)
DB::transaction(function () use ($order) {
    $order->markPending();
    OutboxEvent::create([
        'aggregate_id' => $order->id,
        'type'         => 'OrderCreated',
        'payload'      => $order->toEventPayload(),
    ]);
});

// Separate worker publishes to message broker

Read the saga pattern for distributed transactions before you hand-roll retries. Duplicate events will happen. Network partitions will happen. Idempotency keys belong in every consumer.

Deadlocks do not vanish—they move inside each service. Laravel transactions and deadlocks still apply per database. What changes is you cannot lock rows in two schemas inside one BEGIN block.

What are the biggest database per service pitfalls in production?

Teams underestimate operations and overestimate how cleanly domains split. These pitfalls appear on almost every first split.

Pitfall 1: Fake separation, shared database server

Creating two schemas on one MySQL instance is a reasonable stepping stone. Calling it "database per service" while DBAs run one overloaded server is not. Noisy neighbour queries still hurt everyone. Index tuning on one schema can lock tables another service needs. Use separate instances—or at least separate users with revoked cross-schema privileges—before you claim independence.

Pitfall 2: Distributed monolith via chatty APIs

Replacing one JOIN with five HTTP calls does not improve anything. Latency and failure rates rise. Cache read-heavy reference data locally with TTL. Use query caching strategies inside each service boundary. Batch endpoints beat N+1 HTTP from the gateway.

Pitfall 3: Reporting becomes impossible

Finance asks for a monthly revenue-by-region report. No single database has the full picture anymore. Plan a data warehouse or analytics replica early. ETL jobs from domain events beat nightly mysqldump hacks. Validate JSON event payloads with a JSON formatter tool during schema design so downstream pipelines do not break silently.

Pitfall 4: Migration big-bang

Teams freeze the monolith, split overnight, and lose a weekend. Incremental strangler fig wins. Extract one bounded context, dual-write, backfill, switch reads, retire old tables. Our team migration practices apply per service once multiple repos own schema files.

Pitfall 5: Ignoring observability

A user sees "payment failed." Which service dropped the message? Without correlation IDs across API and queue hops, you grep five log streams. Structured logging and trace headers are not optional extras.

Cross-Service Data DecisionNeed data fromanother service?Strong consistency required?YesSync API + Sagatimeout and retryNoEvents + Read Modelaccept eventual lagNever cross-DB JOINNever shared schema
Database per service data decisions: sync APIs for strong consistency, events for read-heavy eventual views.

Repository abstractions do not fix boundary violations. A "clean" PHP class that secretly opens a second DB connection still couples you. Review repository anti-patterns when code reviews feel fine but deploys keep breaking.

How do you migrate from a monolith to database per service safely?

Start with domain analysis, not infrastructure. List aggregates—Order, Shipment, Invoice—and mark which teams own them. If two teams edit the same aggregate, fix org boundaries before you split databases.

Phase 1: Modular monolith inside one database

Enforce module imports in Laravel. No direct Eloquent calls across module namespaces. Use application services as seams. Add indexes per module's query patterns using guidance from database indexing for performance. This phase costs little and reveals bad boundaries early.

Phase 2: Schema separation on shared instance

Move tables into orders and inventory schemas. Revoke cross-schema grants. Application code already uses service classes—not raw cross-module models. Run integration tests against revoked privileges to catch cheats.

Phase 3: Physical separation and event bus

Provision separate MySQL 9.7 or PostgreSQL 18 instances. Introduce an outbox table and message broker. Dual-write during backfill. Compare row counts nightly. Switch reads only when diffs stay zero for a full business week—including month-end if you handle billing.

Phase 4: Decommission shared paths

Remove fallback reads from the monolith schema. Archive historical rows to cold storage if regulators allow. Update backup jobs per instance. Test restore per service; a single mysqldump of "the app" no longer exists.

Safe Migration PhasesPhase 1Modular monolithPhase 2Schema splitPhase 3Dual-writePhase 4Cut overStrangler Fig ExtractionOne bounded context at a timeBig-Bang SplitHigh outage riskIncremental PathDual-write verify
Incremental strangler migration beats big-bang database splits that stall releases for weeks.

Webhook-based integrations—payment gateways, SMS providers—must stay idempotent after the split. Payment callbacks hitting the wrong service instance because DNS lagged caused a production incident I debugged on a client project. Webhook reliability patterns belong in the migration checklist.

If you need API contracts designed before extraction, our API development service covers versioning, auth, and pagination—the surface other services will depend on. For denormalized search copies, see when denormalization actually helps.

Martin Fowler's bounded context write-up remains the best primer on why data ownership follows organizational seams—not ER diagrams. Read it before you draw microservice boxes on a whiteboard.

Key Takeaways

  • Database per service means private datastores, no cross-service SQL, and explicit API or event contracts.
  • Split when teams, scaling, or compliance diverge—not when your architecture diagram looks boring.
  • Replace JOINs with API composition, events, CQRS read models, or gateway aggregation—each with different consistency trade-offs.
  • Use sagas, outbox tables, and idempotent consumers instead of distributed two-phase commit fantasies.
  • Migrate incrementally with dual-write and verified backfill; big-bang cuts cause the worst outages.
  • Plan reporting, backups, and restore tests per database from day one—not after finance asks for a dashboard.

People Also Ask

Is database per service the same as schema per service?

Schema per service on one server is a valid intermediate step. True database per service uses separate instances or managed clusters so failure, scaling, and backups are isolated. Privilege separation matters: if credentials can still read both schemas, you only have cosmetic boundaries.

Can Laravel monoliths evolve toward database per service?

Yes. Laravel 13 on PHP 8.3 supports modular packages, domain events, queues, and Sanctum-protected internal APIs. Most teams should start with a modular monolith, enforce module boundaries, then extract hot paths. Jumping straight to six repos rarely suits small Nepal teams.

How do you handle transactions across microservice databases?

You do not use a single ACID transaction. You implement sagas with compensating steps, keep each local transaction short, and design for retries. Read your own write conflicts may appear unless you use correlation IDs and user-facing status polling.

What database engines work best per service?

Match the engine to access patterns—not uniformity. OLTP orders fit MySQL 9.7 or PostgreSQL 18. Session caches fit Redis 8.10. Full-text search may need a dedicated index. Polyglot persistence is a feature of the pattern, not a failure of standards.

Ship the split with eyes open

Database per Service: Patterns and Pitfalls boil down to a simple trade: you buy independent deploys and clearer ownership at the cost of harder queries, more infrastructure, and eventual consistency everywhere money moves. Done well, it unblocks teams building booking, payments, and catalog features on platforms like those in our production portfolio. Done hastily, it becomes a distributed monolith with worse observability than the single Laravel app you started with. Map boundaries, pick one aggregate to extract first, and measure dual-write diffs before you cut traffic. Need help designing service boundaries or migration phases for a live system? Contact us to walk through your schema and team structure before the first irreversible split.

Frequently Asked Questions

Each microservice owns a private datastore and schema. Other services never query it directly—they exchange data through HTTP APIs, gRPC, or domain events, using sagas for multi-step writes and accepting eventual consistency instead of cross-service SQL JOINs.

Schema per service on one server is a valid intermediate step, not full independence. True database per service uses separate instances or managed clusters so failure, scaling, and backups are isolated—and credentials cannot read across boundaries.

Adopt it when team boundaries, scaling profiles, or compliance requirements diverge—not because a conference slide said you should. Green signals include read-heavy catalog versus write-heavy orders, independent release cadences, and regulatory isolation for payment or legal data. Red signals: under five engineers, a domain model still shifting weekly, or no distributed tracing yet.

Wait if your team is under five engineers, your domain model shifts weekly, or you lack observability for distributed traces. For many Nepal SMB projects, a well-structured modular monolith on Laravel 12 or 13 beats premature splitting.

You never run JOINs across service boundaries. Choose one of four patterns: synchronous API composition for simple reads, event-driven replication for read-heavy views with seconds-to-minutes lag, CQRS read models for admin dashboards, or API gateway aggregation for mobile clients. Each trades latency, consistency, and ownership differently—document lag in SLAs when using events.

The calling service requests data from the owning service's REST API before proceeding—for example, Checkout calling Inventory's stock endpoint with a two-second timeout and retries. It is familiar and easy to debug, but latency stacks and cascading failures appear if downstream services are slow, so pair it with circuit breakers and timeouts.

Single-database ACID transactions disappear across boundaries. Replace them with sagas, outbox patterns, and idempotent consumers. Each service keeps local transactions short. Duplicate events and network partitions will happen—idempotency keys belong in every consumer. Deadlocks still occur inside each service, but you cannot lock rows in two schemas inside one BEGIN block.

Choreography lets services react to events without a central coordinator—good for simple pipelines with few compensating actions. Orchestration uses a saga manager tracking steps as a visible state machine—prefer this when business rules are complex, such as booking flows touching availability, payment, and supplier notification where all three must succeed or roll back cleanly.

Five recur on almost every first split: fake separation on one overloaded server, distributed monoliths via chatty APIs replacing one JOIN with five HTTP calls, reporting paralysis without a data warehouse, big-bang migration weekends, and missing observability so you cannot trace which service dropped a message when a user sees payment failed.

Start with domain analysis—list aggregates and mark team ownership; fix org boundaries before splitting databases. Phase 1: modular monolith with enforced module imports. Phase 2: schema separation with revoked cross-schema grants. Phase 3: physical separation, outbox table, dual-write, nightly row-count diffs. Phase 4: decommission shared paths and test restore per instance. Incremental strangler migration beats big-bang cuts.

Yes. Laravel 13 on PHP 8.3 supports modular packages, domain events, queues, and Sanctum-protected internal APIs. Most teams should start with a modular monolith, enforce module boundaries, then extract hot paths. Jumping straight to six repositories rarely suits small Nepal teams with limited ops headcount.

No single database holds the full picture after a split—finance's monthly revenue-by-region report becomes impossible without planning. Build a data warehouse or analytics replica early. ETL jobs from domain events beat nightly mysqldump hacks. CQRS read models can serve admin dashboards without hammering live OLTP schemas owned by individual services.

Creating two schemas on one MySQL instance is a reasonable stepping stone, but calling it database per service while DBAs run one overloaded server is not. Noisy neighbour queries still hurt everyone, and index tuning on one schema can lock tables another service needs. Use separate instances—or at minimum separate users with revoked cross-schema privileges—before claiming independence.

It adds operational cost: more backups, restore drills, and failure modes per store. For Nepal SMB budgets of Rs 15,000–40,000/month (~USD 110–295), absorbing six managed databases without a clear business case is unrealistic. Plan per-service backup jobs and restore testing from day one—not after finance asks for a dashboard.

When a user sees payment failed, without correlation IDs across API and queue hops you grep five log streams guessing which service dropped the message. Structured logging and trace headers are not optional extras—they are required ops maturity before splitting. Repository abstractions that secretly open a second DB connection still couple services even when code reviews look clean.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: