"Just because it’s one database doesn’t mean it should act like one app."
Managing multiple applications using a single database with multiple schemas sounds clean—until things start to break under load. Here's what we learned the hard way and how you can handle it better.
🧩 Our Setup (Real-World Case)
We had:
- 🧵 Microservices for modules like
user-service
,order-service
, etc. - 🗃️ All shared the same PostgreSQL DB using dedicated schemas (
user_schema
,order_schema
, ...). - 🪝 Each app pointed to the same datasource (JDBC URL), but with different
search_path
schema config.
What we expected: Clean schema separation with easy analytics and joins.
What we got: Connection leaks, poor connection pool behavior, and schema confusion.
💥 The Real Problem We Faced
❌ Problem: Connection Establishment Delays
- With one shared datasource and multiple apps hitting the DB simultaneously, connections kept timing out.
- Schema switching using
search_path
led to inconsistent behavior under load.
🔎 Root Cause:
- Spring Boot & HikariCP reuse connections from the pool.
- If you use
SET search_path
, it's connection-scoped — not thread-safe across pooled connections. - This caused schema bleed and wrong-table reads.
✅ Best Practices for Multi-Schema, Multi-App Architecture
1. Use Dedicated Datasource per Schema (App)
Don’t do this:
spring.datasource.url=jdbc:postgresql://host:5432/db
spring.jpa.properties.hibernate.default_schema=user_schema
Instead: Create separate schema-bound datasources
user-service:
spring.datasource.url=jdbc:postgresql://host:5432/db?currentSchema=user_schema
order-service:
spring.datasource.url=jdbc:postgresql://host:5432/db?currentSchema=order_schema
Or programmatically:
@Bean
@ConfigurationProperties("spring.datasource")
public DataSource dataSource() {
HikariDataSource dataSource = new HikariDataSource();
dataSource.setJdbcUrl("jdbc:postgresql://host:5432/db?currentSchema=user_schema");
return dataSource;
}
2. Avoid SET search_path
in Application Code
SET search_path
is dangerous with connection pools. Even when used inside interceptors or filters, it creates non-deterministic schema routing.
3. Isolate Database Connections
- Use a dedicated connection pool per service/app.
- Avoid mixing schema logic in shared libraries or model classes.
4. Don't Share Entity Classes Across Modules
Avoid this anti-pattern:
// Shared User entity reused across services
@Entity
@Table(schema = "user_schema")
public class User { ... }
Different services should own their models to preserve autonomy.
5. Optimize Connection Pooling
Make sure:
spring.datasource.hikari.maximum-pool-size: 20
spring.datasource.hikari.minimum-idle: 5
spring.datasource.hikari.connection-timeout: 30000
Avoid low pool sizes in high-throughput services.
⚠️ Drawbacks of This Architecture
❌ Drawback | ⚠️ Description |
---|---|
Schema Bleed | Without proper isolation, one service might access another’s data accidentally. |
Connection Overhead | Multiple apps competing for the same pool degrades performance. |
Debugging Hell | Logs and stack traces are harder to trace across schemas. |
Cross-Schema Joins | Tempting—but cause tight coupling and performance bottlenecks. |
✅ When This Architecture Makes Sense
- ✅ You need shared analytics with easier cross-schema joins.
- ✅ You want single DB backups but logical separation.
- ✅ Your app count is small and traffic is moderate.
🚫 When to Avoid It
- ❌ You're building high-throughput microservices.
- ❌ You need complete service isolation.
- ❌ You're scaling horizontally with DB load balancing.
🧠 Final Thoughts
Working with multiple schemas in the same DB sounds clean, but you need to be aware of the connection management nuances.
Isolating connection pools, avoiding search_path
, and keeping domain models separate helped us clean up performance issues and schema confusion.
Top comments (0)