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?
Real-world analogy
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
- A monolith is one deployable — All the code ships and runs as one unit. That says nothing about quality, and a monolith can be well organised or messy.
- Modular monolith first — One deployable with clear module boundaries is often a sensible starting point. Calls are fast, transactions are simple, and the seams exist if you split later.
- What microservices buy — Each service can deploy and scale on its own, and a team can own it end to end. That helps when many teams block each other in one codebase.
- What they cost — In-process calls become network calls that can time out or fail. You also take on distributed data, tracing, versioned contracts and many things to deploy and watch.
- Each service owns its data — A service keeps its own database and exposes data through its API or events. Other services should not read its tables directly.
- The distributed monolith — Services that share a database, or must deploy together, have the costs of both worlds. You pay for the network and still cannot change one without the others.
- Signals it may be time — Teams wait on each other to release, one part needs very different scaling, or one part needs a different technology. Slowness alone is rarely enough, because it can often be fixed inside the monolith.
- Answering "which would you pick" — Say it depends, then name the factors: team size, how often parts change, scaling needs and operations skill. Many teams start with a modular monolith and split only when evidence appears.
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. IInventoryApi is the only thing other modules may see. It describes what Inventory offers, not how it stores stock.
- 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. TryReserveAsync runs one UPDATE with the stock check in its WHERE clause, so two requests cannot both take the last item.
- 4. ExecuteUpdateAsync returns the number of rows changed. One row means the reservation worked, and zero means not enough stock.
- 5. AddInventory registers the interface with its implementation, so the module wires itself up.
- 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?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Architecting container and microservice-based applications (Microsoft Learn)
- Common web application architectures (Microsoft Learn)
- Microservices architecture style (Microsoft Learn)
Lock it in