Lesson 26 of 45 intermediate

Monolith vs Microservices

One deployable or many: what each choice buys, what it costs, and how to answer in an interview

Guess first

Pick an answer. You will see if you were right at the end of the lesson.

Which statement about a monolith is accurate?

Open interactive version (quiz + challenge)

Real-world analogy

Think of a restaurant. In one big kitchen, a cook walks to the next station and hands over a plate, and it is fast and simple. If the fish station is too slow, the whole kitchen is slow. Splitting into separate food trucks lets each one grow and change on its own. But now plates travel by road, a truck can break down, and ordering a whole meal means calling five trucks. The big kitchen is the monolith, the trucks are microservices, and the roads are network calls that can fail.

What is it?

A monolith is an application deployed as a single unit. Microservices split a system into small services that deploy independently, communicate over the network and own their own data. Neither is automatically better. A monolith trades independent scaling for simplicity. Microservices trade simplicity for independence. A modular monolith sits between them: one deployable, but with modules that talk only through defined interfaces, so a later split is possible.

Real-world relevance

Some companies that adopted microservices early report spending more time on deployment, tracing and cross-service bugs than on features. Others, with dozens of teams, could not ship in one codebase and benefited from the split. The common pattern is to begin with a modular monolith, watch where teams or scaling really hurt, and extract those parts as services. Interviewers like candidates who can explain this trade-off instead of picking a side.

Key points

Quick check

Answer from memory — scrolling up first makes it stick less.

Two services read and write the same database table. What is this a sign of?

Which is a real cost that microservices add compared with a monolith?

Code example

// ---- Inventory module: public contract (other modules may reference only this) ----
public interface IInventoryApi
{
    Task<bool> TryReserveAsync(int productId, int quantity, CancellationToken ct);
}

// ---- Inventory module: internals (not visible to other modules) ----
internal class InventoryApi(InventoryDbContext db) : IInventoryApi
{
    public async Task<bool> TryReserveAsync(int productId, int quantity, CancellationToken ct)
    {
        // one atomic UPDATE: succeeds only if enough stock is left
        var rows = await db.Stock
            .Where(s => s.ProductId == productId && s.Available >= quantity)
            .ExecuteUpdateAsync(s => s.SetProperty(x => x.Available, x => x.Available - quantity), ct);
        return rows == 1;
    }
}

public static class InventoryModule
{
    public static IServiceCollection AddInventory(this IServiceCollection services) =>
        services.AddScoped<IInventoryApi, InventoryApi>();
}

// ---- Ordering module: talks to Inventory only through the interface ----
public class PlaceOrderHandler(IInventoryApi inventory)
{
    public Task<bool> ReserveStockAsync(int productId, int quantity, CancellationToken ct) =>
        inventory.TryReserveAsync(productId, quantity, ct);
}

Line-by-line walkthrough

  1. 1. IInventoryApi is the only thing other modules may see. It describes what Inventory offers, not how it stores stock.
  2. 2. InventoryApi is internal. When each module is its own project, other modules cannot see it, its DbContext or the stock table. Inside one project, internal hides nothing, so the boundary is only a convention.
  3. 3. TryReserveAsync runs one UPDATE with the stock check in its WHERE clause, so two requests cannot both take the last item.
  4. 4. ExecuteUpdateAsync returns the number of rows changed. One row means the reservation worked, and zero means not enough stock.
  5. 5. AddInventory registers the interface with its implementation, so the module wires itself up.
  6. 6. PlaceOrderHandler in the Ordering module asks for IInventoryApi and never touches Inventory tables.

Spot the bug

// OrderService (Orders database)
public async Task PlaceAsync(int productId, int qty)
{
    // Products table belongs to the Catalog service
    await ordersDb.Database.ExecuteSqlRawAsync(
        "UPDATE catalog.Products SET Stock = Stock - {0} WHERE Id = {1} AND Stock >= {0}",
        qty, productId);
}
Need a hint?
Who owns the Products table, and who is changing it?
Show answer
The Orders service writes directly to a table that the Catalog service owns. Now both services depend on one schema, so Catalog cannot change or move its table without breaking Orders, and the services are a distributed monolith. Fix: Orders should call the Catalog API (or send an event) and let Catalog change its own data.

Explain like I'm 5

A monolith is one big toy box where everything is together and easy to reach. Microservices are many small boxes in different rooms, so each one can be changed alone, but you have to walk between rooms to play with two of them.

Fun fact

Several well-known companies have publicly described moving some services back into a monolith after finding the network and operations cost too high for their size. The choice is a trade-off, and it can go both ways.

Hands-on challenge

Say it out loud in 30 seconds: "Would you build this new product as a monolith or as microservices?" Then check: did you mention a modular monolith as a starting point, what microservices buy and cost, each service owning its data, and the signals that justify a split?

More resources

Open interactive version (quiz + challenge) Download the whole course as PDF / Word

Lock it in

← Back to course: .NET + React Playbook