System Design
Align system and component boundaries with product, team and technology structures
Architecture Model
The foundation for planning and growing software systems is a shared understanding of its building blocks. Unfortunately, there is a lot of confusion about terminology and boundaries of the building blocks. To define a consistent architecture model we are going to use a simplified version of the C4 model that defines three fundamental building blocks of different size and nature:
- π Software Systems
- Interconnected technical components with a shared purpose that provide user value
- Evolve autonomously
- Often owned by a single team
- e.g. eCommerce system, payment system, content management system, social media platform
- C4 Model Level 1
- 𧩠Components
- Shaped by languages, frameworks and cloud services
- e.g. web app, backend service, cloud resource
- Have dependencies to other components within or across systems
- Part of a packaging ecosystem (e.g. NPM package, Docker Image, Terraform Module)
- Can have a build and deployment process
- Evolve together
- C4 Model Level 2-3
- π© Code Elements
- e.g. functions, classes, source files
- C4 Model: Level 4
System Boundaries
Software systems are abstract, high-level units that consist of interconnected technical components that, together, serve a certain purpose and deliver value to users. Shaping system boundaries is tricky because it requires the alignment of multiple perspectives which do not necessarily line up. System boundaries are most likely a tradeoff that balances out the following aspects:
- Team Ownership and Autonomy
- Technical Boundaries
- Product Boundaries and User Personas
Repository Boundaries
Components within a system are interconnected and change together. Therefore, it makes sense to store their code and version control history together. This can be achieved by using one monorepo per system. Such a system scoped monorepo would typically have a folder structure that aligns with the component structure:
π components/
π my-app/
π my-library/
π my-service/
π my-cloud-resource/
...
π README.md
System scoped monorepos align system and repository boundaries which makes the repository a place to store various system-level assets:
- π Environments
- Systems can be deployed and operated in multiple environments (e.g. staging, production)
- Systems need to be aware of environment differences, components should not
- π Component Integration State
- The Git history stores component versions that can be deployed together
- π Deployment Pipeline Coordination
- Components that depend on each other need to be deployed in the correct order
- e.g. deploy backend before frontend
- π§ Development Tooling
- Developers need to debug/test all system components together
- e.g. debug modified frontend and modified backend together
- π Shared Configuration
- e.g. environment variables
- π¦ Shared/Peer Dependencies
- Components may need to share dependencies of a specific version
- π Architecture Documentation
- Architecture Documentation can be stored as code
- βοΈ Workflows
Component Boundaries
Components are shaped by languages, frameworks and cloud infrastructure. They can be of different types such as:
- 𧩠Web App
- 𧩠Desktop App
- 𧩠Mobile App
- 𧩠CLI Tool
- 𧩠Library
- 𧩠API Service
- 𧩠Database
- 𧩠Identity Provider
- 𧩠File Storage
- 𧩠Content Delivery Network
- 𧩠Message Queue
- 𧩠Search Service
- 𧩠Data Processing Service
- 𧩠Monitoring Service
- 𧩠Alerting Service
- 𧩠Streaming Service
- 𧩠Payment Service
- 𧩠Prediction Service
- 𧩠Machine Learning Model
- 𧩠CI/CD Platform
Components are interconnected within and across system boundaries in various ways:
- Code Dependency: One component is compiled into another as a code dependency (e.g. NPM package, Docker parent image)
- Client/Server Communication: One component accesses another via an API at runtime (e.g. a web app accesses an API service)
- Provisioning: One component provisions the runtime environment (e.g. via Infrastructure-as-Code) that another component is deployed to
- Deployment: One component deploys another component (e.g. a CI/CD Platform deploys a web app)
The Monolith-to-Microservice Trajectory
When it comes to shaping boundaries between and within software systems there is an ongoing discussion about the "best" paradigm. The popular solutions are "microservices" and the "modular monolith". However, this is more complex than just choosing the "right" paradigm and here are some mixed opinions to show it:
- π¬ Simon Brown
If you canβt build a well-structured monolith, what makes you think you can build a well-structured set of microservices?
- π¬ Martin Fowler: Monolith First
Almost all the successful microservice stories have started with a monolith that got too big and was broken up. Almost all the cases where I've heard of a system that was built as a microservice system from scratch, it has ended up in serious trouble.
- π¬ Dave Farley: Monolith vs Microservices
It is my impression based on the clients I work with and the people I talk to that the commonest way to start a new project these days is to guess at a breakdown of services and then crate a separate repo for each one. [...] This is a terrible idea.
- π¬ Stefan Tilkov: Don't start with a monolith
Starting to build a new system is exactly the time when you should be thinking about carving it up into pieces. I strongly disagree with the idea that you can postpone this.
- π¬ Sam Newman
I remain convinced that it is much easier to partition an existing, "brownfield" system than to do so up front with a new, greenfield system.
- π¬ Randy Shoup: Monolith Architecture Is Best For Start-Ups
I recommend in the 99% case: People that are just starting out with a project or just starting out with an entire company that you 100% start with a monolith. [...] Honestly, I think 90% of the software on the planet really should be done in a monolith.
It becomes apparent that the usefulness of each paradigm changes depending on the business size and situation. Although microservices are a very popular narrative, starting with a modular monolith often seems to be the better strategy. This raises not just the question of how to implement both paradigms but also how to transition from one paradigm to the other.
System scoped monorepos are a solution to that. As an architectural concept they work for both monoliths and microservices and can therefore be kept while gradually migrating from a monolith to microservices. The key idea is to consider each microservice and each monolith to be an independent system that is stored in its own monorepo:
- π Modular Monolith
- A monorepo with many components
- Multiple teams work on that monorepo
- Component ownership is split across teams
- π Microservice System
- A monorepo with only a few or a single component (e.g. backend service, database infrastructure)
- The monorepo is fully owned by a single team
With this in mind it becomes more obvious how to transition from a monolith to microservices: Once a system scoped monorepo becomes to complex or interconnected, a subset of components can be split of into its own monorepo. This essentially creates a new system that can then evolve on its own.