Your main takeaway here is that a "database per service" pattern doesn’t automatically grant you data isolation; you have to actively enforce it.

Imagine this: Your UserService needs to know a user’s email address. The OrderService needs to know the same email address to send order confirmations. If both services are reading from the same underlying users table in a shared database, you’ve already broken the "database per service" ideal, even if they each have their own schema within that single database. The real isolation comes from truly separate data stores.

Here’s how you can actually achieve this:

Common Pitfalls and How to Fix Them

  1. Shared Database Instance, Separate Schemas:

    • What broke: Services are using different schemas (e.g., users.users, orders.users) within a single PostgreSQL or MySQL instance. While it looks like separation, the underlying database engine and its resource pool are still shared. A runaway query in the orders schema can starve the users schema.
    • Diagnosis: Check your connection strings. Are they all pointing to the same host:port?
    • Fix: Deploy entirely separate database instances for each service. For PostgreSQL, this means distinct data_directory locations and potentially different postgresql.conf settings. For MySQL, separate datadir and my.cnf configurations.
    • Why it works: Each service’s data is managed by its own independent database process, with its own memory, disk I/O, and connection limits.
  2. Cross-Service Queries (The "Cheating" Method):

    • What broke: Your OrderService directly queries the UserService’s database to fetch user details. This is the most common way the pattern is violated.
    • Diagnosis: Use your application logs or database query logs. Look for queries originating from one service’s database connection that target tables belonging to another service’s logical domain. For example, OrderService logs showing SELECT email FROM users WHERE user_id = ....
    • Fix: Implement an API call. The OrderService should call the UserService’s API endpoint (e.g., GET /users/{user_id}/email) to retrieve the necessary data.
    • Why it works: This enforces a contract between services. The UserService controls its own data and exposes only what it’s willing to share through a well-defined interface, preventing direct data access.
  3. Data Duplication Without Synchronization:

    • What broke: You did separate databases, but now OrderService needs user email and decides to copy it into its own user_emails table. This copy becomes stale.
    • Diagnosis: Application behavior where stale data is displayed or used. For example, an order confirmation email is sent to an old address because the OrderService’s copy of the email wasn’t updated.
    • Fix: Use asynchronous event-driven updates. When UserService updates a user’s email, it publishes an event (e.g., UserEmailUpdated with user_id and new_email). OrderService subscribes to this event and updates its local user_emails table.
    • Why it works: This establishes a reliable mechanism for keeping duplicated data consistent. The source of truth (UserService) dictates changes, and other services react.
  4. Shared Connection Pools:

    • What broke: Even with separate database instances, multiple services might be configured to use the same connection pool manager or even a shared proxy that masquerades as a database, leading to resource contention.
    • Diagnosis: Monitor resource utilization (CPU, memory, network) on your database instances. If one instance is being hammered while others are idle, and the application logs show extensive database activity from the overloaded service, check its connection pool settings.
    • Fix: Ensure each service manages its own database connection pool. For example, in Spring Boot, this means each service has its own application.properties or application.yml with distinct spring.datasource.url, spring.datasource.username, spring.datasource.password, and pool-specific settings (like HikariCP’s maximum-pool-size).
    • Why it works: Each service has dedicated resources allocated for its database interactions, preventing one service’s heavy load from impacting others.
  5. Infrastructure Misconfiguration (e.g., VPC/Subnet Sharing):

    • What broke: You have separate database instances, but they are deployed in the same network segment or VPC without proper security groups or network ACLs. This allows unintended network traffic between them.
    • Diagnosis: Network traffic analysis tools (e.g., tcpdump, Wireshark) or cloud provider network monitoring. Look for unexpected connections or data flow between database endpoints that shouldn’t be talking directly.
    • Fix: Place each service’s database in its own logically isolated network segment (e.g., separate VPCs or distinct subnets within a VPC) and use strict firewall rules (security groups, NACLs) to allow traffic only from the service’s application servers to its designated database.
    • Why it works: Network-level isolation prevents services from even reaching each other’s databases, reinforcing the logical separation.
  6. Shared ORM/Data Access Layer:

    • What broke: Multiple services share a single library or module for data access, and this shared layer might inadvertently contain logic that bridges service boundaries or uses a common configuration that can be overridden.
    • Diagnosis: Code review of shared libraries. Look for any configuration or entity definitions that aren’t strictly scoped to a single service’s domain. Check for common connection configurations being loaded.
    • Fix: Each service should have its own independent ORM configuration and potentially its own data access library. If a common library is used, it must be designed to be completely stateless and configured entirely by the calling service, with no shared global state or configuration.
    • Why it works: This ensures that the implementation details of data access are encapsulated within each service, preventing accidental cross-service contamination through shared code.

After fixing these, the next issue you’ll likely encounter is managing distributed transactions or ensuring data consistency across services for operations that logically span multiple domains.

Want structured learning?

Take the full Microservices course →