Monorepos, Design Systems, and Scalable Apps: A Love Story (with Flutter Workspaces)

Siddhesh Shivdikar
Design Systems Collective
5 min readJan 29, 2025

Hey there! If you’re like me, you’ve probably found yourself drowning in a sea of duplicated code while working on multiple Flutter apps. One day, you’re copy-pasting a button into `App1`, and the next, you’re doing the same thing for `App2`. It’s like trying to keep track of all your browser tabs — chaotic and exhausting. But guess what? There’s a better way. Enter monorepos and Dart 3.6’s workspaces. Trust me, they’re game-changers.

I’ll walk you through how I use monorepos and workspaces to build scalable apps, share a design system, and keeping my sanity intact. A lil humor cause coding without laughter is like pizza without cheese — just sad.

What’s a Monorepo, and Why Should I Care?

A monorepo is basically a single repository that houses multiple projects or packages. Think of it as a shared workspace for your code. Instead of having separate repos for your design system, app, and utilities, you keep everything under one roof. Here’s why I love it:

1. Code Reusability: I can share my design system, utilities, and other goodies across apps.

2. Consistency: No more wondering if `App1` is using the same button style as `App2`.

3. Simplified CI/CD: One pipeline to rule them all.

4. Easier Collaboration: My team can work on multiple projects without juggling repos.

But wait, there’s more! With Flutter 3.6, I can use workspaces to make my monorepo even more efficient. Let me show you how.

The Problem with Traditional Monorepos

Before Flutter 3.6, managing a monorepo felt like herding cats. I had to:

- Run `dart pub get` for every package.

- Deal with different dependency versions.

- Suffer from memory-hogging IDE analysis contexts.

It was a mess. But with workspaces, Flutter has given us a shiny new tool to tame the chaos.

How do I Set Up a Monorepo with Flutter Workspaces

Let’s say I’m building two apps (`App1` and `App2`) and a shared design system. Here’s how I set up my monorepo:

Step 1: Create the Monorepo Structure

I start by organising my projects like this:

my_monorepo/
├── packages/
│ ├── design_system/ // My shared design system
│ ├── app_1/ // My first app
│ ├── app_2/ // My second app
├── pubspec.yaml // Root pubspec for the workspace

Step 2: Add the Root `pubspec.yaml`

At the root of my monorepo, I create a `pubspec.yaml` file with a `workspace` entry:

name: _
publish_to: none
environment:
sdk: ^3.6.0
workspace:
- packages/design_system
- packages/app_1
- packages/app_2

This tells Flutter, “Hey, these are my packages. Treat them as one big happy family.”

Step 3: Update My Package `pubspec.yaml` Files

For each package in my workspace, I update the `pubspec.yaml` to include:

yaml
environment:
sdk: ^3.6.0
resolution: workspace

This ensures all packages use the same dependency resolution.

Step 4: Run `dart pub get`

I run `dart pub get` from the root of my monorepo. Flutter will:

1. Create a single `pubspec.lock` file at the root.

2. Generate a shared `.dart_tool/package_config.json`.

3. Delete any stray `pubspec.lock` and `.dart_tool/package_config.json` files in my packages.

Now my file structure looks like this:

my_monorepo/
├── packages/
│ ├── design_system/
│ │ └── pubspec.yaml
│ ├── app_1/
│ │ └── pubspec.yaml
│ ├── app_2/
│ │ └── pubspec.yaml
├── pubspec.yaml
├── pubspec.lock
└── .dart_tool/package_config.json

The Magic of Workspaces

With workspaces, I get:

1. Single Dependency Resolution: No more version conflicts between packages.

2. Shared Analysis Context: My IDE won’t eat up all my RAM.

3. Simplified Commands: I run `dart pub get` once, and I’m done.

It’s like having a personal assistant for my codebase.

Building a Scalable Design System

Now that my monorepo is set up, let’s talk about the **design system**. This is where I define my app’s look and feel — colors, typography, buttons, and more. Here’s how I make it shine:

1. Define Design Tokens

I create a `tokens.dart` file in my `design_system` package:

class DesignTokens {
static const Color primaryColor = Color(0xFF6200EE);
static const TextStyle heading1 = TextStyle(fontSize: 32, fontWeight: FontWeight.bold);
}

2. Create Reusable Components

I build components like buttons and cards in my `design_system` package (I’m just using simple examples here design systems are much more complex):

class YourButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;
const PrimaryButton({required this.text, required this.onPressed});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
child: Text(text),
);
}
}

3. Use the Design System in My Apps

In `app_1` and `app_2`, I import the design system and use its components:

import 'package:design_system/design_system.dart';
class HomeScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('App 1')),
body: Center(
child: PrimaryButton(
text: 'Click Me',
onPressed: () {},
),
),
);
}
}

Tips for a Happy Monorepo

Here are some tips I’ve learned along the way:

1. Keep It Organized: I use clear folder structures and naming conventions.

2. Document Everything: I write a `README.md` for each package. (For an Organisation based project please use Confluence or a proper doco / At the time of writing we have @Previews for flutter components but you can still use story books if team capacity permits)

3. Test Thoroughly: I write unit and widget tests for my design system and apps.

4. Use CI/CD: I set up a pipeline to test and deploy my apps automatically.

When Things Go Wrong

Dependency Conflicts: Workspaces force me to resolve conflicts early. It’s annoying but worth it.

Stray Files: If `pub get` complains about stray `pubspec.lock` files, I delete them manually.

IDE Issues: If my IDE acts up, I restart it or run `flutter clean`.

Conclusion
Monorepos and workspaces are like peanut butter and jelly — they just work together. By organizing my code into a monorepo and using Flutter’s workspace feature, I can build scalable apps, share a design system, and keep my codebase clean and maintainable.

So go forth, fellow developer, and conquer the world of scalable apps. And remember: if your codebase feels like a jungle, just grab a machete (or a monorepo) and start hacking.

Got questions? Drop them in the comments below. And if you found this tutorial helpful, share it with your fellow developers. After all, sharing is caring (unless it’s your Pizza Slice — then it’s every person for themselves).

Happy coding! 🚀

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Published in Design Systems Collective

A welcoming community for designers and developers passionate about scalable, consistent design. Explore articles, insights, and resources to build and refine your design systems. Join us to connect, learn, and shape the future of systematic design together.

Written by Siddhesh Shivdikar

Hello! I'm Siddhesh , a software engineer based in Mumbai, India. I'm passionate about field of AI and Data Science

Responses (1)

Write a response