These are my notes on Monolith to Microservices by Sam Newman.
Practical and useful book on the subject.
Key Insights
- Microservices are independently deployable services modeled around a business domain.
- Don't focus on size but:
- How many microservices can you handle.
- How to define boundaries to get the most out of the microservices, without everything becoming a horrible coupled mess.
- Modular monolith still have the challenge of a monolith DB.
- DDD:
- Aggregate: self-contained unit that have a life cycle around them.
- Bounded context (BC) + Aggregate == unit of cohesion.
- The aggregate is a self-contained state machine that focuses on a single domain concept in our system, with the BC representing a collection of associated aggregates, with an explicit interface to the wider world.
- Microservices are not the goal.
- What are you going to achieve that you cannot with your current architecture?
- Have you considered alternatives to using microservices?
- How will you know if the transition is working?
- Reuse it not an outcome.
- When not to use microservices:
- Unclear domain.
- Startups.
- Customer installed and managed software.
- Dr John Kotter's eight-step process for implementing org change (from Leading Change):
- Establish a sense of urgency.
- Creating the Guiding Coalition.
- Developing a Vision and Strategy.
- Communicating the change vision.
- Empowering employees for broad-based action.
- Generating short-team wins.
- Consolidating gains and producing more change.
- Anchoring new approaches in the culture.
- Small changes, little steps.
- The biggest the bet and bigger the accompanying fanfare, the harder is to pull out when it is going wrong.
- Pattern: Change Data Capture (CDC):
- Use when there is no other option.
- Pattern: Database-as-a-Service interface.
- When you are relying on network analysis to determine who is using your database, you are in trouble.
- Split the DB first or the code?
- Code first.
- Static reference data:
- Duplicate, dedicated schema, static library.
- Data service: only when creating microservices is cheap.
- BPM tools: issue is that they are non-dev tools that end up being used by devs.
- The more coupling, the earlier the pains will manifest:
- 2-10 services:
- Breaking changes.
- Reporting.
- 10-50:
- Ownership at scale.
- Developer experience.
- Running too many things.
- 50+:
- Loads of teams.
- Global vs local optimization.
- Orphaned services.
- In all:
- Robustness and resilience.
- Monitoring and troubleshooting.
- End-to-end testing.
- Solutions:
- No cross team tests.
- Consumer-driven contracts.
- Use automated release remediation and progressive delivery in addition to end-to-end tests.
- Solutions:
- 2-10 services:
- Without strong code ownership (one and only one team can change service, other teams can do pull requests to propose changes) a microservices' architecture will grow into a distributed monolith.
TOC
- Chapter 1 - Just Enough Microservices
- Chapter 2 - Planning a Migration
- Chapter 3 - Splitting the Monolith
- Chapter 4 - Decomposing the database
- Chapter 5 - Growing Pains
Chapter 1 - Just Enough Microservices
- Microservices are independently deployable services modeled around a business domain.
- Type of SOA.
- Start with the technology that you know.
- Don't focus on size but:
- How many microservices can you handle.
- How to define boundaries to get the most out of the microservices, without everything becoming a horrible coupled mess.
- Modular monolith still have the challenge of a monolith DB.
- Other monoliths:
- Distributed monolith.
- Third-party black-box systems, both on premise and SaaS.
- Coupling types:
- Types.
- Implementation coupling.
- Temporal coupling (in the sense of sync calls).
- Deployment coupling (release trains).
- Domain coupling.
- DDD:
- Aggregate: self-contained unit that have a life cycle around them.
- Bounded context (BC):
- Hide implementation and internal details.
- BC + Aggregate == unit of cohesion.
- The Aggregate is a self-contained state machine that focuses on a single domain concept in our system, with the BC representing a collection of associated aggregates, again with an explicit interface to the wider world.
- Start by targeting services that encompass entire BC.
- You can split them further latter, hiding this decision.
Chapter 2 - Planning a Migration
- Microservices are not the goal.
- Key questions:
- What are you going to achieve that you cannot with your current architecture?
- Have you considered alternatives to using microservices?
- How will you know if the transition is working?
- Reuse it not an outcome.
- Limit the scope of expected outcomes.
- Possible whys:
- Improve team autonomy, alternatives:
- Modular monoliths.
- Assign responsibilities based on functional grounds.
- Self-servicing.
- Reduce time to market, alternatives:
- Value stream mapping to identify bottlenecks.
- Scale cost-effectively for load, alternatives:
- Vertical or horizontal scaling.
- Improve robustness:
- Being able to react to expected variations.
- Alternatives:
- Running multiple copies of your monolith.
- Use more reliable SW/HW.
- Automate manual processes.
- Scale number of developers, alternatives:
- Modular monolith (but less).
- Embrace new technology, alternatives:
- None.
- Improve team autonomy, alternatives:
- When not to use microservices:
- Unclear domain:
- Getting services wrong can be expensive due to large number of cross-service changes and overly coupled components.
- Startups.
- Customer installed and managed software:
- Due to increased operational complexity.
- Unclear domain:
- Dr John Kotter's eight-step process for implementing org change (from Leading Change):
- Establish a sense of urgency.
- Creating the Guiding Coalition:
- Try to bring somebody from "the business".
- Developing a Vision and Strategy:
- Vision: realistic yet aspirational.
- Commitment to vision is important, but overly commitment to strategy can be dangerous
- Communicating the change vision:
- Face to face + broadcast.
- Empowering employees for broad-based action:
- Bandwidth change: increase capacity or reduce load.
- Generating short-team wins.
- Consolidating gains and producing more change:
- Keep pushing on.
- Anchoring new approaches in the culture:
- Communicate successes and failures.
- Small changes, little steps.
- Whiteboard is where the cost of change and the cost of mistake is the lowest.
- Where to start?
- Identify BC and their relationships:
- Just enough DDD to get started.
- Maybe use Event Storming
- Relationship show how easy/difficult should be to extract that BC (warn: code may disagree).
- Plot BC in:
- Identify BC and their relationships:
- Is the transition working?
- Regular checkpoints. Agenda:
- Restate what you are trying to achieve. Does it still make sense?
- Review quantitative metrics.
- Ask for qualitative feedback: Happier in BVSSH.
- Decide if any change is needed.
- Regular checkpoints. Agenda:
- Avoid the skunk cost fallacy:
- The biggest the bet and bigger the accompanying fanfare, the harder is to pull out when it is going wrong.
Chapter 3 - Splitting the Monolith
- You have more options if you can change the monolith.
- Best if you can copy (not move) existing code.
- Consider refactoring into modular monolith first.
- Pattern: Stranger Fig Application:
- Steps:
- Identify what to migrate.
- Copy to microservice.
- Reroute calls to new microservice.
- Safe to rollback routing.
- There must be a clear way to redirect the calls to the new service.
- In case of HTTP, a reverse HTTP proxy in front of the monolith.
- Consider Ngnix + Lua if need something custom.
- If custom logic is very complex (like changing protocol from SOAP to gRPC), consider:
- Avoid putting the logic in the proxy, as it is a shared service between teams.
- Implement it in the microservice.
- Use service mesh.
- In message systems, the proxy consumes the monolith queue and does a content-based routing to two queues: new monolith queue and microservices one.
- While migrating, avoid any functionality change.
- Steps:
- Pattern: UI Composition:
- UI to call new microservice.
- Widget or page level.
- Mobile apps are monoliths, unless you can make changes without resubmitting them.
- Micro-frontend.
- Pattern: Branch by Abstraction:
- Steps:
- Create abstraction for the functionality to be replaced.
- Change clients to use new abstraction.
- Create new implementation that uses the new microservice.
- Switch over the new implementation.
- Cleanup.
- Verify branch by abstraction:
- If call to new implementation fails, call the old implementation.
- Steps:
- Pattern: Parallel Run:
- In strangler and branch by abstraction, call both implementation and compare results.
- Can check also performance.
- N-version programming:
- Implement functionality in several different ways, and do a parallel run, choosing the "correct" (quorum) one.
- For fault tolerance and to avoid bugs.
- Verification techniques:
- Use spy in microservices to record what will do, but without doing it.
- When duplicated side-effects are not ok.
- Github scientist.
- Use spy in microservices to record what will do, but without doing it.
- Not trivial, use just when there is a high risk.
- Pattern: Decorating Collaborator:
- When cannot or don't want to change monolith.
- Proxy works as a decorator that calls new microservice in addition to old monolith.
- Pattern: Change Data Capture (CDC):
- React to changes happened in the data store.
- Typical implementations:
- DB triggers:
- Use very sparingly.
- Transaction log poll.
- Batch delta copier:
- Process that on a regular schedule scans the DB to find what data has changed since last run.
- DB triggers:
- Use when there is no other option.
Chapter 4 - Decomposing the database
- Shared DB:
- It is ok with:
- Read-only static reference data that is stable and has a clear owner.
- Pattern: Database-as-a-Service interface.
- Pattern: Database view:
- Less coupling than shared DB.
- When you are relying on network analysis to determine who is using your database, you are in trouble.
- Useful only for read-only.
- Pattern: Database wrapping service:
- Move database dependencies to service dependencies.
- When is too hard pulling the schema apart.
- More flexible than DB view.
- Can take writes.
- Stepping stone, buys you time.
- Pattern: Database-as-a-Service interface:
- Create a specific and dedicated DB to be accessed externally.
- Mapping engine options:
- CDC.
- Batch process.
- Build from an event log.
- It is ok with:
- Transferring Ownership:
- Pattern: Aggregate exposing monolith:
- Monolith expose a proper aggregate API.
- When a newly extracted microservice still needs data owned by the monolith.
- Maybe a future microservice.
- Pattern: Change data ownership:
- When monolith still depends on newly extracted microservice data.
- Ideally monolith should call new service API:
- Copying the data back to the monolith DB as an alternative.
- Pattern: Aggregate exposing monolith:
- Data synchronization:
- Pattern: Synchronize data in application:
- Steps:
- Bulk synchronize data:
- If monolith was kept online, implement CDC to copy data since snapshot created.
- Synchronize on write, read from old schema.
- Synchronize on write, read from new schema.
- Decommission old schema.
- Bulk synchronize data:
- Steps:
- Pattern: Tracer write:
- Same as synchronize data in app, but one table at a time (instead of the whole bundled context) using the new microservice API.
- Pattern: Synchronize data in application:
- Split the DB first or the code?
- DB first:
- Easy, little short-term benefit.
- When concerned about performance or data consistency.
- Pattern: repository per BC:
- First step to understand dependencies.
- Pattern: database per BC:
- Bet for future split.
- Recommend for greenfield.
- Code first:
- Most common.
- Short-term improvements.
- Pattern: Monolith as data access layer:
- Same pattern as "aggregate exposing monolith".
- Pattern: Multischema storage:
- New microservice uses new schema for new functionality/data, old schema for existing data.
- DB and code at the same time:
- Avoid.
- DB first:
- Schema separation examples:
- Pattern: Split table:
- When table is owned by 2 or more BC.
- Lose referential integrity.
- Will need to chose the owner of the data.
- Pattern: Move foreign-key relationship to code:
- Increase latency: from 1 join query to 1 select + n service calls.
- Data consistency, deletion options:
- Check with all services before deleting:
- More coupling, reverse dependency.
- Don't use.
- Handle 404/410 gracefully in dependant service.
- Don't allow deletion:
- Soft delete/tombstone record.
- Check with all services before deleting:
- Pattern: Split table:
- Static reference data:
- Duplicate:
- As it changes infrequently maybe ok.
- For large volume of data.
- Background process to update it.
- Dedicated schema:
- No duplication, always up to date.
- Allows for cross-schema joins.
- Static library:
- For small datasets.
- Data service:
- When creating a new microservice is cheap.
- Maybe can emit change events.
- Duplicate:
- Avoid distributed transactions.
- Sagas:
- Model transactions as business processes.
- Specially for long lived transactions.
- Rollback with compensating transactions.
- Two implementations:
- Orchestrated:
- Central coordinator. Command and control.
- Pro: easier to understand.
- Cons: coupling.
- Warn: Anemic microservices: coordinator having too much logic that should be in microservices.
- Different services can play the coordinator role for different flows.
- BPM tools: issue is that they are non-dev tools that end up being used by devs.
- Camuda and Zeebe are targeted to microservices developers.
- Choreographed:
- Distributed responsibility. Trust but verify.
- Usually event based.
- Pro: decoupled.
- Cons: harder to understand whole flow.
- Use correlation Id to build/know the state of the saga:
- Service to read all events to show view.
- Orchestrated:
Chapter 5 - Growing Pains
- More detailed in "Building microservices" book.
- Based on anecdotal experience.
- The more coupling, the earlier the pains will manifest:
- 2-10 services:
- Breaking changes.
- Reporting.
- 10-50:
- Ownership at scale.
- Developer experience.
- Running too many things.
- 50+:
- Loads of teams.
- Global vs local optimization.
- Orphaned services.
- In all:
- Robustness and resilience.
- Monitoring and troubleshooting.
- End-to-end testing.
- 2-10 services:
- Ownership at scale:
- Without strong code ownership (one and only one team can change service, other teams can do pull requests to propose changes) a microservices' architecture will grow into a distributed monolith.
- Breaking changes:
- Avoiding accidental breaking changes:
- Explicit schema to avoid structural breakages.
- Protolock to prohibit incompatible changes.
- To avoid semantic breakages: testing.
- Make it hard to change a service contract:
- Make it obvious, no magic, or generate schemas from code.
- Make it hard to change a service contract:
- Explicit schema to avoid structural breakages.
- Non-accidental:
- Do not break, but accrete.
- Give consumers time to migrate:
- Run two versions of the service.
- Support old and new endpoints in the service.
- Be more relaxed if changes are within a team.
- Avoiding accidental breaking changes:
- Reporting:
- Build a reporting specific schema.
- Monitoring and Troubleshooting:
- Log aggregation:
- First thing to do when implementing microservices.
- Tracing:
- API GW or service mesh to generate the correlation ID.
- Test in production:
- Synthetic transactions.
- Towards observability:
- Log aggregation:
- Local developer experience:
- How many services to run locally?
- Solutions:
- Stubs.
- Point to instance running elsewhere.
- One remote env per developer:
- Slow to deploy to test changes.
- Cost.
- Mix local/remote:
- Telepresence for Kubernetes.
- Azure's cloud functions.
- Running too many things:
- Deployment, configuration and management of instances becomes more difficult.
- Solution:
- Kubernetes.
- Function-as-a-Service (preferred).
- End-to-end testing:
- Even slower and even more brittle.
- Solutions:
- No cross team tests.
- Consumer-driven contracts:
- Pact.
- Use automated release remediation and progressive delivery in addition to end-to-end tests.
- Global vs local optimization:
- Solving the same problem twice.
- Divergent tech stack.
- Solutions:
- Cross-cutting group to raise awareness/make tech decisions.
- Robustness and resilience:
- See Release it!.
- Orphaned Services:
- In-house service registries that combines service discovery + code repository data.
Top comments (0)