How We Maintain a Monorepo, and Why DLL Boundaries Matter More
The company I run fundamentally adopts a Monorepo approach. Our folder structure is therefore not designed to "make the code look neat," but rather based on how we manage dependencies and reuse code properly.
Many people debate whether to organize by feature or by domain. I approach this from a slightly different perspective.
The core is not folders, but project (.csproj) boundaries — in other words, DLL-level separation.
Level 0 - Product-Level Separation
First, we clearly define: "Which product does this code belong to?"
- Academy: Code related to POCU Academy
- ProctoredExamService: Online exam proctoring service
- Engine: Code shared across multiple products (effectively internal middleware)
At this level, product boundaries are already clear. If code ownership becomes ambiguous, the overall structure starts to destabilize.
Level 1 - Project (.csproj) Level
This is the most important layer.
Each product contains multiple .csproj files.
For example:
Academy.ServicesAcademy.BuildfarmShop(the web app users directly interact with)
Level 1 is not just a folder. It represents a DLL boundary.
The Real Reason I Split at Level 1
Is it feature separation? Domain separation? No.
It is for proper dependency management and access control of shared code.
Operational Approach
1) App-Specific Code
Code used only by a specific app remains inside that project. The internal folder structure is freely organized based on team agreement.
In practice, folder structure affects development efficiency by perhaps 10%.
Most navigation happens through:
- Go to Definition
- Search All References
- Global Search (Ctrl + Shift + F)
- Tracing through build errors
This is far more efficient than manually navigating folders.
2) Shared Code
When code is required by two or more apps, we move it into a shared library such as Academy.Libs.
Namespaces are automatically determined by folder structure.
For example:
Academy.Libs/Services/Order/OrderService.cs
-> namespace Academy.Services.Order
If we later extract this folder into a dedicated Academy.Services.csproj, the namespace remains unchanged.
We simply reconnect project references and everything works.
This is critical. We must be able to insert code quickly and extract it into a standalone module when needed.
3) Further Separation When Necessary
As shared code grows, we separate it into dedicated libraries.
For example:
Academy.Entities: ORM entities + query extensionsAcademy.Services: Shared service logic
Here, access control strategy becomes crucial.
Access Control and Collaboration Rules
Many classes inside Academy.Entities and Academy.Services are marked as internal.
We selectively grant access via InternalsVisibleTo only when necessary.
Why?
- ORM entities
- Core service logic
- Performance-critical components
Allowing unrestricted modification by junior developers significantly increases the risk of production issues.
Actual Collaboration Rules
- Shop Project
- Anyone can modify
- Merge to main without mandatory review
- Academy.Services / Academy.Entities
- Only senior developers may modify
- Juniors require senior review before merging
If folder structure contributes 10% to efficiency, structured access control contributes far more.
This is what truly ensures productivity and stability.
Separation for NuGet Size Optimization
Sometimes we split libraries purely for deployment reasons.
For example, placing the clang toolset inside Academy.Libs would increase every application's deployment size by hundreds of megabytes.
So we separate it into its own DLL.
Again, this is not about feature or domain separation — it is about deployment strategy and dependency control.
Level 2 - Internal Project Folders
This layer is flexible.
Typical folders:
- Services
- Models
- Entities
- TransferData
My usual convention:
- DTOs →
TransferData - ViewModels →
Models - Database entities →
Entities
This structure changes frequently:
- When features expand
- When concepts are redefined
- When I make mistakes
Modern C# IDEs automatically update namespaces and references, so moving files is low risk.
Move files, compile, verify. Simple.
How We Actually Navigate the Codebase
In reality:
- 90% of navigation happens via IDE features
- 10% through manual folder traversal
My philosophy is this:
Folder structure is merely a management tool.
What truly matters is project-level dependency management and access control.
Summary
- Level 0 and 1 must be rigorously structured
- Level 1 is about DLL boundaries, not feature/domain labels
- Level 2 can remain flexible
- IDE navigation + compilation drive maintainability
- Eliminating duplication and enforcing access control ensures long-term stability
In one sentence:
Insert quickly, extract cleanly when needed — and enforce strong access control on top.
This is how the company I run manages its monorepo architecture.