1.1 Why Modular Monolith

The Benefits of a Modular Architecture

Choosing a modular architecture is not primarily about organizing code into folders or creating more projects inside a solution. It is about deciding where responsibilities belong, which parts of the system may change independently, and how different areas of the business are allowed to collaborate.

This distinction is important.

A system may contain dozens of projects and still behave like a single, tightly coupled application. Conversely, a system deployed as a single application can preserve strong internal boundaries and allow its parts to evolve with a significant degree of autonomy.

BrewUpErp follows the second approach.

The backend is currently deployed as a modular monolith, but its internal structure is organized around business capabilities such as Sales, Warehouse, Purchases, Master Data, and Dashboards. Each module represents more than a technical component: it owns a specific part of the brewery domain, its language, its rules, and its data.

The main benefit of this architecture is therefore not deployment simplicity alone. It is the ability to manage change without allowing every change to spread across the entire system.

Reducing Cognitive Load

As a system grows, understanding it becomes progressively harder.

In a traditional layered monolith, code is usually divided into technical areas such as controllers, services, repositories, and entities. This organization may look clean at first, but a single business feature often cuts across every layer.

To understand how a sales order works, a developer may need to navigate through multiple projects, generic services, shared entities, database repositories, and integration components. The business concept is distributed across the technical structure.

A modular architecture reverses this relationship.

Code that belongs to Sales remains close to the Sales module. Warehouse concepts remain inside Warehouse. Purchasing rules remain inside Purchases. A developer working on a sales order can focus primarily on the Sales bounded context instead of loading the entire ERP into their head.

The module becomes a cognitive boundary.

This does not remove complexity, but it contains it. Each part of the system can be understood through a smaller and more focused model.

Keeping Changes Local

One of the most practical advantages of modularity is locality of change.

A new Sales requirement should primarily affect the Sales module. A change in stock allocation should primarily affect Warehouse. A modification to supplier ordering policies should belong to Purchases.

Without clear boundaries, changes tend to propagate.

A property added to a shared entity may require updates in several unrelated services. A database modification may affect multiple workflows. A seemingly local requirement may trigger changes throughout the application because many components depend on the same internal representation.

In a modular architecture, other modules should depend on explicit contracts rather than internal implementation details.

For example, Sales should not need to understand how Warehouse stores inventory or calculates its internal state. It only needs a defined way to request information or communicate that an order requires fulfilment. Warehouse can then change its internal model without forcing Sales to change with it.

The goal is not to eliminate dependencies. A business system is built through collaboration. The goal is to make those dependencies intentional, visible, and limited.

Protecting the Domain Model

Different parts of a business often use similar words with different meanings.

A beer in the product catalogue is not necessarily represented in the same way as stock stored in the warehouse. A sales order, a purchase order, and a shipment may reference the same physical product, but they do so for different reasons and according to different rules.

Using one universal model for the entire application may appear to reduce duplication. In practice, it often creates semantic coupling.

Every concept becomes a compromise between the needs of several areas. Models accumulate properties that are irrelevant to most consumers. Changes made for one use case unexpectedly affect others.

Bounded contexts allow each module to define the model that best represents its responsibilities.

Master Data can own the commercial identity of a beer. Sales can model products from the perspective of customer orders. Warehouse can model inventory, availability, and shipment preparation. These representations may be related, but they do not need to be identical.

This protects the ubiquitous language of each context and prevents the domain model from becoming a collection of generic, shared data structures.

Enabling Independent Evolution

BrewUpErp currently keeps its backend modules inside a single deployable application. This avoids many of the operational costs associated with distributed systems while the domain and the project are still evolving.

However, sharing a deployment unit does not mean that every module must evolve in the same way.

Each bounded context can have its own domain model, application services, infrastructure implementations, read models, and tests. The application host acts as the composition root that brings the modules together, while their internal responsibilities remain separated.

This provides an important balance.

The project retains the simplicity of a monolithic deployment, but it does not have to accept the structural coupling of a traditional monolith.

A module may adopt a new persistence strategy, reorganize its aggregates, or introduce a different integration mechanism without requiring a redesign of the whole system—as long as its external contracts remain stable.

This is particularly valuable for BrewUpErp because the project is deliberately a work in progress. Some areas are more mature than others, and the architecture continues to evolve as new experiments are introduced. Strong boundaries allow that experimentation to remain local.

Making Architectural Decisions Visible

Modularity makes the shape of the system easier to see.

In BrewUpErp, modules are explicitly registered by the application composition root. The main host does not discover arbitrary services hidden throughout the codebase; it declares which modules participate in the application.

This makes the architecture visible near the entry point of the system.

The repository structure also communicates the major business capabilities. A developer can recognize Sales, Warehouse, Purchases, Master Data, Dashboards, and long-running processes without first reconstructing the architecture from namespaces and database tables.

Visibility matters because architecture is not only what was originally designed. It is what developers can still recognize after months or years of changes.

When boundaries are visible, violations become easier to identify and architectural discussions become more concrete.

Improving Testability

Modules create natural testing boundaries.

Domain rules can be tested inside the context that owns them. Application use cases can be exercised through the module façade. Integration contracts can be tested without requiring knowledge of another module’s internal implementation.

This allows tests to focus on behaviour and responsibility rather than on the entire system.

Modularity also makes architectural testing possible.

BrewUpErp can define rules such as:

  • Sales must not reference Warehouse internals.
  • Domain projects must not depend on infrastructure.
  • Read models must not modify aggregates.
  • Modules may collaborate only through approved contracts.

These rules can be checked automatically through architecture tests and fitness functions.

This is essential because directory structure alone cannot preserve modularity. Without continuous verification, shortcuts gradually introduce direct references, shared internal models, and accidental dependencies.

A modular architecture is not maintained by intention. It is maintained by feedback.

Supporting Different Interfaces

BrewUpErp is not limited to a single user interface.

The project contains an HTTP API, a Blazor web application, and a .NET MAUI mobile application. The web application also mirrors part of the bounded-context decomposition used by the backend.

A modular backend helps support these different consumers because capabilities are exposed through clear application boundaries.

The web client, mobile client, external integrations, and future automated agents should not need direct access to domain internals. They interact with use cases, query services, and published contracts.

This reduces the risk that a particular interface begins to dictate the structure of the domain model.

The business capabilities remain central; interfaces become different ways of accessing them.

Creating an Evolutionary Path

A modular monolith does not imply that the system must remain a monolith forever.

It simply avoids making distribution the starting point.

Should a module eventually require independent scaling, a different release cycle, stronger isolation, or separate ownership, an existing boundary provides a possible extraction point.

This does not make the transition to microservices automatic. A project boundary is not the same as a distributed boundary, and network communication introduces new consistency and operational problems.

However, a well-defined module gives the team something that can be evaluated independently.

The decision can then be based on an actual need rather than on an assumption made at the beginning of the project.

In this sense, modularity preserves options.

It allows BrewUpErp to begin with a simpler operational model while leaving room for selected capabilities to become independently deployable when there is a concrete reason to do so.

Providing Boundaries for AI Capabilities

The modular structure also becomes important as BrewUpErp evolves toward AI-assisted and agent-based capabilities.

An AI model should not receive unrestricted access to the entire internal system. It should interact with explicit business capabilities exposed through controlled tools, query services, and contracts.

A Sales capability can answer questions about orders. Warehouse can expose availability and inventory information. Master Data can provide catalogue and customer information. Each capability remains grounded in the context that owns the corresponding knowledge.

The same boundaries used to protect the application from accidental coupling can therefore protect it from semantic ambiguity introduced by AI integrations.

Instead of exposing one undifferentiated ERP model, the system can expose a collection of specialised capabilities.

This makes tool descriptions clearer, limits the information available to each operation, and provides a foundation for agents with bounded responsibilities.

AI does not make modularity less important. It makes explicit boundaries even more valuable.

Modularity Is Not Free

A modular architecture introduces its own costs.

Contracts must be designed and maintained. Some concepts may be represented more than once. Cross-context workflows require explicit coordination. Developers must resist the temptation to bypass a module because accessing its database or internal classes appears faster.

There is also a risk of creating artificial boundaries before the domain is sufficiently understood.

For this reason, BrewUpErp should not be presented as a finished architectural blueprint. It is an evolving project in which boundaries can be challenged and refined as the understanding of the brewery domain improves.

The objective is not to create as many modules as possible.

The objective is to find boundaries that allow business concepts to remain coherent and change to remain manageable.

A Structure Designed for Change

The greatest advantage of a modular architecture is not that the code looks cleaner.

It is that the system can continue to change without every new requirement increasing the coupling between all its parts.

For BrewUpErp, modularity provides:

  • smaller areas to understand;
  • business-focused models;
  • localised changes;
  • explicit collaboration;
  • enforceable architectural rules;
  • support for multiple interfaces;
  • controlled exposure to AI systems;
  • and an evolutionary path toward independently deployable components.

BrewUpErp remains a work in progress. Modules will evolve, responsibilities may move, and some current decisions will probably be reconsidered.

That is not a failure of the architecture.

It is precisely the reason the architecture is modular.