N
Noah Sheldon
BlogProjectsSkillsExperienceLinks
free ai audit
Home/Blog/High Level Design/Modern Tech Stack Evolution: From Monoliths to Microservices
OverviewHigh Level DesignArchitectureCloud

Modern Tech Stack Evolution: From Monoliths to Microservices

How architectural decisions have shifted in the age of cloud-native development and serverless computing.

Jul 19, 2026
12 min

Ten years ago, if you asked a software architect "what is the right architecture for my application?", the most common answer was "start with a monolith, split it when you have to." It was practical advice. Today, the answer is more nuanced - not because monolithic architecture stopped working, but because the tools and patterns for distributed systems have matured dramatically.

In my career across a global investment bank, a healthcare analytics firm,a financial services firm, and my own products, I have lived through this evolution. I have built monoliths, migrated them to microservices, and watched the industry swing from one extreme to the other. This post captures what I have learned about when and how to make that transition.

What you will learn: Why monoliths are still the right starting point · When microservices actually make sense · Domain-driven design as the decomposition guide · Communication protocols: REST, gRPC, event streaming · The Strangler Fig migration pattern · Data management: database-per-service, event sourcing · Serverless, service mesh, and the future · A decision framework for choosing your architecture


1. The Monolith: Where Every System Starts

The monolith gets a bad reputation, but it is the correct default for almost every new project. A well-structured monolith - with clear module boundaries, a clean domain model, and a single deployment unit - is faster to build, easier to test, and simpler to operate than any distributed alternative.

The problems with monoliths are not architectural - they are organizational. As your team grows, code ownership blurs. Deployments become coupling nightmares. A change to one feature requires QA-regressing the entire application.

Diagram 1: When a monolith makes sense vs. when it breaks

Monolith Works Great• 1–10 developers• Single business domainMonolith Breaks• 10+ developers on one codebase• Multiple distinct business domainsThe trigger: organizational coupling, not code size

2. When Microservices Actually Help

Microservices are not a scaling strategy - they are an organizational strategy. They exist to let different teams work independently. The technical benefits (independent scaling, fault isolation, technology diversity) are secondary to the primary goal: team autonomy.

You should consider microservices when you have:

  1. Multiple teams that need to ship independently on different schedules.
  2. Distinct business domains with clear boundaries (e.g., user management, payments, analytics).
  3. Different scaling requirements - one part of the system needs 100x more compute than another.
// A decision framework for microservices
function shouldAdoptMicroservices(context: {
  teamSize: number;
  distinctDomains: number;
  scalingDifferential: number;  // ratio of heaviest to lightest workload
}): "monolith" | "modules" | "services" {
  if (context.teamSize < 8 && context.distinctDomains < 3) {
    return "monolith";  // You are not ready yet
  }
  if (context.scalingDifferential > 10) {
    return "services";  // Heavy vs light workloads need separation
  }
  return "modules";  // Well-structured modules in a monorepo
}

3. Domain-Driven Design: Finding the Service Boundaries

The hardest part of microservices is deciding where to draw the boundaries. Get it wrong, and you end up with distributed big ball of mud - services that are so tightly coupled they might as well be a monolith, but with network latency between every call.

Domain-driven design (DDD) provides the answer. Instead of designing services around technical layers (controllers, services, repositories), design them around business domains:

Diagram 2: DDD service boundaries in a trading system

Trade ProcessingOrder entry,confirmation, settlementRisk ManagementPosition keeping,VaR calculationReportingEMIR, MiFID II,audit trailsWrong: TradeController.cs · DatabaseAccess.cs · ReportService.csRight: TradeProcessing/ · RiskManagement/ · Reporting/

Identifying Bounded Contexts

// Before: services organized by technical layers
// (wrong - no business meaning)
services/
  controllers/        # All the controllers
  services/           # All the business logic
  repositories/       # All the data access

// After: services organized by bounded contexts
// (right - each has its own domain language)
services/
  trade-processing/   # "trade", "confirmation", "settlement"
  risk-management/    # "position", "var", "exposure"
  reporting/          # "report", "regulation", "audit"

// Each bounded context owns its data, its language, its model.
// No shared database. No shared entity classes.
// Communication through events, not shared models.

4. Communication Protocols: Picking the Right Channel

Once you have services, they need to talk to each other. The choice of communication protocol is one of the most consequential decisions you will make:

Diagram 3: Communication protocol tradeoffs

REST / HTTPSimple, universal,everyone speaks itgRPCFast, typed,bidirectional streamingEvent BusAsync, decoupled,eventual consistencyRule: REST for external APIs · gRPC for internal · Events for integration

REST: The Universal Interface

// POST /api/trades - Create a new trade
// (used by the frontend and external systems)
{
  "trade_id": "IRS-2025-03-15-0042",
  "counterparty": "BANK_A",
  "notional_amount": 50000000.00,
  "currency": "GBP",
  "maturity_date": "2030-06-15"
}

Event-Driven: The De-coupler

For cross-service communication, events are more decoupled than direct calls. When a trade is settled, the Trade Processing service emits an event:

// Event emitted by Trade Processing
{
  "event_type": "trade.settled",
  "producer": "trade-processing-service",
  "timestamp": "2025-03-15T14:30:00Z",
  "payload": {
    "trade_id": "IRS-2025-03-15-0042",
    "settlement_status": "confirmed"
  }
}

// The Risk Management service subscribes to "trade.settled"
// → recalculates VaR
// The Reporting service subscribes to "trade.settled"
// → generates EMIR report

// Neither service knows about the other.
// New subscribers can be added without changing the producer.

5. Migration: The Strangler Fig Pattern

The hardest migration I have done was at a global investment bank, moving a post-trade processing system from a monolith to microservices. The system could not go down - every minute of downtime meant delayed settlements and regulatory risk.

The Strangler Fig pattern let us replace the monolith piece by piece:

Diagram 4: The Strangler Fig migration

Step 1: CoexistMonolithNew ServiceStep 2: RouteMonolith (smaller)New Service (live)Step 3: RemoveOld code deleted
// Step 1: Create the new service alongside the monolith
// The new service handles one bounded context (e.g., Reporting)

// Step 2: Route traffic through a proxy
// Use a feature flag to send specific requests to the new service
if (featureFlags.isEnabled("reporting-service")) {
  return await reportingService.handle(request);
} else {
  return await monolith.handle(request);
}

// Step 3: Monitor and validate
// Compare outputs from old and new systems in production
async function validateMigration(request) {
  const newResult = await reportingService.handle(request);
  const oldResult = await monolith.handle(request);

  if (!deepEqual(newResult, oldResult)) {
    await alertOncall("Migration divergence detected", { newResult, oldResult });
  }
  return newResult;  // Trust the new service
}

// Step 4: Route ALL traffic to the new service
// Step 5: Delete the old code

6. Data Management: Database-Per-Service

In a monolith, every module accesses the same database. In microservices, each service owns its data. No other service queries its database directly. This is non-negotiable - it is what makes services independently deployable.

Event Sourcing for Audit Trails

In financial systems, you cannot just update a record - you need to know what the value was before and after, who changed it, and when. Event sourcing stores every state change as an immutable event:

// Every state change is an event - never deleted, never updated
events = [
  { type: "trade.created",    data: { id: "A", amount: 100 }, timestamp: "T1" },
  { type: "trade.amended",    data: { id: "A", amount: 150 }, timestamp: "T2" },
  { type: "trade.cancelled",  data: { id: "A" },              timestamp: "T3" },
]

// Current state = replay all events
function getCurrentState(events) {
  return events.reduce((state, event) => {
    if (event.type === "trade.created")  return { ...state, ...event.data, status: "active" };
    if (event.type === "trade.amended")  return { ...state, ...event.data };
    if (event.type === "trade.cancelled") return { ...state, status: "cancelled" };
    return state;
  }, {});
}

// Audit query: what was the state at T2?
function getStateAt(events, timestamp) {
  return getCurrentState(events.filter(e => e.timestamp <= timestamp));
}

7. The Future: Serverless and Service Mesh

Two trends are shaping the next generation of distributed systems:

Diagram 5: The evolving stack

Serverless (FaaS)No servers to managePay-per-call pricingService Mesh (Istio)Traffic mgmt, securityObservability out-of-boxCommon PatternEvent-driven coreServerless edgesMesh for traffic control

The winning architecture for most systems today is not "pure microservices" or "pure monolith" - it is a hybrid. A core monolith or set of services for the critical path (where consistency and latency matter), with serverless functions for event handlers, webhooks, and batch jobs.


8. Summary: What to Remember

Monolith by Default · Split by Domain · Migrate Incrementally
  • Start with a monolith. It is the fastest, simplest, and most reliable architecture for new projects. Do not adopt microservices until you have the team and domain complexity that justifies them.
  • Use domain-driven design to find service boundaries. Services should align with business domains (Trade Processing, Risk Management, Reporting), not technical layers (Controllers, Services, Repositories).
  • The Strangler Fig pattern is the only safe way to migrate a live system. Never rewrite the entire monolith at once - replace it one bounded context at a time.
  • Database-per-service is non-negotiable. Shared databases create hidden coupling that destroys the independence microservices are supposed to provide.
  • Choose protocols by context: REST for external APIs, gRPC for high-performance internal calls, event streaming for cross-service integration. Using the wrong protocol creates accidental complexity.

Chapter

High Level Design

Reading level

Overview

About the author

NS

Noah Sheldon

Applied AI/ML @ Fitch

Keep reading

Agentic Design

Beyond the Chatbot: The Rise of Agentic Orchestrators

The shift from conversational AI to autonomous execution. How agentic orchestrators work, why they matter, and what it takes to build them at enterprise scale.

Read · 8 min

High Level Design

Scaling Trading Systems at BNP Paribas: Lessons in Financial Technology

Deep dive into building resilient post-trade systems that handle millions in daily transactions.

Read · 15 min

High Level Design

System Design Fundamentals: Scaling Zero to Millions

Comprehensive guide to system design principles - from MVP architecture to distributed systems handling millions of requests.

Read · 15 min
NS

Noah Sheldon

Associate Director, Applied AI/ML @ Fitch Ratings

Get a free AI opportunity audit

never miss a deep dive

New essays land when they ship. No spam, unsubscribe anytime.