Definition
- 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.
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");
}
}
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.
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.
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.");
}
}
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.
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.");
}
}
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.
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
}
}
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.
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");
}
}
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.
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.
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");
}
}
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.
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");
}
}
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
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.
---