Monolith To Microservices

These are my notes on the book Monolith to Microservices by Sam Newman.

Monolith to Microservices - Key Concepts

Chapter 1: Just Enough Microservices

Definition and Core Concepts

  • Microservices are independently deployable services modeled around a business domain.
  • They communicate with each other via networks, making them a form of distributed system.
  • Microservices encapsulate data storage and retrieval, exposing data via well-defined interfaces.
  • Databases are hidden inside the service boundary.
  • Independent deployability is the idea that we can make a change to a microservice and deploy it into a production environment without having to utilize any other services.

Architecture Principles

  • Microservices should not share databases. If one service wants to access data held by another service, then it should go and ask that service for the data it needs.
  • One of the main challenges is the way computers talk to each other: networks.
  • With microservices modeled around a business domain, we see alignment between IT artifacts and business domain.
  • Understanding the balancing forces between coupling and cohesion is important when defining microservice boundaries:
    • Coupling speaks to how changing one thing requires a change in another
    • Cohesion talks to how we group related code
    • A structure is stable if cohesion is high, and coupling is low.” - Larry Constantine
  • For better change management, we group code by cohesion of business functionality, rather than technology.

Summary

  • Microservices are independently deployable services modeled around a business domain
  • They communicate via networks
  • We use information hiding together with domain-driven design to create services with stable boundaries
  • Services should be easier to work on independently
  • We should reduce the many forms of coupling

Chapter 2: Planning a Migration

Understanding the Goal

  • Microservices are not the goal in themselves
  • You should migrate to microservices to achieve something that you can’t currently achieve with your existing system architecture
  • What you’re trying to achieve will greatly change where you focus your time and how you prioritize efforts

Three Key Questions

  1. What are you hoping to achieve? This should be aligned to what the business is trying to achieve, articulated as benefit to end users.
  2. Have you considered alternatives to using microservices? There are often many other ways to achieve the same benefits.
  3. How will you know if the transition is working? How will you know if you’re going in the right direction?

Why Choose Microservices?

Improve Team Autonomy

  • It’s all about your people - providing them with confidence, motivation, freedom and desire to achieve their potential
  • Keeping organizational groups small helps them build close bonds and work effectively together with less bureaucracy
  • Alternative approaches: Pushing more responsibility into teams doesn’t necessarily require architecture changes

Reduce Time to Market

  • Being able to make and deploy changes to individual microservices without waiting for coordinated releases helps release functionality more quickly
  • Alternative approaches: Many variables affect shipping speed; a path-to-production modeling exercise may help identify the real blockers

Scale Cost-Effectively for Load

  • Breaking processing into individual microservices allows independent scaling of only the parts constraining load
  • Can scale down microservices under less load, potentially turning them off when not required
  • Alternative approaches: Various caching strategies, read replicas, or horizontal scaling of monolith behind load balancer

Improve Robustness

  • Breaking applications into independently deployable processes opens up mechanisms to improve robustness
  • Alternative approaches: Running multiple copies of the monolith behind a load balancer or queue to add redundancy

Scale the Number of Developers

  • With clearly identified boundaries and limited coupling, we create pieces of code that can be worked on independently
  • Alternative approaches: Improve team structure, create clearer module boundaries within monolith

Embrace New Technology

  • Monoliths typically limit technology choices
  • By isolating technology changes within service boundaries, we can understand benefits in isolation and limit impact if issues arise
  • Alternative approaches: Feature flags, branch by abstraction, reducing risk by limiting scope

When Microservices Might Be a Bad Idea

Unclear Domain

  • Getting service boundaries wrong can be expensive, leading to cross-service changes and overly coupled components
  • Could be worse than having a single monolithic system

Customer-Installed and Managed Software

  • If your software is packaged and shipped to customers who operate it themselves, microservices may be a bad choice
  • Microservice architecture pushes complexity into the operational domain

Not Having a Good Reason

  • The biggest reason not to adopt microservices is not having a clear idea of what you’re trying to achieve

Importance of Incremental Migration

  • An incremental approach helps you learn about microservices as you go
  • Limits the impact of getting something wrong
  • Think of the monolith as a block of marble - chip away at it incrementally rather than blowing it up
  • If you’re doing everything at once, it can be difficult to get good feedback about what is/isn’t working well

Cost of Change

  • Making small, incremental changes allows understanding the impact of each alteration
  • Helps mitigate the cost of mistakes, though doesn’t eliminate mistakes entirely
  • We can and will make mistakes, and should embrace that while mitigating costs

Domain Modeling

  • Coming up with a domain model is a near-essential step in structuring a microservice transition
  • Helps when prioritizing decomposition
  • What’s needed is just enough information to make reasonable decisions about where to start decomposition

Event Storming

  • A collaborative exercise involving technical and non-technical stakeholders
  • Works from the bottom up: define “Domain Events” (things that happen in the system)
  • Group events into aggregates, then group aggregates into bounded contexts
  • The output isn’t just the model itself, but the shared understanding of the model
  • Based on upstream/downstream dependencies, we can identify which functionality is easier or harder to extract

Measuring Success

  • Define measures that can be tracked to answer if the transition is working
  • Establish checkpoints for reflection on whether you’re heading in the right direction

Regular Checkpoints

  • Build pause and reflection time into your delivery process
  • Analyze available information and determine if a change of course is required

Quantitative Measures

  • Select measures based on the goals you’re trying to achieve

Qualitative Measures

  • Include feedback from the people building the software
  • Are they enjoying the process? Do they feel empowered or overwhelmed?
  • Get the support they need for new responsibilities or skills?
  • Include a sense check from your team when reporting to management

Avoiding the Sunk Cost Fallacy

  • Have a review process to keep you honest
  • Making each step small makes it easier to avoid the pitfalls of sunk cost fallacy

Being Open to New Approaches

  • Multiple variables and different paths exist for breaking apart a monolithic system
  • Be prepared to experiment with different approaches

Chapter 3: Splitting the Monolith

Incremental Migration

  • Migrate to microservices in small steps
  • Learn from the process and change minds if needed
  • If you can change the existing system, you have more flexibility in available patterns

Pattern: Strangler Fig Application

  • Have the new system initially be supported by and wrapping the existing system
  • Old and new can coexist, giving the new system time to grow and potentially replace the old system
  • Relies on three steps:
    1. Identify parts of the existing system to migrate
    2. Implement functionality in new microservice
    3. Reroute calls from monolith to new microservice
  • Until the call is redirected, the new functionality isn’t technically live
  • Important distinction: Deployment and release are separate concepts
    • Just because software is deployed doesn’t mean it’s being used by customers
    • Treating these as separate allows validation in the production environment before use
    • Patterns like strangler fig, parallel run, and canary release use this distinction
  • A key benefit is the ability to roll back changes easily if required

Pattern: UI Composition

  • Allows for re-platforming systems by migrating whole vertical slices of functionality
  • Requires ability to change the existing user interface to allow new functionality to be safely inserted

Pattern: Parallel Run

  • Instead of calling either old or new implementation, we call both, allowing comparison of results
  • Only so much testing can be done before deployment
  • Especially useful when comparing results to ensure they’re equivalent

N-Version Programming

  • Multiple implementations of the same functionality used side by side in safety-critical systems
  • Signals sent to all implementations, which then send their response
  • Results compared and “correct” one selected by looking for quorum
  • Unlike other patterns, these implementations continue to exist alongside each other
  • Alternative implementations reduce the impact of bugs in any one subsystem

Pattern: Decorating Collaborator

  • Used when you want to trigger behavior based on something in the monolith but can’t change the monolith itself
  • The decorator pattern attaches new functionality without the underlying system knowing
  • Makes it appear that the monolith is making calls to services directly without changing the monolith

Pattern: Change Data Capture

  • Rather than intercepting calls made into the monolith, we react to changes made in a datastore

Chapter 4: Decomposing the Database

Pattern: The Shared Database

  • Sharing a single database among multiple services has several issues:
    • Denies opportunity to decide what’s shared and what’s hidden, contradicting information hiding
    • Makes it difficult to understand what parts of schema can be changed safely
    • Creates unclear ownership of data

Coping Patterns

  • Splitting the database to allow each microservice to own its data is nearly always preferred
  • Direct sharing of a database is appropriate only in two situations:
    1. For read-only static reference data
    2. When a service directly exposes a database as a defined endpoint designed for multiple consumers

Pattern: Database View

  • Useful when we want a single source of data for multiple services
  • A view presents a service with a schema that is a limited projection from an underlying schema
  • Can limit data visibility, hiding information services shouldn’t access
  • Implements a form of information hiding by controlling what’s shared vs. hidden

Pattern: Database Wrapping Service

  • When something is too hard to deal with, hiding the mess can make sense
  • Creates a thin wrapper around the database, moving database dependencies to service dependencies
  • Works well when the underlying schema is too difficult to pull apart

Pattern: Database-as-a-Service Interface

  • Sometimes clients need a database to query (for large data amounts or using SQL-requiring tool chains)
  • Allow clients to view data managed in a database, but separate exposed database from internal service database
  • Useful for reporting scenarios where clients need to join across large amounts of data
  • Could extend to importing data into a larger data warehouse

Transferring Ownership

  • Before pulling data from a monolithic database, consider where data should actually live

Pattern: Aggregate Exposing Monolith

  • When the newly extracted service encapsulates business logic that changes data, that data should be under the new service’s control
  • Data should move from its current location to the new service

Data Synchronization

  • The challenge occurs when the service manages data that needs to be kept in sync between monolith and new service

Pattern: Synchronize Data in Application

  • Implementation steps:
    1. Bulk Synchronize Data
    2. Synchronize on Write, Read from Old Schema
    3. Synchronize on Write, Read from New Schema
  • This pattern may make sense if you want to split the schema before splitting application code

Pattern: Tracer Write

  • Move the source of truth for data incrementally, tolerating two sources during migration
  • Wanting a single source of truth is rational (ensures consistency, controls access, reduces maintenance)
  • But insisting on only one source forces a big switchover, which has risks
  • Phased switchover reduces impact of each release by tolerating multiple sources of truth
  • Key challenge: addressing inconsistency when data is duplicated
Approaches to resolve inconsistency:
  1. Write to one source: All writes go to one source of truth, then synchronize to the other
  2. Send writes to both sources: All write requests sent to both sources (either by client or intermediary)
  • In all cases, there will be delay before data is consistent in both sources
  • Creates eventual consistency - eventually both sources will have the same data
  • Need to understand acceptable inconsistency period for your case

Splitting Apart the Database

  • Need to find seams in databases to split them cleanly

Physical vs. Logical Database Separation

  • A single database engine can host multiple logically separated schemas
  • Logical decomposition enables independent change and information hiding
  • Physical decomposition improves system robustness and can remove resource contention

Pattern: Split Table

  • Sometimes data in a single table needs to be split across multiple service boundaries

Pattern: Move Foreign-Key Relationship to Code

Transactions

  • Sagas provide an alternative to distributed transactions
  • Unlike two-phase commit, a saga coordinates multiple state changes without locking resources for long periods
  • Models steps as discrete activities executed independently
  • Forces explicit modeling of business processes

Saga Characteristics

  • Does not provide ACID atomicity at saga level (only for subtransactions)
  • Provides enough information to reason about current state
  • Need to consider failure handling and recovery
Recovery approaches:
  1. Backward recovery: Reverting failures and cleaning up (rollback)
    • Requires defining compensating actions to undo previously committed transactions
  2. Forward recovery: Continuing from failure point
    • Requires ability to retry transactions
    • System must persist enough information to allow retry

Saga Implementation Patterns

  1. Orchestrated sagas:

    • Use central coordinator (orchestrator) to define execution order and trigger compensating actions
    • Command-and-control approach with good visibility into saga status
  2. Choreographed sagas:

    • Distribute responsibility among multiple collaborating services
    • “Trust-but-verify” architecture
    • Often use events for service collaboration
    • Events broadcast via message broker to interested services
    • Challenge: harder to understand what’s happening
    • Solution: Use correlation IDs to track saga state across events