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
- What are you hoping to achieve? This should be aligned to what the business is trying to achieve, articulated as benefit to end users.
- Have you considered alternatives to using microservices? There are often many other ways to achieve the same benefits.
- 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:
- Identify parts of the existing system to migrate
- Implement functionality in new microservice
- 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:
- For read-only static reference data
- 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:
- Bulk Synchronize Data
- Synchronize on Write, Read from Old Schema
- 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:
- Write to one source: All writes go to one source of truth, then synchronize to the other
- 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:
- Backward recovery: Reverting failures and cleaning up (rollback)
- Requires defining compensating actions to undo previously committed transactions
- Forward recovery: Continuing from failure point
- Requires ability to retry transactions
- System must persist enough information to allow retry
Saga Implementation Patterns
-
Orchestrated sagas:
- Use central coordinator (orchestrator) to define execution order and trigger compensating actions
- Command-and-control approach with good visibility into saga status
-
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