🏰️

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
  • βœ”οΈ 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.