machsoftware-developmentdevopstechnology

End to end monorepos for MACH

8 min read

Using monorepos for MACH projects

Supercharge your MACH developer experience using monorepos

--

While it might feel counter-intuitive in MACH architectures, monorepos are a great way to simplify and streamline your MACH development process.

Using a monorepo to host your complete MACH platform can greatly improve your teams productivity and collaboration. Ultimately this results in faster time to value and better control over a MACH project.

Monorepos?! Never heard of it. We can imagine. We are talking about how to implement source code management and version control for MACH projects.

We’re in good company with this approach! Big companies with many (micro)services and moving parts have been using monorepos for a long time. A big example being Google, who in 2021 reported that 10.000 engineers commit a whopping 40.000 commits per day to their mono repo. A number that has only grown since, we can imagine. So scale shouldn’t be a problem.

As with any approach, tool or technique, this is no silver bullet or ‘quick fix’! You still need to carefully structure you repo and be disciplined about this in the long term, to ensure that you maintain composability and prevent ending up with an entangled ‘MACH monolith’.

Managing microservices using monolithic techniques

When pursuing a composable architecture, by its nature composability is an important design goal. And with that, there is a continuous debate about Monoliths vs. Microservices architecture.

Implementing microservices is complicated and often underestimated, especially for teams new to it. And as a result projects may spin out of control within no time.

For more complex and scalable use-cases, they definitely have their purpose though. They provide a level of composability that is harder to achieve with monoliths, and the same can be said about scaling the organisation that builds the system.

A key aspect of building a successful microservices architecture is to provide a great and simple developer experience that is easy to grasp and fast to get started with. With a microservices architecture this gets more complicated with each service or moving part that the architecture grows with.

This in contrast with a monolithic architecture; it is often a lot easier to get started on a monolithic project, usually by initialising a single project and executing a single command to run the project. There is simply only a single way to do it!

What if we could get a monolith-like experience with a MACH Microservices project?

A common microservices approach

As you might know, a MACH architecture consists of many moving parts and microservices. There is usually a front-end React/NextJS application, a GraphQL federation Gateway, and underneath that several back-end microservices, each exposing a GraphQL subgraph. Additionally, these microservices interact with all kinds of Cloud and SAAS services, such as AWS and commercetools.

Historically, we would create an individual git repository for each of those components, with each of them having their own CI/CD pipeline. Getting started/productive on a project as a developer was therefore a real pain.

Even worse, there was a disconnect between developers that worked on other parts of the application, relying on hosted environments to serve as a backend or frontend to their local changes.

This soon becomes a spiderweb of users and repo’s, that all need versioning, the creation of artifacts and deployment to the right target environments.

After a number of MACH projects approached this way, we thought this could be simplified substantially and started using monorepos instead.

Enter the mono repository

A mono repository, or monorepo, is what the name implies: one git repository that contains the entire project in a nested structure. So instead of creating many individual repositories, we simply create subdirectories in a single repository, to host our services in.

This means we’re fitting all of the components of the entire MACH architecture, into a single repository. And if we say ‘entire’, we mean it: front-end, back-end, infrastructure, configuration, documentation, deployment pipelines for all of it. Everything is hosted in this single Git repository.

Why, you may ask. This may lead to a mess quite soon right? Of course it can — but no different than a multi-repo setup with many individual microservices. With a mono repo you at least have all of the mess in one place ;-).

But we’re not alone. These days there are ample tools to manage mono repositories. Examples that we are fans of, are tools such as pnpm and turborepo.

Using these tools we’re able to significantly improve the developer experience, such as having a single command, pnpm dev, to start up the entire MACH architecture at once, consisting of a large number of services. This greatly reduces the time needed to get started in a new project, improves code sharing between packages and services, and lets you grasp a project much faster.

Pros and cons of a monorepo

There are many benefits of using a monorepo for a microservices architecture:

  • Ability to share testing and deployment pipelines between services.

  • Straightforward to spin up ‘branch deployments’ for individual Pull Requests, that run the entire application.

  • Faster to get started with a project for new developers (time to code) using a single, top level command to install and run the project.

  • Visibility of everything that happens, to the entire team. Whether you are working on the front-end or back-end.

  • Bonus: you can easily share validation (for example with zod) between front-end and back-end.

  • Easy to do larger refactorings that span microservices.

  • Easier creation and usage of shared libraries.

  • Easier to set ‘sane defaults’ at the project level, like code style standards, security checks, static analysis.

  • Combined with Github, the use of CODEOWNERS greatly facilitates your workflows.

So only benefits? No there are also a couple of drawbacks we found. But these do have to do more with the size of a project, rather than the monorepo itself. With a multi-repo setup, problems would be worse.

  • Versioning & tagging individual services becomes harder, because everything is on the same commit hash and tag.

  • You need a big machine to run the monorepo locally — though no different then running all microservices. But in the Apple Silicon-age this is not really a problem anymore.

  • In larger teams with many microservices, a lot of information flows and it is hard to keep a good overview. But then again; this is no different with separate repositories; that is even harder to follow.

  • You need tooling and checks to handle the added complexity of a mono repository, ranging from dependency management towards code governance. But then again, you also need these when not using a mono repo.

What does a monorepo look like for MACH?

Just like when creating a structure using individual repositories for individual components, a good structure is critical when using a monorepo. At Lab Digital, a MACH monorepo typically looks like below, with clear places for certain pieces of the architecture to be placed.

├── .github├── backend│ ├── docker│ ├── examples│ ├── packages│ │ ├── commercetools-client│ │ ├── commercetools-testing│ │ ├── eslint-custom-config│ │ ├── graphql-schema-generate│ │ ├── graphql-utils│ │ ├── lib│ │ └── observability│ └── services│ ├── commercetools-checkout│ │ ├── src│ │ └── terraform│ ├── commercetools-account│ │ ├── src│ │ └── terraform│ ├── graphql-gateway│ │ ├── src│ │ └── terraform│ ├── page-resolver│ │ ├── src│ │ └── terraform├── config #mach-composer configuration├── docs├── frontend│ ├── packages│ │ ├── lib│ │ ├── types│ │ └── ui│ └── sites│ └── example-commerce│ ├── e2e│ ├── public│ ├── src│ └── terraform├── infrastructure└── packages

As you can see, this includes an entire distributed application; from frontend to backend, to infrastructure and everything in between-including MACH composer configuration in the config folder. Furthermore, a repo like this supports ‘multi instance’ deployments, with a front-end architecture that scales to any number of sites, using shared (component) libraries and MACH composer that scales the back-end.

There a certain over-arching directories that contain platform-wide components. And there are components that are simply individual, standalone microservices, that implement a particular piece of functionality. As you can see, these services also come with their own terraform directory, which implies that they are responsible for declaring their own infrastructure requirements. This allows to easily add or remove services from the project.

Conclusion

Introducing monorepos to MACH projects has brought us major productivity improvements, not to mention more enjoyment, across a number of projects. So much that we’ve migrated legacy projects to a monorepo structure as well.

There are no big cons to using monorepos, and if you must, you can of course always split off individual modules from the monorepo. But so far we haven’t seen that happening in about two years of following this approach.

So if you ask us, monorepos and MACH should be considered a best practise.

Keep it simple, stupid!

Tags used in this article:
machsoftware-developmentdevopstechnology