Database Engineering

Breaking the Mold: Common Data Modeling Anti-Patterns in Microservices

Transitioning from a monolithic architecture to microservices is often touted as the silver bullet for scalability and independence. However, many engineering teams fall into the trap of distributing their database's complexity without distributing the logic. This results in systems that are technically distributed but conceptually monolithic. In database engineering, how you structure your data is just as critical as how you structure your code. Today, we will explore two pervasive anti-patterns: the Distributed Monolith and Over-Normalization, and discuss how to avoid them to build truly resilient microservices.

The Distributed Monolith

The distributed monolith occurs when microservices appear independent at the code level but are tightly coupled at the data level. This usually happens when services share a single database schema or when one service directly queries the database tables of another service. This violates the core principle of microservices: each service should own its data exclusively.

Consider a scenario where an OrderService needs customer data. Instead of exposing this data via a well-defined API, developers might join directly against the CustomerService’s database. This creates a rigid dependency. If the customer data schema changes, the order service breaks, regardless of whether the order service code was updated. This coupling prevents independent deployment and scaling.

To fix this, adopt the Database Per Service pattern strictly. Services should communicate via asynchronous events (e.g., using Kafka or RabbitMQ) or synchronous REST/gRPC APIs. This ensures that data ownership remains encapsulated within the service boundary.

The Trap of Over-Normalization

While normalizing data reduces redundancy and ensures integrity in traditional relational databases, it becomes a performance bottleneck in distributed environments. Over-normalization refers to the excessive splitting of data into many small tables, requiring complex joins to reconstruct meaningful entities.

In a monolith, joins are cheap because everything resides in the same process space. In microservices, data is partitioned across different network boundaries. Reconstructing a single logical entity often requires querying multiple services and merging the results on the application side. This leads to the N+1 query problem, increased latency, and higher complexity in error handling.

Instead of normalizing, consider denormalization. Store redundant data where it is needed. For example, if an OrderService frequently displays the customer's name, store that name directly in the order record. While this introduces update anomalies (you must update the name in all existing orders if the customer changes it), the trade-off for read performance and simplified architecture is usually worth it. Use background jobs to handle eventual consistency for updates.

Practical Implementation Example

Let’s look at a practical example contrasting the anti-pattern with the recommended approach. In the anti-pattern, the database schema might look like this:

-- ANTI-PATTERN: Shared Schema / Distributed Monolith
CREATE TABLE orders (
    id UUID PRIMARY KEY,
    user_id UUID, -- Direct foreign key to user table
    status VARCHAR(20)
);

CREATE TABLE users (
    id UUID PRIMARY KEY,
    email VARCHAR(255) UNIQUE
);

-- This join couples the services tightly
SELECT o.id, u.email 
FROM orders o 
JOIN users u ON o.user_id = u.id;

Now, consider the improved approach where data is denormalized and ownership is clear:

-- BEST PRACTICE: Denormalized & Owned Data
CREATE TABLE orders (
    id UUID PRIMARY KEY,
    user_id UUID, -- ID is kept for reference, not direct lookup
    user_email VARCHAR(255), -- Denormalized for fast reads
    status VARCHAR(20)
);

-- When user updates email, an event is published
CREATE TABLE user_audit_log (
    user_id UUID,
    old_email VARCHAR(255),
    new_email VARCHAR(255),
    timestamp TIMESTAMP
);

Conclusion

Avoiding distributed monoliths and over-normalization is not just about technical correctness; it is about architectural discipline. By enforcing strict data boundaries and accepting the reality of network latency through denormalization, you enable your microservices to scale independently and deploy without fear of cascading failures. Remember, the goal is not to have the most efficient database for a single machine, but the most resilient system for a distributed world. Start by auditing your current data access patterns, identify the tight couplings, and slowly refactor towards a model that prioritizes loose coupling and eventual consistency.

Share: