How We Maintain a Monorepo, and Why DLL Boundaries Matter More

Pope Kim Feb 18, 2026

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.Services
  • Academy.Buildfarm
  • Shop (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 extensions
  • Academy.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

  1. Level 0 and 1 must be rigorously structured
  2. Level 1 is about DLL boundaries, not feature/domain labels
  3. Level 2 can remain flexible
  4. IDE navigation + compilation drive maintainability
  5. 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.