Modern enterprise platforms often contain multiple applications that serve different business capabilities.
At first, maintaining these applications as completely independent projects may seem straightforward. Over time, however, teams often discover that the applications share a significant amount of functionality: authentication, UI components, API clients, validation, utilities, configuration, types, and common business behaviour.
One approach to solving this problem is a frontend monorepo.
In one of the enterprise applications I worked on, multiple React applications supporting different business capabilities were maintained within a single Git repository. These included applications such as:
Agreements
Nominations
Cargo Operations
Deal Check
Administration
Feature Management
Although these applications belonged to the same overall platform, they remained separate applications with their own functional responsibilities.
The monorepo approach allowed us to maintain these applications together while sharing common frontend capabilities.
This article explains the architecture, the reasoning behind the approach, its benefits and challenges, and some of the lessons learned from implementing a multi-application React monorepo.
1. What Is a Monorepo?
A monorepo, or monolithic repository, is a source-control strategy where multiple applications and libraries are maintained in a single repository.
Without a monorepo, we could have:
agreement-ui
nominations-ui
cargo-ops-ui
deal-check-ui
admin-ui
shared-uiEach application could have its own Git repository.
With a monorepo, these projects are brought together:
enterprise-frontend/
│
├── apps/
│ ├── agreements/
│ ├── nominations/
│ ├── cargo-operations/
│ ├── deal-check/
│ ├── administration/
│ └── feature-management/
│
├── packages/
│ ├── ui-components/
│ ├── authentication/
│ ├── api-client/
│ ├── shared-types/
│ ├── utilities/
│ └── configuration/
│
├── package.json
├── configuration/
└── README.mdThe important point is that one repository does not mean one application.
A monorepo can contain multiple independently deployable applications.
That distinction is particularly important in enterprise architecture.
2. Monorepo Does Not Mean Monolith
The terms are sometimes confused.
A monolithic application might look like:
One Application
|
+--------------+------------------+
| | |
Agreements Nominations Cargo Ops
| | |
+--------------+------------------+
|
DatabaseEverything is part of the same application and generally shares the same deployment lifecycle.
A frontend monorepo is different:
Git Monorepo
|
+--------------+------------------+
| | |
▼ ▼ ▼
Agreements Nominations Cargo Ops
App App App
| | |
▼ ▼ ▼
Deployable Deployable Deployable
Application Application ApplicationThe repository is shared, but the applications can remain logically and operationally separate.
This gives us an important architectural principle:
Repository boundaries and application boundaries do not have to be the same thing.
3. Why Use a Monorepo for Multiple React Applications?
Consider an enterprise platform with several React applications.
Each application may require:
Authentication
Authorization
Header
Navigation
Buttons
Dialogs
Data Grid
Notifications
API communication
Validation
Logging
Common utilitiesWith separate repositories, these capabilities can become duplicated.
For example:
Agreement Repository
├── Login
├── Button
├── DataGrid
└── API Client
Nominations Repository
├── Login
├── Button
├── DataGrid
└── API Client
Cargo Repository
├── Login
├── Button
├── DataGrid
└── API ClientThis creates several problems.
A bug fixed in one implementation may need to be fixed in several others.
A UI change may have to be replicated across repositories.
Different teams may use different versions of the same library.
Over time, the applications can start behaving differently.
A monorepo provides another option:
Shared Libraries
|
+-----------------+-----------------+
| | |
▼ ▼ ▼
Agreements Nominations Cargo OpsCommon functionality can be implemented once and consumed by multiple applications.
4. High-Level Architecture
A simplified version of the frontend architecture can be represented as follows:
React Monorepo
|
+--------------------------+-------------------------+
| | |
▼ ▼ ▼
+----------------+ +----------------+ +-----------------+
| Agreements | | Nominations | | Cargo Ops |
| React App | | React App | | React App |
+------+---------+ +---------+-------+ +---------+-------+
| | |
+--------------------------+--------------------------+
|
Shared Frontend Libraries
|
+----------------------+----------------------+
| | |
▼ ▼ ▼
UI Components Authentication API Client
| | |
+----------------------+----------------------+
|
▼
API Gateway
|
+-----------------+-----------------+
| | |
▼ ▼ ▼
Agreement MS Nomination MS Cargo Ops MSThis architecture gives us two important characteristics:
Shared development model
Applications and common libraries live together.
Independent application boundaries
Each application still represents a specific business capability.
5. Applications and Shared Libraries
One of the most important design decisions in a monorepo is separating applications from reusable libraries.
A conceptual structure could be:
apps/
agreements/
nominations/
cargo-operations/
deal-check/
packages/
ui/
authentication/
api-client/
models/
utilities/The apps directory contains executable applications.
The packages directory contains reusable libraries.
For example:
apps/agreements
|
+----> packages/ui
|
+----> packages/authentication
|
+----> packages/api-client
|
+----> packages/utilitiesThe Nominations application can use the same libraries:
apps/nominations
|
+----> packages/ui
|
+----> packages/authentication
|
+----> packages/api-client
|
+----> packages/utilitiesThis is one of the major benefits of the monorepo approach.
6. Shared UI Components
Enterprise applications generally contain many common UI components.
For example:
packages/ui/
Button
Modal
Dialog
DataGrid
DatePicker
Dropdown
Notification
LoadingIndicator
FormControlsInstead of every application implementing its own version of these components, they can consume a common library.
For example:
import { Button } from '@platform/ui';
<Button
variant="primary"
onClick={handleSave}>
Save
</Button>The Agreements application and Cargo Operations application can use the same component.
This provides consistency in:
Appearance
Behaviour
Accessibility
Validation
Error handling
User interaction
A change to a shared component can then be propagated to the applications that consume it.
7. Avoiding the "Shared Everything" Problem
There is an important warning here.
Just because code can be shared does not mean it should be shared.
For example, imagine:
packages/shared/
agreementLogic
nominationLogic
cargoLogic
dealLogic
adminLogic
...Eventually, the shared package can become a dumping ground for unrelated functionality.
Instead, shared code should have a clear purpose.
A useful rule is:
Share technical capabilities aggressively, but share business logic deliberately.
Good candidates for shared libraries:
UI components
Authentication
HTTP client
Logging
Date utilities
Common validation
Configuration
Type definitionsBusiness-specific logic should generally remain within the relevant application unless there is a genuine common domain requirement.
8. Shared Authentication
Authentication is another strong candidate for reuse.
Suppose every application needs to understand:
Login
Token handling
Token refresh
Logout
Session management
Authorization
Route protectionImplementing these separately creates unnecessary duplication.
Instead:
packages/authenticationcan provide common capabilities.
Conceptually:
Authentication Library
|
+------------------+-------------------+
| | |
▼ ▼ ▼
Agreements Nominations Cargo OpsEach application consumes the same authentication behaviour.
This also provides a central place to address changes to authentication mechanisms.
For example, if the identity provider changes, the implementation can potentially be updated in one shared location rather than separately across every application.
9. Authorization and Route Protection
Authentication answers:
Who is the user?
Authorization answers:
What is the user allowed to do?
Different business applications may expose different permissions.
For example:
User
|
+-- Agreements
| +-- View
| +-- Edit
| +-- Approve
|
+-- Nominations
| +-- View
| +-- Create
|
+-- Cargo Operations
+-- View
+-- ManageA shared authorization mechanism can provide common permission handling while individual applications determine which permissions are relevant to their functionality.
A conceptual React route might look like:
<ProtectedRoute
permission="Agreement.Edit">
<AgreementEditPage />
</ProtectedRoute>The actual implementation can vary depending on the authentication and authorization mechanism used.
10. Shared API Client
Another important shared capability is communication with backend APIs.
Without a common API client, every application might independently implement:
HTTP requests
Headers
Authentication tokens
Error handling
Timeouts
Retries
Correlation IDs
LoggingA shared client can centralize these concerns.
Shared API Client
|
+--------------------+--------------------+
| | |
▼ ▼ ▼
Agreements API Nominations API Cargo APIFor example:
const response =
await apiClient.get<AgreementContract>(
`/agreements/${agreementId}`
);The client can handle cross-cutting concerns without forcing every page to implement them repeatedly.
11. Contract-Driven Frontend Integration
This monorepo architecture becomes even more interesting when combined with the contract-driven API approach discussed in the first article.
The complete flow becomes:
React Application
|
v
Shared API Client
|
v
API Gateway
|
v
.NET Microservice
|
v
Contract / DTO
|
v
React ApplicationFor example:
Agreement Application
|
| GET
v
API Gateway
|
v
Agreement Service
|
v
AgreementContract
|
v
React StateThe user modifies the data.
During save:
React State
|
v
AgreementContract
|
v
Shared API Client
|
v
API Gateway
|
v
Agreement Service
|
v
EF Core
|
v
SQL ServerThis creates a clear frontend/backend boundary.
12. Shared TypeScript Contracts
If the backend exposes well-defined contracts, frontend TypeScript models can represent them.
For example:
export interface AgreementContract {
agreementId: number;
agreementNumber: string;
status: string;
effectiveDate: string;
expiryDate?: string;
rowVersion: string;
}The model can be consumed by the Agreements application:
import {
AgreementContract
} from '@platform/models';The benefit is consistency.
Instead of defining the same contract differently in several applications:
Agreement App
AgreementContract
Admin App
AgreementContract
Reporting App
AgreementContracta common representation can be maintained where appropriate.
However, shared contracts should be managed carefully.
If an application has different requirements from another application, forcing both to consume one giant model can create unnecessary coupling.
13. Dependency Graph
One of the most useful concepts in a monorepo is the dependency graph.
Consider:
UI Library
|
+--------------+--------------+
| | |
▼ ▼ ▼
Agreements Nominations Cargo Ops
| | |
+--------------+--------------+
|
API Client
|
AuthenticationThe applications depend on common libraries.
If the UI library changes, the monorepo tooling can determine which applications are affected.
Conceptually:
Change:
packages/ui/Button
|
v
Affected Projects:
Agreements
Nominations
Cargo Operations
Deal CheckThis becomes particularly valuable as the repository grows.
14. Independent Application Development
Although applications share a repository, developers can focus on their respective business areas.
For example:
Team A
Agreements
Team B
Nominations
Team C
Cargo Operations
Team D
Deal Check
Platform Team
Shared UI
Authentication
API ClientThe repository provides a common development environment while application ownership remains clear.
This is one reason why defining ownership is important in a large monorepo.
15. Independent Builds
A common misconception is that a monorepo means the entire repository must be built whenever anything changes.
That doesn't have to be the case.
A mature monorepo setup can identify affected applications.
For example:
Developer changes:
apps/agreements/The CI/CD system can potentially execute:
Build Agreements
|
v
Test Agreements
|
v
Deploy Agreementsrather than:
Build Everything
|
v
Test Everything
|
v
Deploy EverythingThis becomes increasingly important as the number of applications grows.
The exact implementation depends on the monorepo tooling and CI/CD pipeline.
16. CI/CD Pipeline for a Monorepo
The CI/CD pipeline needs to understand that the repository contains multiple applications.
A conceptual pipeline is:
Git Commit
|
v
Azure DevOps
|
v
Change Detection
|
+---------------+---------------+
| | |
v v v
Agreement Cargo Ops Nominations
| | |
v v v
Build Build Build
| | |
v v v
Test Test Test
| | |
v v v
Package Package Package
| | |
v v v
Deployment Deployment DeploymentThis provides an important advantage:
A single repository can still support independent application delivery.
17. What Happens When a Shared Library Changes?
This is one of the more interesting scenarios in a monorepo.
Suppose:
packages/ui/DataGridis changed.
The dependency graph might identify:
DataGrid
|
+--> Agreements
|
+--> Nominations
|
+--> Cargo OperationsThose applications may need to be rebuilt and tested.
On the other hand, if a change is completely isolated to:
apps/agreements/then unrelated applications don't necessarily need to be rebuilt.
This is where good dependency management and affected-project detection become important.
18. Git Workflow
A monorepo also changes how teams think about Git.
Instead of:
Agreement Repository
Nominations Repository
Cargo Repositorydevelopers work in:
Enterprise Frontend RepositoryA feature branch might contain:
feature/agreement-validationwith changes such as:
apps/agreements/...
packages/ui/...
packages/models/...A single pull request can therefore contain coordinated changes across an application and its shared dependencies.
For example:
PR
Agreement Page
+
Shared DatePicker
+
Agreement ContractThis can be much easier than coordinating changes across multiple repositories.
19. Cross-Application Refactoring
This is one of the strongest arguments for a monorepo.
Suppose an organization decides to change the standard API error-handling mechanism.
With separate repositories:
Agreement Repo
↓
PR
Nominations Repo
↓
PR
Cargo Repo
↓
PR
Deal Check Repo
↓
PRWith a monorepo:
Shared API Client
↓
One coordinated change
↓
Affected applicationsThe complete change can be reviewed in one place.
This is particularly useful for enterprise-wide technical changes.
20. Code Consistency
A monorepo also provides an opportunity to standardize:
ESLint
Prettier
TypeScript
Testing
Build configuration
Dependency versions
Coding standards
Git hooksFor example, applications can share common configuration:
configuration/
eslint
typescript
prettier
testingThis reduces the possibility of different applications gradually developing different technical standards.
21. Dependency Management
Dependency management becomes particularly important in a monorepo.
Suppose one application uses:
React version Xwhile another uses:
React version Yand the shared UI library expects:
React version ZThis can create compatibility problems.
A monorepo provides centralized visibility into dependencies.
Teams can establish policies around:
Supported versions
Dependency updates
Security vulnerabilities
Common packages
Deprecated libraries
The goal is not necessarily to force every package to have exactly the same dependency versions, but to make dependency relationships visible and manageable.
22. Testing Strategy
Testing can also be organized around applications and shared libraries.
For example:
packages/ui
|
+-- Unit Tests
packages/api-client
|
+-- Unit Tests
apps/agreements
|
+-- Unit Tests
+-- Component Tests
+-- Integration Tests
apps/cargo-operations
|
+-- Unit Tests
+-- Component Tests
+-- Integration TestsThe pipeline can then execute the appropriate tests based on what has changed.
Shared libraries deserve particularly strong test coverage because one defect can potentially affect multiple applications.
23. Monorepo and Microservices Can Coexist
Another important architectural observation from this type of system is that frontend repository organization and backend service architecture are independent decisions.
For example:
FRONTEND
Git Monorepo
|
+---------------+---------------+
| | |
▼ ▼ ▼
Agreements Nominations Cargo Ops
React React React
BACKEND
Microservices
|
+---------------+---------------+
| | |
▼ ▼ ▼
Agreement Nomination Cargo Ops
Service Service ServiceThe frontend may use a monorepo while the backend uses independently deployed microservices.
There is no contradiction.
The monorepo is primarily a source-code organization and development strategy.
Microservices are primarily an application architecture and deployment strategy.
They address different problems.
24. Monorepo vs Polyrepo
It is useful to compare the two approaches.
Area | Monorepo | Polyrepo |
|---|---|---|
Source control | Single repository | Multiple repositories |
Code sharing | Straightforward | Requires packages or duplication |
Cross-application refactoring | Easier | More coordination required |
Dependency visibility | Centralized | Distributed |
CI/CD | More sophisticated | Usually simpler per repository |
Repository size | Larger | Smaller |
Team isolation | Lower | Higher |
Shared standards | Easier to enforce | Can diverge |
Cross-project changes | One PR possible | Multiple PRs often required |
Build optimization | Requires affected-project strategy | Naturally isolated |
Neither approach is universally better.
The right choice depends on organizational structure, application boundaries, release requirements, team autonomy, and the amount of shared functionality.
25. Challenges of a React Monorepo
A monorepo solves several problems, but it introduces its own challenges.
Repository Size
As applications and libraries increase, the repository can become large.
This requires good organization and tooling.
Build Performance
Building every application for every commit defeats one of the advantages of independent applications.
Affected-project builds become important.
Shared Library Coupling
A poorly designed shared library can create strong dependencies between applications.
For example:
Cargo
|
v
Shared Library
^
|
AgreementsA seemingly small change can unexpectedly affect multiple applications.
Ownership
Someone needs to own shared libraries.
Otherwise, every team may change common code without understanding its impact.
Dependency Management
Common dependencies must be managed carefully.
CI/CD Complexity
The pipeline needs to understand multiple applications, shared libraries and affected projects.
26. Avoiding a Distributed Monolith
A poorly designed monorepo can gradually become what I would call a distributed frontend monolith.
For example:
Agreement
|
+----> Shared Library A
|
+----> Shared Library B
|
+----> Cargo
|
+----> Shared Library CNow changing one application can potentially affect everything.
The purpose of a monorepo is not to remove boundaries.
It is to make collaboration and reuse easier while preserving appropriate boundaries.
Good architecture therefore requires:
Clear application ownership
Clear library ownership
Stable public interfaces
Dependency rules
Limited coupling
Well-defined shared capabilities
27. Recommended Dependency Direction
A useful dependency structure is:
Applications
|
v
Shared Technical Libraries
|
v
Infrastructure / UtilitiesFor example:
Agreements
|
+--> UI
+--> Authentication
+--> API Client
+--> UtilitiesBut ideally:
Shared UI
X
|
X----> AgreementsThe shared library should not depend on a specific business application.
Otherwise, the supposedly shared library becomes coupled to that application.
28. The Role of API Contracts
The frontend monorepo becomes particularly powerful when API contracts are treated as first-class interfaces.
For example:
Backend
|
v
AgreementContract
|
v
API Gateway
|
v
Shared API Client
|
v
Agreements AppThe contract forms a boundary between frontend and backend.
This means the architecture has multiple boundaries:
Git Repository Boundary
|
v
Application Boundary
|
v
API Contract Boundary
|
v
Microservice Boundary
|
v
Data BoundaryGood architecture is largely about defining and protecting these boundaries.
29. When Should You Choose a Monorepo?
A monorepo is particularly attractive when multiple applications:
Belong to the same overall product/platform
Share substantial frontend libraries
Use similar technologies
Need consistent development standards
Frequently require coordinated changes
Share authentication and infrastructure
Are maintained by related teams
Need visibility across applications
A polyrepo approach may be more appropriate when:
Teams are completely independent
Applications have very different lifecycles
There is little shared code
Strong repository-level isolation is required
Applications belong to different products or organizations
Again, there is no universal answer.
30. Lessons Learned
From an enterprise frontend perspective, several lessons stand out.
1. A monorepo is not a substitute for architecture
Putting everything in one Git repository does not automatically create good architecture.
The applications and libraries still need clear boundaries.
2. Shared code needs ownership
Shared libraries are effectively internal platforms.
They need maintainers, standards and controlled evolution.
3. Don't share business logic unnecessarily
Common UI and technical infrastructure are good candidates for reuse.
Business-specific functionality should remain close to the application that owns it unless there is a genuine common requirement.
4. Contracts are important
The API contract should be treated as an interface between the frontend and backend, not merely as JSON returned by an endpoint.
5. Build only what is affected
As the repository grows, affected-project detection becomes critical for keeping CI/CD efficient.
6. Independent deployment is still possible
Multiple applications can live in one repository while maintaining separate build and deployment lifecycles.
7. Monorepo and microservices solve different problems
A monorepo organizes source code and collaboration.
Microservices organize backend business capabilities and deployment boundaries.
They can complement each other very effectively.
31. Putting Everything Together
The overall architecture can therefore be represented as:

This architecture combines a frontend monorepo with a backend microservices architecture.
The repository provides a common development and collaboration model, while the application and service boundaries preserve separation of responsibilities.
Conclusion
Managing multiple enterprise React applications can become challenging when each application independently implements the same infrastructure, UI components, authentication mechanisms and API communication.
A frontend monorepo provides an alternative approach.
By bringing related applications into a single Git repository and organizing reusable functionality into shared libraries, teams can improve code reuse, consistency, cross-application refactoring and dependency visibility.
However, a monorepo should not become a reason to eliminate application boundaries.
The real objective is to achieve the right balance:
One repository for collaboration, multiple applications for business boundaries, and shared libraries for genuinely common capabilities.
In an enterprise platform, this can provide a strong foundation for multiple React applications while still allowing each business capability to evolve and deploy independently.
The combination of a React monorepo on the frontend and microservices on the backend is particularly powerful because each architecture addresses a different concern.
The monorepo optimizes how teams build and maintain the frontend.
The microservice architecture defines how the backend organizes business capabilities and services.
Together, they provide a scalable foundation for large enterprise applications.
Join the conversation! Your thoughts help the community grow.