DEV Community

araf
araf

Posted on • Edited on

🗄️ Best Practices for Handling Multiple Schemas in the Same Database Across Applications (with Real-World Lessons & Code)

"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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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 { ... }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)