← Back to DevBytes

When to Choose SQL vs NoSQL Over Monolith vs Microservices

When to Choose SQL vs NoSQL Over Monolith vs Microservices

Architecture decisions are among the most consequential choices a developer or engineering team will make. Two of the most debated decisions in modern software engineering are whether to use a SQL or NoSQL database, and whether to build a monolith or adopt microservices. While these decisions are often treated separately, they are deeply intertwined. The database you choose often dictates the architectural patterns you can reasonably adopt, and the architecture you choose often constrains your database options. This tutorial walks through both decisions, explains how they interact, and provides practical guidance for making the right call for your application.

What It Is

SQL databases are relational databases that store data in structured tables with predefined schemas. They use SQL (Structured Query Language) for querying and enforce ACID (Atomicity, Consistency, Isolation, Durability) properties. Examples include PostgreSQL, MySQL, and SQLite.

NoSQL databases are non-relational data stores designed for flexibility, horizontal scalability, and specific use cases. They come in several flavors: document stores (MongoDB, CouchDB), key-value stores (Redis, DynamoDB), column-family stores (Cassandra, HBase), and graph databases (Neo4j, ArangoDB). NoSQL databases typically favor BASE (Basically Available, Soft state, Eventual consistency) semantics over strict ACID guarantees.

A monolith is a single unified application where all modules — user interface, business logic, data access, background jobs — are bundled into one codebase and deployed as a single unit. Monoliths share a single database in most cases.

Microservices are an architectural style where the application is broken into small, independently deployable services, each owning its own data and communicating over a network (typically HTTP/REST, gRPC, or message queues). Each service can be built, deployed, and scaled independently.

Why It Matters

Choosing the wrong database or architecture can lead to painful migrations, performance bottlenecks, and team burnout. A common mistake is adopting microservices with NoSQL databases because they are "modern," only to discover that the application does not actually need horizontal scale and the team is now burdened with distributed system complexity. Conversely, sticking with a monolith and a single SQL database when your application genuinely requires independent scaling of different components can lead to a fragile, hard-to-evolve system.

The key insight is that these decisions should be driven by concrete requirements: data shape, consistency needs, read/write patterns, team size, deployment frequency, and scaling requirements. Let's examine how to evaluate each.

How to Use It: Evaluating SQL vs NoSQL

Use SQL when your data is highly relational, you need strong consistency, complex queries and joins are common, and your schema is relatively stable. SQL databases excel at transactional workloads like e-commerce orders, financial records, and inventory management.

Use NoSQL when your data is semi-structured or unstructured, your schema evolves rapidly, you need massive horizontal scale, or you have a specific access pattern that fits a non-relational model. NoSQL databases excel at content management, real-time analytics, caching, event logging, and graph traversal.

Here is a practical comparison using PostgreSQL (SQL) and MongoDB (NoSQL) for storing user profiles with embedded addresses:

-- PostgreSQL: Normalized schema with two tables
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email VARCHAR(255) UNIQUE NOT NULL,
    name VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE addresses (
    id SERIAL PRIMARY KEY,
    user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
    street VARCHAR(255) NOT NULL,
    city VARCHAR(100) NOT NULL,
    postal_code VARCHAR(20),
    is_primary BOOLEAN DEFAULT FALSE
);

-- Querying a user with their addresses requires a JOIN
SELECT u.id, u.email, u.name, a.street, a.city
FROM users u
LEFT JOIN addresses a ON a.user_id = u.id
WHERE u.id = 42;
// MongoDB: Document schema with embedded addresses
// No migration needed — just insert documents
db.users.insertOne({
    email: "jane@example.com",
    name: "Jane Doe",
    createdAt: new Date(),
    addresses: [
        {
            street: "123 Main St",
            city: "Portland",
            postalCode: "97201",
            isPrimary: true
        },
        {
            street: "456 Oak Ave",
            city: "Seattle",
            postalCode: "98101",
            isPrimary: false
        }
    ]
});

// Querying a user with addresses is a single read — no join needed
db.users.findOne({ _id: ObjectId("...") });

Notice the trade-off: PostgreSQL enforces referential integrity and allows flexible querying across users and addresses, but requires joins. MongoDB gives you a single-document read, which is fast, but makes it harder to query "all addresses in Portland" without denormalizing or restructuring.

How to Use It: Evaluating Monolith vs Microservices

Choose a monolith when your team is small (typically fewer than 10-15 engineers), your application is in early stages, your domain boundaries are not yet clear, and you value deployment simplicity. Monoliths are easier to build, test, debug, and deploy. Most successful companies — including GitHub, Shopify, and Basecamp — started as monoliths and many still run large monolithic cores.

Choose microservices when you have multiple teams that need to work independently, different components have different scaling requirements, you need independent deployment cycles, and your domain boundaries are well understood. Microservices introduce significant operational overhead: service discovery, distributed tracing, network failure handling, and data consistency across services.

Here is a simple example showing how a single monolithic Express.js application compares to a microservice-based approach for an e-commerce API:

// Monolith: Single application handling everything
const express = require('express');
const app = express();

// All routes in one codebase, sharing one database connection
app.post('/orders', async (req, res) => {
    const { userId, items } = req.body;
    // Validate inventory
    const available = await db.query('SELECT * FROM inventory WHERE sku IN (?)', items);
    // Create order
    const order = await db.query('INSERT INTO orders ...');
    // Charge payment
    const payment = await paymentProcessor.charge(userId, order.total);
    // Send confirmation email
    await emailService.send(userId, 'order-confirmation', order);
    res.json(order);
});

app.get('/products/:id', async (req, res) => {
    const product = await db.query('SELECT * FROM products WHERE id = ?', req.params.id);
    res.json(product);
});

app.listen(3000);
// Microservice: Order service (separate codebase, separate database)
const express = require('express');
const app = express();
const axios = require('axios');

app.post('/orders', async (req, res) => {
    const { userId, items } = req.body;

    // Call Inventory Service over HTTP
    const inventoryRes = await axios.post('http://inventory-service/check', { items });
    if (!inventoryRes.data.available) {
        return res.status(409).json({ error: 'Item out of stock' });
    }

    // Save order to this service's own database
    const order = await orderDb.query('INSERT INTO orders ...');

    // Publish event to message queue for Payment and Notification services
    await messageQueue.publish('order.created', { orderId: order.id, userId, total: order.total });

    res.json(order);
});

app.listen(3001);

The microservice version is more complex: it makes a network call to the inventory service, publishes an event instead of directly calling the email service, and uses its own database. This decoupling allows the order service to be deployed and scaled independently, but it introduces network failure modes and eventual consistency challenges.

How These Decisions Interact

The database and architecture decisions are coupled in important ways:

A practical pattern is the polyglot persistence approach: each microservice chooses the database that best fits its needs. The order service might use PostgreSQL for transactional integrity, the recommendation service might use Neo4j for graph relationships, and the activity feed service might use Cassandra for high-volume time-series writes.

Best Practices

In practice, the right choice is rarely "SQL or NoSQL" or "monolith or microservices" in isolation. The right choice is the combination that matches your data shape, consistency requirements, scaling needs, and team structure at your current stage of growth. Most applications are best served by starting with a well-structured monolith backed by a SQL database, then extracting services and introducing specialized databases only when concrete demands justify the added complexity. Architecture is not a one-time decision but an ongoing evolution — the best systems are the ones that can change direction without being rewritten from scratch.

— Ad —

Google AdSense will appear here after approval

← Back to all articles