Back to Microservices Fundamentals
Q12Question 12 of 12

Why does shared-database access between microservices weaken their independence, and what are the alternatives?

Key Concepts
  • Shared database couples services through the schema regardless of how independent the diagram looks.
  • Schema changes require coordinated simultaneous redeploys across all touching services.
  • Business rules enforced in code can be silently bypassed by another service writing directly to the table.
  • No clear state ownership: when a value is wrong, nobody knows which service is responsible.
  • Fix: explicit data ownership -- one service owns its tables exclusively; others use APIs or events.
  • Events propagate changes without synchronous coupling; replicated views give fast local reads.
  • A separate reporting store serves cross-cutting queries without joining operational databases.

Definition

A shared database couples services through the schema no matter what the diagram says -- restore independence by giving each service sole ownership and routing cross-service access through APIs, events, replicated views, and reporting stores.
  • Schema coupling -- any change to a shared table must be coordinated across every service that touches it.
  • Bypassed rules -- a service enforces invariants in code; another service writing directly to the table skips them.
  • No ownership -- when many services write a table, nobody owns it; debugging is archaeology.
  • Fix: data ownership -- one service, and only that service, reads and writes a given set of tables.
  • Controlled interfaces -- cross-service access goes through the owning service's API so rules always run.
  • Events -- propagate changes without synchronous coupling.
  • Replicated views -- read-only local copies fed by events for fast querying.
  • Reporting store -- a dedicated warehouse for cross-cutting queries.

The org chart at National Bank of India shows Loans and Accounts as independent teams, but both write directly into the same ledger. Whenever Accounts reorganizes the ledger, Loans breaks. If Loans writes a balance without applying the overdraft rule, the rule is silently broken. The fix: each department keeps its own book; if Loans needs a balance, it asks Accounts through a defined request.

Understanding the Concepts

Why does a shared database weaken independence?

The architecture diagram may show separate services, but if they share a database, they are coupled through the schema. Any column rename, type change, or constraint addition requires coordinated simultaneous redeploys. The diagram lies about the real dependency structure.

JAVA
package com.bank.antipattern;

public class SharedDatabaseCoupling {
    public static void main(String[] args) {
        System.out.println("Diagram says: AccountService and LoanService are independent");
        System.out.println("Reality: both query SELECT balance FROM accounts");
        System.out.println("Schema change: ALTER TABLE accounts RENAME bal TO balance");
        System.out.println("Result: BOTH services break --> forced coordinated deploy");
    }
}
TEXT
Expected output:
Diagram says: AccountService and LoanService are independent
Reality: both query SELECT balance FROM accounts
Schema change: ALTER TABLE accounts RENAME bal TO balance
Result: BOTH services break --> forced coordinated deploy

How can business rules be bypassed?

When AccountService enforces the overdraft rule in Java code, but LoanService writes directly to the accounts table, the overdraft rule is silently skipped. The invariant is broken without any error.

JAVA
package com.bank.antipattern;

import java.math.BigDecimal;

public class BypassedRules {

    // AccountService enforces: balance >= overdraft_limit
    public boolean debit(String accountId, BigDecimal amount) {
        BigDecimal balance = getBalance(accountId);
        BigDecimal limit   = getOverdraftLimit(accountId);
        if (balance.subtract(amount).compareTo(limit) < 0) {
            System.out.println("REJECTED: would breach overdraft limit");
            return false;
        }
        // update balance
        return true;
    }

    // WRONG: LoanService writes directly to the table
    // jdbcTemplate.update("UPDATE accounts SET balance = ? WHERE id = ?", newBal, id);
    // Overdraft rule BYPASSED --> data corrupted

    private BigDecimal getBalance(String id) { return new BigDecimal("50000"); }
    private BigDecimal getOverdraftLimit(String id) { return new BigDecimal("0"); }
}

How does unclear ownership cause debugging problems?

When five services write the same table and a balance is wrong, which service caused it? Nobody knows. Debugging becomes archaeology: checking logs across five services to find who wrote the bad value.

JAVA
package com.bank.antipattern;

public class OwnershipProblem {
    public static void main(String[] args) {
        String[] writers = {
            "AccountService", "LoanService", "TransferService",
            "FeeService", "InterestService"
        };
        System.out.println("Rajesh's balance is wrong. Who wrote the bad value?");
        for (String w : writers) {
            System.out.println("  Check " + w + " logs...");
        }
        System.out.println("Nobody knows. 5 services, 5 suspects, no owner.");
    }
}
TEXT
Expected output:
Rajesh's balance is wrong. Who wrote the bad value?
  Check AccountService logs...
  Check LoanService logs...
  Check TransferService logs...
  Check FeeService logs...
  Check InterestService logs...
Nobody knows. 5 services, 5 suspects, no owner.

What is explicit data ownership?

One service, and only that service, reads and writes a given set of tables. No other service has a JDBC connection to those tables. Cross-service access goes through the owning service's API, so business rules always run.

JAVA
package com.bank.service;

import java.math.BigDecimal;

public class ExplicitOwnership {
    // AccountService OWNS the accounts table exclusively
    // No other service has a JDBC connection to it

    // LoanService calls the API:
    public BigDecimal getBalanceViaApi(String accountId) {
        // return accountClient.getBalance(accountId);
        // AccountService's overdraft rule ALWAYS runs on debit
        return new BigDecimal("150000.00"); // stub
    }

    public static void main(String[] args) {
        ExplicitOwnership svc = new ExplicitOwnership();
        System.out.println("Balance via API: " + svc.getBalanceViaApi("SAV2001"));
        System.out.println("Overdraft rules enforced by AccountService.");
    }
}
TEXT
Expected output:
Balance via API: 150000.00
Overdraft rules enforced by AccountService.

How do events propagate changes without coupling?

AccountService publishes an event when the balance changes. LoanService reacts and updates its own local read-only view. No synchronous coupling, no shared table.

JAVA
package com.bank.service;

import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.annotation.KafkaListener;
import java.math.BigDecimal;

public class EventPropagation {

    // AccountService publishes
    public void publishBalanceChanged(KafkaTemplate<String, String> kafka,
                                      String accountId, BigDecimal newBalance) {
        String event = accountId + ":" + newBalance;
        kafka.send("account.balance-changed", accountId, event);
        System.out.println("Published: " + event);
    }

    // LoanService reacts
    @KafkaListener(topics = "account.balance-changed")
    public void onBalanceChanged(String event) {
        System.out.println("Received: " + event);
        // Update local read-only view
    }
}
TEXT
Expected output:
Published: SAV2001:150000.00
Received: SAV2001:150000.00

What are replicated views and reporting stores?

A replicated view is a read-only local copy shaped for the consumer's query patterns, fed by events. A reporting store is a dedicated warehouse for cross-cutting queries that span multiple services.

JAVA
package com.bank.service;

public class ReplicatedViewAndReporting {
    public static void main(String[] args) {
        // Replicated view: LoanService owns a local copy of balance
        System.out.println("LoanService: local balance view (read-only, fed by events)");
        System.out.println("Fast local reads, no API call on every loan check");

        // Reporting store: cross-cutting queries
        System.out.println("\nReporting store: each service feeds data into warehouse");
        System.out.println("Cross-cutting reports run there, not by joining operational DBs");
        System.out.println("Operational stores stay private and owned");
    }
}
TEXT
Expected output:
LoanService: local balance view (read-only, fed by events)
Fast local reads, no API call on every loan check

Reporting store: each service feeds data into warehouse
Cross-cutting reports run there, not by joining operational DBs
Operational stores stay private and owned

How do you enforce ownership?

Enforce with architecture tests and per-service database credentials so violations are caught at compile time or connection time, not after a production incident.

JAVA
package com.bank.architecture;

import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;

public class OwnershipEnforcement {
    // Architecture test: loans module cannot access accounts repository
    @ArchTest
    static final ArchRule noSharedDB = noClasses()
        .that().resideInAPackage("..loans..")
        .should().dependOnClassesThat().resideInAPackage("..accounts.repository..");

    // Per-service credentials: LoanService physically cannot connect to AccountDB
    // DataSource for LoanService points only to loan_db
}

What are common mistakes?

The most common mistake is treating shared-database access as permanent architecture rather than debt with a repayment plan.

JAVA
package com.bank.antipattern;

public class CommonMistakes {
    public static void main(String[] args) {
        // WRONG: "we have separate services" but they share one database
        System.out.println("WRONG: diagram shows independence, schema couples them");

        // WRONG: treating shared-DB as permanent architecture
        System.out.println("WRONG: shared DB is debt with no repayment plan");

        // CORRECT: each service owns its data; others use APIs or events
        System.out.println("CORRECT: data ownership + APIs + events");
    }
}
TEXT
Expected output:
WRONG: diagram shows independence, schema couples them
WRONG: shared DB is debt with no repayment plan
CORRECT: data ownership + APIs + events

What should a senior engineer know?

  • Shared-DB access is occasionally a bounded compromise during migration, but it is debt with a repayment plan, not a design choice.
  • Enforce with architecture tests and per-service credentials.
  • The real independence test: can this service evolve its schema without coordinating with anyone?
  • Events plus replicated views give fast reads without shared state.
JAVA
package com.bank.architecture;

public class SeniorInsights {
    public static void main(String[] args) {
        System.out.println("Test: can this service change its schema alone?");
        System.out.println("If NO --> not truly independent, despite the diagram");
        System.out.println("Fix: data ownership + events + replicated views");
    }
}
TEXT
Expected output:
Test: can this service change its schema alone?
If NO --> not truly independent, despite the diagram
Fix: data ownership + events + replicated views

Visual Overview

DIAGRAM
  Shared Database (the problem)
  ┌────────────┐  ┌────────────┐
  │ Account    │  │ Loan       │
  │ Service    │  │ Service    │
  └──────┬─────┘  └──────┬─────┘
         │               │
         └───────┬───────┘
                 ▼
          ┌────────────┐
          │ Shared DB  │  schema change breaks both
          │ accounts   │  rules can be bypassed
          └────────────┘  nobody owns the data

  Fixed: Data Ownership
  ┌────────────┐           ┌────────────┐
  │ Account    │   API     │ Loan       │
  │ Service    │◄─────────│ Service    │
  └──────┬─────┘  events   └──────┬─────┘
         │  ┌──────────┐         │
         └──│ Account  │         └── Loan DB (own)
            │ DB (own) │
            └──────────┘
  Rules always enforced via API
  Schema changes are local
  Ownership is clear

Key Takeaway

Shared-database access between microservices couples them through the schema regardless of how independent the diagram looks. Schema changes require coordinated deploys, business rules enforced in code can be bypassed by another service writing directly, and when many services write the same table nobody owns the data.

The alternatives all follow one principle: each service owns its data exclusively. Cross-service access goes through the owning service's API so rules always run. Events propagate changes without synchronous coupling. Replicated read-only views give consumers fast local reads without sharing the write path. A separate reporting store serves cross-cutting queries without joining operational databases.

At National Bank of India, the Loans service was writing the accounts table directly, bypassing the overdraft and audit rules. Pankaj gave Accounts sole ownership, so Loans called the debit API where every invariant runs, while a balance-changed event kept a read-only view in Loans for fast lookups.

Remember one sentence: a shared database couples services through the schema, so give each service sole data ownership and route cross-service access through APIs, events, replicated views, and reporting stores.

---

Share
Use ← → arrow keys to navigate