Manikandan — Manikandan
Microservices

Day 1: Decompose by Business Capability

ManikandanManikandan
16 min read·Updated Aug 10, 2026

Decompose by Business Capability is a way of deciding where to cut a system into microservices.

Intro

Decompose by Business Capability is a way of deciding where to cut a system into microservices. Instead of slicing by technical layer (UI, business logic, database) or by whatever code happens to be easy to move, you look at what the business does (for an insurer: sell policies, take claims, assess damage, pay out, detect fraud) and make each stable, top-level capability its own service. Business capabilities change slowly, so the service boundaries stay stable even when the org chart, screens, and technology change.

Why we need this

Business reasons

  • A business capability (e.g. “Claims Intake”) has an owner, a goal, and measurable outcomes. A service aligned to it can be funded, staffed, and prioritised like a mini-product.
  • Capabilities are far more stable than org charts, applications, or technology. “Assess a claim” existed 50 years ago and will exist in 20; the screens that do it will not.
  • Change requests arrive in business language (“change how we approve high-value claims”). If the code is organised in the same language, one request touches one service.

Technical reasons

  • Good service boundaries maximise cohesion (things that change together live together) and minimise coupling (few cross-service calls).
  • Bad boundaries produce a “distributed monolith”: all the operational cost of microservices with none of the independence.
  • Capability-based boundaries give a natural mapping to team ownership (Day 4: Service per Team) and to data ownership (Day 5: Database per Service).

What problem it solves

Problem (from the topic list): tight functional coupling and cross-departmental coordination bottlenecks.

Without it, in our insurance claims system:

  • One ClaimsApp monolith contains claim intake, adjuster assignment, payment, fraud scoring, and customer notifications in one solution and one database.
  • Finance wants to change the payout rounding rule. The change touches ClaimService.cs, which the Fraud team is also editing. Two departments must coordinate a single release.
  • The Payments team wants to deploy on Friday; the Intake team is mid-way through a half-finished feature on the same branch. Release gets blocked.
  • Everything scales together. A spike in claim submissions after a storm forces you to scale the whole app, including the rarely-used reporting code.
  • Teams split by technical layer (a “UI team”, a “DB team”) need three teams to ship one business change.

When it is needed (and when it is NOT)

It fits when

  • The system has several distinct business functions, owned by different departments or teams.
  • Teams are blocked on each other for releases.
  • You can name the capabilities with the business (they show up in the company’s operating model or value stream).
  • You are starting a monolith-to-microservices migration and need a first, defensible cut (see Day 49: Strangler Fig).

It is overkill or wrong when

  • The application is small (one team, under roughly 5-8 developers) and one deployable is fine. A well-structured modular monolith is cheaper.
  • The business is still discovering what it does (early startup); capabilities will move and you will re-cut boundaries repeatedly.
  • The domain model inside a capability is itself tangled and needs finer modelling; then combine with Day 2: Decompose by Subdomain.
  • You lack basic DevOps maturity (CI/CD, monitoring, containers). Each new service adds operational load.

How to identify the problem (key signals)

  1. Merge conflicts and release queues: several teams edit the same files (ClaimService.cs) and wait on each other to release.
  2. Cross-department meetings for every change: a “simple” rule change needs Finance, Fraud, and Customer Service in one room.
  3. God classes/tables: a Claim entity with 80+ columns serving every department; a ClaimManager class with thousands of lines.
  4. Blast radius: a bug in the notification code takes down claim submission.
  5. Uneven scaling: one hot path forces scaling of the entire application; CPU graphs show 90% of the load comes from 10% of the features.
  6. Slow builds/tests: a full pipeline takes 40+ minutes because everything is compiled and tested together.
  7. Naming mismatch: developers cannot explain the code structure in business terms (“what does Common.Helpers do?”).

Flow Diagram

How a claims monolith is cut along business capabilities, each with its own service and data.

flowchart TB
SPA["Angular SPA"] --> GW["API Gateway"]
GW --> I["claims-intake"]
GW --> A["claims-assessment"]
GW --> P["claims-payment"]
GW --> F["fraud-detection"]
I --> IDB[("IntakeDb")]
A --> ADB[("AssessmentDb")]
P --> PDB[("PaymentDb")]
F --> FDB[("FraudDb")]
I -. "ClaimRegistered" .-> BUS{{"Service Bus"}}
BUS -.-> A
BUS -.-> F
A -. "ClaimApproved" .-> BUS
BUS -.-> P
BUS -.-> N["notifications"]

Level 1: Beginner

Analogy. A hospital is not organised as “the building’s wiring department” and “the building’s paperwork department”. It is organised by what it does: Emergency, Radiology, Pharmacy, Billing. Each has its own staff, tools, and manager, and they talk through defined requests (“please send a scan”). Microservices by business capability work the same way.

How to find capabilities (3 steps)

  1. List what the business does, as verbs/nouns: Sell Policy, Manage Customers, Register Claim, Assess Claim, Pay Claim, Detect Fraud, Notify Customer.
  2. Group them into stable, top-level capabilities (avoid tiny ones like “Validate Email”).
  3. Make one service per capability. Each service owns its logic and its data.

Insurance claims capability map

CapabilityOwner (business)Service
Claim IntakeClaims Operationsclaims-intake
Claim AssessmentAdjustersclaims-assessment
Claim PaymentFinanceclaims-payment
Fraud DetectionRiskfraud-detection
Customer CommunicationCustomer Servicenotifications

Minimal example (.NET 10, C# minimal API): the Claim Intake service

// claims-intake/Program.cs (.NET 10 LTS)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<IClaimStore, InMemoryClaimStore>();
var app = builder.Build();
app.MapPost("/claims", (RegisterClaimRequest req, IClaimStore store) =>
{
var claim = new Claim(Guid.NewGuid(), req.PolicyNumber, req.Description, "Registered");
store.Add(claim);
return Results.Created($"/claims/{claim.Id}", claim);
});
app.MapGet("/claims/{id:guid}", (Guid id, IClaimStore store) =>
store.Find(id) is { } c ? Results.Ok(c) : Results.NotFound());
app.Run();
record RegisterClaimRequest(string PolicyNumber, string Description);
record Claim(Guid Id, string PolicyNumber, string Description, string Status);
interface IClaimStore { void Add(Claim c); Claim? Find(Guid id); }
class InMemoryClaimStore : IClaimStore
{
private readonly Dictionary<Guid, Claim> _items = new();
public void Add(Claim c) => _items[c.Id] = c;
public Claim? Find(Guid id) => _items.GetValueOrDefault(id);
}

Notice what is not here: no payment logic, no fraud scoring, no email sending. Intake only registers claims.

Level 2: Intermediate

In a real .NET + Angular + database application

Each capability service has: its own repository (or folder), its own database, its own pipeline, and a small public API. The Angular app calls them through one entry point (Day 19: API Gateway) and never touches another service’s database.

Angular SPA
|
API Gateway (YARP / Azure API Management)
|-- /api/claims/** -> claims-intake (SQL Server: IntakeDb)
|-- /api/assessments/** -> claims-assessment (PostgreSQL: AssessmentDb)
|-- /api/payments/** -> claims-payment (SQL Server: PaymentDb)
|-- /api/fraud/** -> fraud-detection (PostgreSQL: FraudDb)

Rule for data: the Intake service stores PolicyNumber as a reference. It does not join to the Policy tables. If it needs policy details it calls the Policy service or keeps a small local copy fed by events (Day 11: Domain Event).

Service structure (intake) using EF Core with SQL Server

claims-intake/Domain/Claim.cs
public class Claim
{
public Guid Id { get; private set; } = Guid.NewGuid();
public string PolicyNumber { get; private set; } = default!;
public string Description { get; private set; } = default!;
public ClaimStatus Status { get; private set; } = ClaimStatus.Registered;
public DateTimeOffset RegisteredAt { get; private set; } = DateTimeOffset.UtcNow;
private Claim() { } // EF Core
public Claim(string policyNumber, string description)
{
if (string.IsNullOrWhiteSpace(policyNumber))
throw new ArgumentException("Policy number is required.", nameof(policyNumber));
PolicyNumber = policyNumber;
Description = description;
}
}
public enum ClaimStatus { Registered, UnderAssessment, Approved, Rejected, Paid }
// claims-intake/Data/IntakeDbContext.cs
public class IntakeDbContext(DbContextOptions<IntakeDbContext> options) : DbContext(options)
{
public DbSet<Claim> Claims => Set<Claim>();
}
// claims-intake/Program.cs (registration)
builder.Services.AddDbContext<IntakeDbContext>(o =>
o.UseSqlServer(builder.Configuration.GetConnectionString("IntakeDb")));

Angular: a feature per capability, calling through the gateway (standalone component, inject(), signals; valid in current Angular major versions)

claims.service.ts
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
export interface Claim { id: string; policyNumber: string; description: string; status: string; }
@Injectable({ providedIn: 'root' })
export class ClaimsService {
private http = inject(HttpClient);
register(policyNumber: string, description: string) {
return this.http.post<Claim>('/api/claims', { policyNumber, description });
}
}

Real-world guidance

  • Name the folders and namespaces after the capability (Claims.Intake), not the layer.
  • Run a short workshop (Event Storming or a capability-mapping session) with business owners to find the list. Do not derive it from the existing database tables.
  • Start with two or three services, not fifteen.

Level 3: Advanced

Performance and scalability

  • Each capability scales on its own. Intake may need 10 replicas after a hailstorm while Payment stays at 2.
  • Cross-capability reads (e.g. “claims list with payment status”) become network calls. Avoid chatty calls: use API Composition (Day 10), a read model (Day 8: CQRS), or events to keep a local copy.

Security

  • Each service validates the caller’s token itself (Day 38: Access Token) and enforces its own authorisation, e.g. only Finance roles can call claims-payment.
  • Smaller services give a smaller attack surface per service and let you isolate sensitive data (payment details) in one boundary.

Failure modes

  • If Assessment is down, Intake must still accept claims. Prefer async messaging (Day 16) over synchronous calls between capabilities.
  • A workflow that spans capabilities (register, assess, pay) cannot use one database transaction. It needs a Saga (Day 7).

Common mistakes

  1. Too fine-grained: “Claim Validation Service”, “Claim Numbering Service”. You get chatty services and a distributed monolith. A capability is a business function, not a method.
  2. Splitting by technical layer: a “data-access service” or “UI service” is not a business capability.
  3. Mirroring today’s org chart: org charts change every year. Use the capabilities, not the reporting lines.
  4. Sharing the database “temporarily” forever: if two services read the same tables, they are one service (see Day 6 for the interim approach and its risks).
  5. Shared “Common” library holding domain models: it re-couples every service at compile time. Share only technical concerns (see Day 40: Chassis).
  6. Ignoring the data: cutting code but not data leaves the coupling in place.

Sanity checks for a boundary

  • Can one team change the capability and deploy it without asking another team?
  • Does most of a typical change stay inside one service?
  • Can you explain the service’s purpose in one business sentence without the word “and”?

Level 4: Expert and Architect view

Alternatives compared

ApproachBoundary is based onStrengthWeaknessChoose when
Decompose by business capabilityWhat the business does (stable)Stable, business-aligned, easy to explain, maps to teamsBoundaries can still hide tangled domain models insideStarting a decomposition; org has clear functions
Decompose by subdomain (Day 2)DDD subdomains / bounded contextsPrecise models; separates Core vs Supporting vs GenericNeeds strong domain modelling skillInside a capability that is complex; large domains
Decompose by technical layerUI / logic / dataSimple for small teamsEvery change crosses layers; no independenceAlmost never for microservices
Decompose by use case / verbOne service per operationVery small servicesChatty, high overheadRarely; some serverless designs
Modular monolithSame capability modules, one deployableCheap ops, easy refactoring, transactionalSingle deploy, shared runtimeSmall teams; capabilities not yet proven

In practice, capability and subdomain decomposition are used together: capabilities give the first coarse cut, and DDD bounded contexts refine what is inside each.

Patterns it combines with

  • Day 2 Decompose by Subdomain (refine boundaries), Day 4 Service per Team (ownership), Day 5 Database per Service (data isolation), Day 7 Saga (cross-capability workflows), Day 11 Domain Event (integration), Day 19 API Gateway (client access), Day 49 Strangler Fig (migration path).
  • Conway’s Law and the “Inverse Conway Maneuver”: design the team structure to produce the architecture you want.

ADR-style justification

ADR-001: Use business capabilities as the primary microservice boundary for the Claims Platform Status: Proposed Context: The current claims monolith is edited by four departments’ teams. Releases occur every 6 weeks, and merge conflicts and cross-team coordination cause most delays. Claim volume spikes during weather events and only the intake path needs extra capacity. Decision: Decompose along five top-level business capabilities: Claim Intake, Claim Assessment, Claim Payment, Fraud Detection, and Customer Communication. Each gets its own service, database, and pipeline. Start by extracting Fraud Detection and Notifications (low coupling), then Payment. Consequences (positive): independent releases per capability; targeted scaling; clearer ownership; smaller blast radius. Consequences (negative): distributed data consistency needs sagas; more infrastructure and monitoring; cross-service queries need composition or read models; boundary mistakes are costly to fix later. Alternatives considered: modular monolith (rejected for now because independent scaling and release are required); technical-layer split (rejected: no independence); subdomain-only (deferred: applied later inside Assessment where the model is complex). Review trigger: if more than about 30% of changes span two or more services after 6 months, revisit the boundaries.

Azure implementation

This pattern is a design method, so no Azure service “implements” it. Azure provides the hosting, routing, data, and observability for the resulting services.

Services

NeedAzure service
Host each capability serviceAzure Container Apps (simplest for teams without Kubernetes skills) or Azure Kubernetes Service (AKS) for full control; App Service is also valid per service
Edge / routingAzure API Management, or Azure Application Gateway, or YARP running as a container
Data per serviceAzure SQL Database (SQL Server engine), Azure Database for PostgreSQL flexible server
Async integrationAzure Service Bus
Secrets/configAzure Key Vault, Azure App Configuration
ObservabilityApplication Insights / Azure Monitor (Log Analytics workspace)
CI/CDAzure DevOps Pipelines or GitHub Actions, one pipeline per service; Azure Container Registry
IdentityMicrosoft Entra ID (with managed identities for service-to-service access)

How to configure (typical)

  1. One resource group per environment (or per capability group), one Azure Container Apps environment shared by related services.
  2. One container app per capability, with its own managed identity and its own database. Grant the identity access to only its database and Key Vault secrets.
  3. Ingress: expose only the gateway externally; keep capability services with internal ingress.
  4. Autoscaling per service: HTTP concurrency rule for Intake; queue-length (KEDA) rule for Payment workers reading Service Bus.
  5. Tag resources with capability and owner so cost and incidents map back to a business owner.

Pricing and tier considerations (verify current numbers on the Azure pricing pages before budgeting; prices vary by region and change over time)

  • Azure Container Apps: the Consumption plan bills for vCPU/memory usage and requests, with a monthly free grant, and can scale to zero. The Dedicated (workload profile) option gives reserved capacity for steady workloads. Note that a scale-to-zero service has cold-start latency, so keep Intake with a minimum of 1 replica.
  • AKS: you pay for the nodes; the control plane has a free tier and a paid Standard tier with an uptime SLA. Cheaper at scale, but needs platform skills.
  • Azure API Management: has Consumption, Developer, Basic, Standard, and Premium classic tiers, plus v2 tiers (Basic v2, Standard v2, Premium v2). Pick by SLA, VNet needs, and throughput; Developer tier is not for production.
  • Azure SQL / PostgreSQL: one database per service multiplies cost. Use serverless or elastic pools for small, spiky services, and keep production-critical ones (Payment) on provisioned tiers.
  • Cost warning: more services means more databases, more monitoring ingestion, and more pipelines. Count these before deciding on fine-grained splits.

Reference architecture (text)

Users open the Angular SPA hosted on Azure Static Web Apps (or Blob Storage + Front Door). The SPA calls Azure API Management (or a YARP gateway), which validates the Entra ID token and routes /api/claims to claims-intake, /api/assessments to claims-assessment, and so on. All services run as container apps in one Container Apps environment inside a virtual network. Each service uses its managed identity to read secrets from Key Vault and connect to its own private database (Azure SQL for Intake and Payment; PostgreSQL for Assessment and Fraud) over private endpoints. Cross-capability communication uses Service Bus topics (e.g. claim-registered), consumed by Assessment, Fraud, and Notifications. All services send telemetry to Application Insights with a shared operation ID, so a claim can be traced across capabilities.

Teaching guide for my team

2-minute beginner explanation “Imagine our claims department. There’s a desk that receives claims, a desk that judges them, a desk that pays, and a desk that checks for fraud. Each desk has its own people and its own filing cabinet, and they pass notes to each other. We build our software the same way: one small application per desk. When Finance changes the payout rules, only the Payment application changes. The Intake desk never notices. We choose desks by what the business does, not by technical layers.”

5-minute intermediate explanation Start with the monolith pain (shared ClaimService, one release train). Show the capability map. Explain that each service owns its own data and exposes an API or events. Show how the Angular app reaches the services through a gateway. Then cover the trade-offs: a workflow across capabilities can no longer use a single transaction (introduce Saga), cross-service reports need composition or read models, and there is a real operational cost (more pipelines, databases, and monitoring). Finish with the boundary sanity checks: independent deploy, changes stay inside one service, one-sentence purpose.

Hands-on exercise Task: Given this list of features of a legacy claims app: “submit claim, upload photos, assign adjuster, calculate reserve, approve claim, issue payment, send SMS, score fraud risk, generate monthly finance report, manage customer profile”. Group them into 4-6 business capabilities, name a service for each, list which data each one owns, and identify the two calls or events that cross a boundary. Expected outcome: Something close to: Claim Intake (submit claim, upload photos), Claim Assessment (assign adjuster, calculate reserve, approve claim), Claim Payment (issue payment, finance report data), Fraud Detection (score fraud risk), Customer Communication (send SMS), Customer Management (profile). Crossing points: ClaimRegistered event (Intake to Assessment, Fraud) and ClaimApproved event (Assessment to Payment, Notifications). Participants should be able to justify why “upload photos” is not its own service. (There may be more than one reasonable answer; look for justification, not an exact match.)

Interview-style questions

  1. How is decomposing by business capability different from decomposing by technical layer? Capability slicing creates vertical services, each with its own UI-facing API, logic, and data, aligned to a business function. Layer slicing creates horizontal services, so every feature change crosses several services and teams.
  2. How do you know a capability is too small? If it has no independent business purpose, needs a synchronous call from almost every other service, or can’t be changed and deployed without changing another service, it is a function, not a capability. Merge it.
  3. Two capabilities both need customer data. What do you do? Only one service (Customer Management) owns the customer data. The others store a customer ID, and either query it via API or keep a small read-only copy updated via events. They never read its database directly.

Mastery checklist

  • I can define a business capability and distinguish it from a function, a use case, and a technical layer.
  • I can produce a capability map for a domain with the business owners (workshop, not from the database schema).
  • I can apply the three sanity checks (independent deploy, change stays inside, one-sentence purpose) to a proposed boundary.
  • I can spot a distributed monolith and name at least three symptoms.
  • I can explain how data ownership follows the capability, and what replaces cross-service JOINs and transactions.
  • I can explain when a modular monolith is the better choice.
  • I can write an ADR justifying the decomposition, including negative consequences.
  • I can describe how the resulting services are hosted, secured, and monitored on Azure.

Key takeaway

Cut services along what the business does, not along how the code is layered, so that a business change stays inside one service, one team, and one release. Start coarse, and refine with subdomains only where a capability proves too complex.

Interactive Architectural Roadmaps

Explore Complete Roadmaps & Pattern Checklists

Track your learning with interactive checklists for all 23 Gang of Four patterns and modern Microservice architecture patterns.

Share:
Back to Blog

Related Posts

View All Posts
Microservices

Day 4: Service per Team

Service per Team means every service boundary has exactly one cross-functional team that owns it end to end: code, database, pipeline, production operation, and on-call.

Manikandan
Manikandan·21 min read
Microservices

Day 3: Self-contained Service

A self-contained service answers a client request using only its own code and its own local data.

Manikandan
Manikandan·16 min read
Microservices

Day 2: Decompose by Subdomain

Decompose by Subdomain uses Domain-Driven Design (DDD) to split a large business domain into subdomains (Core, Supporting, Generic) and gives each one its own bounded context, its own model, and usually its own service.

Manikandan
Manikandan·15 min read
Microservices

Day 53: Client-Side UI Composition (Micro-Frontends)

Client-Side UI Composition splits one large single-page application into several smaller front-end applications ("micro-frontends") that are built, tested, and deployed independently by different teams.

Manikandan
Manikandan·17 min read