Mert Özen Further With Every Line
Software Architecture Microservices — #01

What Are Microservices and What Are They Not? Independent Deployability and Information Hiding

Mert Özen Nov 8, 2026 11 min 4 views
What Are Microservices and What Are They Not? Independent Deployability and Information Hiding

A microservice is a service modeled around a business domain that can be deployed without touching the others; the criterion is not size but independent deployability and information hiding. Lewis and Fowler's nine characteristics, Newman's definition, separating the contract from the internal model in C#, the difference from SOA, and Bookstore's service candidates.

Version note: The code in this post was built and run with C# 14 and .NET 10.

Guess First

A year ago the Bookstore team announced "we've moved to microservices". Today there are six separate containers: Catalog, Ordering, Inventory, Payment, Shipping and Notification. Each is its own ASP.NET Core project and they call each other over HTTP (Hypertext Transfer Protocol). But all six connect to the same SQL (Structured Query Language) Server database, the entity classes come from a shared NuGet package called Bookstore.Domain, and on every release day all six are deployed together, in a specific order.

Two questions. First: is this system microservices? Second: someone on the team proposes a 50-line PriceFormatter service that formats book prices and says "now that's a real microservice; Catalog is 40,000 lines, you can't call something that big a microservice". Which one is more "micro"? Write your answers down; by the end of the post we'll see why both answer the wrong question.

Lewis and Fowler's Definition: Nine Common Characteristics

Today's meaning of the term comes largely from "Microservices", the article James Lewis and Martin Fowler published on martinfowler.com on 25 March 2014. It describes microservice architecture as building a single application as a set of small services, each running in its own process and talking to the others through lightweight mechanisms, most often an HTTP API (Application Programming Interface). The services are organized around business capabilities and deployed independently through automation.

The authors say plainly that this is not a formal definition; what they do is list the common characteristics they saw in teams using the style:

  • Componentization via Services: components are split out not as libraries but as services running out of process.
  • Organized around Business Capabilities: teams and services are built around what the business does, not around technical layers.
  • Products not Projects: a team owns its software for its whole life in production.
  • Smart endpoints and dumb pipes: business logic lives in the services; the plumbing in between only carries messages.
  • Decentralized Governance: each team can choose its own technology and standards.
  • Decentralized Data Management: each service manages its own data.
  • Infrastructure Automation: build, test and deploy are automated.
  • Design for failure: another service going down is treated as normal and designed for.
  • Evolutionary Design: the system can be changed, and thrown away, piece by piece.

The most important sentence on the list hides inside the first item. The article defines a component as a unit that can be replaced and upgraded independently and gives the main reason for using services: a change to one service usually requires redeploying only that service. The same paragraph adds a warning: if service interfaces change, coordination is still needed. The whole promise of microservices lives in the tension between those two sentences.

Newman's Center: Independent Deployability

Sam Newman simplifies the definition further in the second edition of Building Microservices (O'Reilly, 2021): microservices are independently releasable services modeled around a business domain. The first chapter unfolds this into a few concepts: independent deployability, modeling around a domain, owning their own state, size and flexibility. Newman puts one of them at the center: can you change a service and take it to production without changing anything else?

That test answers the opening question right away. If all six containers are deployed together, what you have is the distributed monolith we described in the Introduction to Architecture series post "Understanding the Monolith: Single-Process, Modular, and Distributed Monoliths": all the network cost of microservices, with no independent deployment in return. The number of containers, the number of repositories or whether you run Kubernetes does not change that answer.

What breaks independent deployability usually comes from the same three places. A shared database: when Catalog renames a column, everyone reading that table breaks. A shared domain library: when a new version of the Bookstore.Domain package ships, everyone rebuilds and redeploys. Brittle contracts: services know each other's internal model, so an internal change in one leaks into the other. All three share a root cause: the inside of a service is visible from the outside. That takes us back to 1972.

Information Hiding: From Parnas to the Service Boundary

In "On the Criteria To Be Used in Decomposing Systems into Modules", published in Communications of the ACM (ACM: Association for Computing Machinery) in 1972, David Parnas argued that when you split a system into modules, the criterion should not be the steps of the processing flow but the design decisions that are likely to change. Each module should hide one such decision and expose only a narrow interface that does not depend on it. Newman puts this idea at the foundation of microservices: a service boundary is an information hiding boundary. The less you expose, the more you can change freely inside.

Let's see what that means for Bookstore, using Catalog. Catalog's internal model holds information the Catalog team may change at any time: supplier cost, warehouse bin, search keywords. The contract it exposes is only the part other people really need:

namespace Bookstore.Catalog;

// Catalog's internal model: invisible from outside, the Catalog team changes it at will
internal sealed class Book(Guid id, string isbn, string title, string author, decimal listPrice)
{
    public Guid Id { get; } = id;
    public string Isbn { get; } = isbn;
    public string Title { get; set; } = title;
    public string Author { get; } = author;
    public decimal ListPrice { get; set; } = listPrice;
    public decimal SupplierCost { get; set; }
    public string WarehouseBin { get; set; } = "";
    public string SearchKeywords { get; set; } = "";
}

// The contract exposed to others: only the properties they really need
public sealed record BookSummaryV1(Guid Id, string Isbn, string Title, decimal Price, string Currency);

internal static class BookMapping
{
    public static BookSummaryV1 ToContract(this Book book) =>
        new(book.Id, book.Isbn, book.Title, book.ListPrice, "EUR");
}

Ordering does not reference Catalog's classes. It reads the response into a type it defines itself, with only as many properties as it needs:

namespace Bookstore.Ordering;

// Ordering reads Catalog's response into its own type; it does not reference Catalog's classes
public sealed record CatalogBook
{
    public required Guid Id { get; init; }
    public required string Title { get; init; }
    public required decimal Price { get; init; }
}

Now let's try three Catalog versions from Ordering's point of view. The first is today's contract, the second adds a new property, the third renames one:

CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
JsonSerializerOptions web = new(JsonSerializerDefaults.Web);

Book dune = new(Guid.Parse("0199f0c4-5c1e-7000-8000-000000000001"), "978-0441013593", "Dune", "Frank Herbert", 12.50m)
{
    SupplierCost = 7.10m,
    WarehouseBin = "B-14-03",
    SearchKeywords = "arrakis spice desert"
};

// 1) Catalog v1 response: internal properties (SupplierCost, WarehouseBin) never leave
string v1 = JsonSerializer.Serialize(dune.ToContract(), web);
Console.WriteLine($"Catalog v1 : {v1}");
Console.WriteLine($"Ordering   : {JsonSerializer.Deserialize<CatalogBook>(v1, web)}");

// 2) Catalog adds a new property to the contract: deployed without touching Ordering
string v2 = """{"id":"0199f0c4-5c1e-7000-8000-000000000001","isbn":"978-0441013593","title":"Dune","subtitle":"Deluxe Edition","price":12.50,"currency":"EUR"}""";
Console.WriteLine($"Catalog v2 : additive change -> {JsonSerializer.Deserialize<CatalogBook>(v2, web)}");

// 3) Catalog renames the "price" property to "listPrice": breaking change
string v3 = """{"id":"0199f0c4-5c1e-7000-8000-000000000001","isbn":"978-0441013593","title":"Dune","listPrice":12.50,"currency":"EUR"}""";
try
{
    JsonSerializer.Deserialize<CatalogBook>(v3, web);
}
catch (JsonException ex)
{
    Console.WriteLine($"Catalog v3 : {ex.Message}");
}
Catalog v1 : {"id":"0199f0c4-5c1e-7000-8000-000000000001","isbn":"978-0441013593","title":"Dune","price":12.50,"currency":"EUR"}
Ordering   : CatalogBook { Id = 0199f0c4-5c1e-7000-8000-000000000001, Title = Dune, Price = 12.50 }
Catalog v2 : additive change -> CatalogBook { Id = 0199f0c4-5c1e-7000-8000-000000000001, Title = Dune, Price = 12.50 }
Catalog v3 : JSON deserialization for type 'Bookstore.Ordering.CatalogBook' was missing required properties including: 'price'.

The first line is information hiding itself: SupplierCost and WarehouseBin never reach the network. If Catalog completely changes its cost calculation tomorrow, or replaces warehouse bins with barcodes, it doesn't need to tell anyone. On the second line Catalog added subtitle to the contract and Ordering kept working without doing anything. That is the moment Catalog can be deployed on its own. On the third line, a small rename in Catalog broke Ordering. Shipping that change requires both teams to deploy on the same day; this is exactly where independent deployability disappears.

The required keyword plays a part here: if a property Ordering really needs is missing, it fails loudly instead of quietly opening an order at a price of 0. Techniques for changing contracts in a backward-compatible way, expand-contract and consumer-driven contract tests, are the subject of the seventh post in this series. The lesson to take away here is more basic: every property a service exposes is something that service can no longer change on its own.

The "Small Service" Myth: Boundary, Not Size

The "micro" prefix is the most misunderstood part. Lewis and Fowler give no size in the article; they report that the largest examples they saw fit Amazon's "two pizza team" idea (a team you can feed with two pizzas, a dozen people at most), and that at the small end a team of six looked after about six services. Newman doesn't see line count as a meaningful measure either, and he quotes James Lewis's quip that a microservice should be "as big as my head": small enough to understand, but big enough to carry one business capability whole.

That is why the PriceFormatter proposal is wrong. Formatting a price is not a business capability but a helper function; it has no data of its own, no decisions of its own, and no separate team that wants to change it. Moving it into its own process adds a network call to every price display, one more deployment pipeline and one more alert, and you get nothing in return. Such overly small pieces are sometimes called nanoservices and are cited as an anti-pattern. Meanwhile the 40,000-line Catalog is a business area that can have its own data, its own rules and, as it grows, its own team; line count doesn't disqualify it as a service candidate.

The right question is not "how small?" but "inside this boundary, are there things that change together, and outside it, things that change independently?". That is cohesion and coupling at service scale; it is the same thing the SOLID (Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) and Design Principles series post "Coupling and Cohesion: Two Measures of Good Design, Connascence and the Law of Demeter" describes for classes, only with a much higher price. How to draw the boundary is the subject of the third post in this series.

How It Differs from SOA

"Isn't this just SOA (Service-Oriented Architecture)?" has been asked since 2014. Lewis and Fowler's answer is careful: microservices resemble what some SOA advocates described, but the term SOA has come to mean so many different things that on its own it isn't useful. In practice, the enterprise SOA projects of the 2000s were mostly built around a central ESB (Enterprise Service Bus), with central governance and heavyweight standards. Some microservices advocates reject the SOA label; others see microservices as "SOA done right".

DimensionEnterprise SOA (common practice)Microservices
Communication infrastructureESB: routing, transformation, orchestration in the busDumb pipe: HTTP or a broker, logic in the service
GovernanceCentral standards, a shared canonical modelDecisions per team, agreement through contracts
DataOften a shared enterprise databaseEach service owns its data
Boundary criterionOften technical or integration-drivenBusiness capability and bounded context
DeploymentLarge, coordinated releasesPer service, independent and frequent

The table generalizes, and not every SOA project looked like this. But the dividing line is clear: microservices reject collecting logic in a central bus and a shared enterprise model. They put information hiding and independent change above everything else.

Bookstore's Service Candidates

In the Introduction to Architecture series we defined Bookstore's six architectural parts. For now let's read them only as service candidates: candidates, not decisions. Each hides different information and owns different data:

  • Catalog: books, authors, categories, search and price display. Hides: the search index, supplier data. Publishes: BookPriceChanged.
  • Ordering: cart, checkout, order status and campaign rules. Hides: pricing and return rules. Publishes: OrderPlaced, OrderCancelled.
  • Inventory: stock, reservations, warehouse intake. Hides: where each copy is. Publishes: StockReserved, OutOfStock.
  • Payment: authorize, capture and refund with the payment provider. Hides: the provider's model and tokens. Publishes: PaymentCaptured, PaymentFailed.
  • Shipping: shipping labels and tracking numbers. Hides: the carrier's API and the old SOAP (Simple Object Access Protocol) integration. Publishes: OrderShipped.
  • Notification: order and shipping emails, SMS (Short Message Service). Hides: the email provider, templates, delivery preferences.

These six parts can be arranged in three shapes, and the difference is less about the folder layout of the code than about how many deployment units there are and who owns the data:

01-three-shapes-en.png

These candidates match the boundaries we drew as bounded contexts in the DDD (Domain-Driven Design) series; in the third post we'll see that this is no coincidence. The .NET world also has an official reference for this shape: Microsoft's dotnet/eShop sample (on .NET 10 and Aspire) is split into separate APIs such as Catalog, Basket, Ordering and Identity; Catalog, Ordering and Identity have separate databases, Basket uses Redis, and notifications between services travel over RabbitMQ as integration events. But that doesn't mean all six candidates must become services. Bookstore today is a single team of 3 developers, and independent deployability is deliberately on the "not now" list. Which candidate deserves to become a service, and when, depends on the cost calculation that is the subject of the next post.

Common Pitfalls

Defining microservices by deployment technology. Docker, Kubernetes or separate repositories don't make a system microservices. The criterion is whether a service can change and reach production without touching the others.

Measuring size in lines of code. Splitting one business capability across several services "to keep them small" turns every change into several deployments. What should be small is not the boundary but the contract it exposes.

Making the entity the contract. Exposing Catalog's Book entity as JSON (JavaScript Object Notation) as is turns every change to the internal model into a breaking change. The internal model and the contract should be separate types with an explicit mapping in between.

A shared domain package. A NuGet package that shares entities between services kills information hiding at compile time. What can be shared should be small and stable; both Newman and the Shared Kernel discussion in the DDD series say so.

Treating microservices as the default starting point. This style is not a goal but the solution to a specific problem (teams waiting on each other, parts that scale differently). Without the problem, only the cost remains.

A Small Experiment

★☆☆ Remove the required keyword from the CatalogBook type in this post and run the third scenario again. What does Ordering see? What bug does this turn into in production, and who notices it, when?

★★☆ Pick two parts called "services" in a system you work on. In the last three months, how many times did changing one force you to deploy the other on the same day? Sort the causes into three groups: shared database, shared library and contract change.

★★★ Draft a contract for Bookstore's Payment candidate: what Ordering needs to know (payment result, amount, currency) and what Payment should hide (the provider's transaction number, card token, retry count) should be separate types. Then assume you switch providers: what changed in the contract?

In this post we reduced microservices to two concepts: independent deployability and information hiding. In the next post we look at the other side of the coin: the price that the network, operations and teams pay for this style, what the Prime Video case actually says, and a decision checklist for the question "do we need microservices?".