DEV Community

Cover image for Code-Level Monolith: The Hybrid Architecture & The Art of "Flexible Deployment"
Alireza Feizi
Alireza Feizi

Posted on

Code-Level Monolith: The Hybrid Architecture & The Art of "Flexible Deployment"

1. The Illusion of Choice and the End of False Dichotomies

Over the past decade, the software development industry has witnessed radical swings in architectural paradigms; a rapid, sometimes impulsive movement from Monolithic systems to Microservices, and more recently, a reflective return toward modern, structured monolithic architectures.

For years, software engineers have faced a false dichotomy: either choose the "simplicity and development speed" of a Monolith and accept the risk of it turning into a "Big Ball of Mud," or submit to the back-breaking complexity of Microservices (network management, distributed consistency, and orchestration) to achieve "scalability."

But is there a third way?

This article provides a deep anatomical analysis of the "Code-Level Monolith" (or Modulith) architecture. This architecture is an engineered attempt to achieve the "Holy Grail" of software engineering: combining the modular independence of microservices with the operational simplicity and performance of monolithic systems.

In this approach, we face a paradigm shift:

"Modularity is a logical concept, not a physical one."

In this article, we will demonstrate how to design a system that is completely modular and isolated during development (like microservices) but can be executed as a single binary (All-in-One) or a set of distributed services at deployment time, depending on your needs. This capability of "frictionless switching" is exactly the missing link that engineering teams need to escape "premature complexity."


2. A Return to Reason: Why Tech Giants Shifted Gears

Leading companies like Amazon Prime Video (which reduced costs by 90% by returning to a monolith), Shopify, Google, and Segment have all redefined their strategies in favor of modular monoliths, driven by technical reasons and economic pressures. This return is not out of nostalgia for the past, but a pragmatic response to unmanageable complexity.

To understand why the Code-Level Monolith is emerging as a winning architecture today, we must review the turbulent evolution of software architecture.

2.1. The Era of Traditional Monoliths and the Curse of the "Big Ball of Mud"

The history of modern software development began with monolithic systems. In this classic architecture, all system components—UI, Business Logic, and Data Access—resided in a single Codebase and were deployed as a single Executable unit.

  • The Deceptive Advantage: Simplicity. Developers wrote code quickly, applied changes, and deployment ended with simply copying a file to the server.
  • The Fall: As systems grew and complexity increased, this architecture often morphed into the famous "Big Ball of Mud" anti-pattern. In this state:
    • Blurred Boundaries: Lines between different modules vanished, and classes became Tightly Coupled.
    • Destructive Butterfly Effect: A small change in tax calculation logic could lead to unpredictable errors in inventory management.
    • Fragility: "Fear of Change" slowed down development and degraded software quality.

2.2. The Microservices Mirage: Promises and Hidden Costs

In response to the dead-end of traditional monoliths, and inspired by companies like Netflix and Uber facing planetary-scale scalability challenges, Microservices emerged as the savior.

  • The Core Idea: Breaking the "Big Ball" into small, independent services, each responsible for a specific Business Capability and communicating over a network.
  • The Golden Promises:
    • Team Independence: Each team can work with their preferred technology and speed.
    • Granular Scaling: Allocating more resources only to high-consumption services (e.g., the Search service).
    • Fault Isolation: A crash in the payment service doesn't necessarily bring down the whole site.
  • The Bitter Reality: Over time, many organizations faced a painful truth: "You are not Netflix!" Microservices didn't eliminate complexity; according to the "Conservation of Complexity" law, they merely shifted it from the Code level (Logic) to the Infrastructure & Ops level. New challenges included:
    • Network Latency: Replacing fast in-memory calls with slow HTTP/gRPC requests.
    • Debugging Nightmare: Tracing a bug across 10 different services.
    • Distributed Transactions: Needing complex patterns like Sagas to maintain data integrity.
    • Overhead Costs: Needing specialized DevOps teams just to keep the lights on.

2.3. The Rise of Code-Level Monolith: The Golden Balance (2020 - 2025)

In recent years, a new approach known as "Modular Monolith" or Code-Level Monolith has gained popularity. This architecture attempts to reconcile "development simplicity" with "architectural order."

In this paradigm:

  1. Physical Monolith: The system is still built and deployed as a Single Unit (like the Quick Connect project in all-in-one mode).
  2. Logical Modularity: All code is usually kept in a Monorepo, but at the code level, modules are strictly isolated, and Strict Boundaries are enforced by the compiler or linting tools.

The Fundamental Difference:
Communication between modules (e.g., between the Order module and User module) happens via Function Calls in memory, not over the network. This means Zero Latency.

This architecture rests on a key principle:

"You can have a fully modular system that runs in a single process (Code-Level Monolith), and conversely, you can have a system of microservices that are tightly coupled and dependent (Distributed Monolith)."


3. Theoretical Foundations: Anatomy of a "Modular Monolith"

To successfully implement a Code-Level Monolith, simply "dumping all files into one folder" is not enough. This architecture requires high engineering discipline, which we summarize in three fundamental principles: Strict Boundaries, Location Transparency, and Isolated Data Strategy.

3.1. Strict Boundaries: The Firewall Between Modules

In a Monorepo, your biggest enemy is "Abstraction Leak." If a developer can freely use any class inside another class, you will very quickly end up with "Spaghetti Code."

  • Compile-Time Enforcement: In Go, the internal packages mechanism is designed exactly for this purpose. Code inside an internal folder is only accessible by its parent package; other modules cannot import it. This turns "Encapsulation" from an ethical recommendation into a compiler constraint. (In ecosystems like Python/Django or Java/Spring, this is done using Linting Tools or separate Maven/Gradle modules).
  • Alignment with DDD: Each module in this architecture corresponds to a Bounded Context in Domain-Driven Design (DDD). The Chat module should not directly see the User database model; it should only speak to the "Public API" or "Contract" of the Manager module.

3.2. Data Strategy: Physical Sharing, Logical Separation

The Achilles' heel of most architectural migrations is the database layer. In Code-Level Monolith, the golden rule is:

"The Database is shared, but the Schema is private."

  • No Foreign Keys: There should be no Foreign Keys between tables of different modules (e.g., orders and users). While this makes Referential Integrity harder to guarantee, it is vital for preserving module independence.
  • Indirect Access: No module has the right to directly SELECT or JOIN on another module's tables. If the Chat module needs to know the user's name, it shouldn't read the users table; instead, it must call the GetUserProfile method from the Manager module. This ensures that if the Manager database structure changes one day, the Chat code won't break.

3.3. Location Transparency Principle

This is the beating heart of the "Modular Monolith" architecture. Your business logic code should not know if the service it calls is running in the same process (In-Memory) or on another server (Over Network).

  • Dependency Inversion: Instead of the Order service depending on the User service, it should depend on an Interface that it defines itself.
    • Wrong: import "myapp/user/service" and using UserService directly.
    • Right: Defining type UserProvider interface inside the Order module.
  • Runtime Binding: It is the job of the main.go file (or Composition Root) to decide what implementation to inject into this interface:
    • All-in-One Mode: Inject the service object directly (In-Memory Adapter) -> Call speed: Nanoseconds.
    • Microservice Mode: Inject a gRPC Client Wrapper (Network Adapter) -> Call speed: Milliseconds.

3.4. The Grafana Pattern: The Art of Poly-Deployment

The Grafana Loki project (log aggregation system) is the best industrial example of this architecture. Loki's source code includes various components like Ingester, Distributor, and Querier, all compiled into a single binary.

The magic happens at runtime:

  • Run with flag ./loki -target=all: All components spin up in one process and talk via Function Calls (suitable for local development or light workloads).
  • Run with flag ./loki -target=ingester: The binary acts only as an Ingester, and other codes are turned off (suitable for scalable environments needing 50 Ingester nodes).

This pattern allows the engineering team to change the Deployment Architecture without changing a single line of Business Logic.


4. Anatomy of Quick Connect: Theory in Practice

In this section, we go straight into the "operating room." The open-source project Quick Connect was designed as a laboratory to implement the Code-Level Monolith pattern in Go. Let's see how these theories translate into code.

4.1. Project Structure: Order in Chaos

The repository for this project is a Monorepo, but not a messy, free-for-all one. The directory structure is designed to scream "Service Independence":

  • app/ (Service Domain): All main services (like chatapp, managerapp, and notificationapp) live in this directory. The vital point is that each service has a completely isolated database and repository. No service has the right to peek into another service's repository directory.
  • pkg/ (Shared Code): Since all code is in one repository, general utilities (like Logger, Error Handling, and Auth tools) are placed here to prevent rewriting.
  • cmd/ (Entry Point): This is where we decide how the application runs (Microservice or Monolith).

4.2. The Art of Defining Interfaces: The Consumer Law

In Quick Connect, direct communication is forbidden. If the chatapp service needs user information to display the sender's name, it must not depend on the managerapp service.

The solution is Dependency Inversion. The chat service defines an interface that says: "I need someone who gives me the user's name given an ID."

Golden Rule: The interface must be defined at the Consumer side, not the Provider side.

4.3. The Adapter Pattern: The Bridge

This is where the real magic happens. For every external dependency (like the User service), we write two different implementations (Adapters). The decision of which one to use is deferred to Runtime.

A) Network Adapter (gRPC Adapter):
When the system runs as a Microservice, this adapter serializes the request and sends it over the network:

package manager

import (
    "context"
    "github.com/syntaxfa/quick-connect/protobuf/manager/golang/userinternalpb"
    "google.golang.org/grpc"
)

// UserInternalAdapter acts as a client adapter via gRPC network call.
type UserInternalAdapter struct {
    client userinternalpb.UserInternalServiceClient
}

func NewUserInternalAdapter(conn grpc.ClientConnInterface) *UserInternalAdapter {
    return &UserInternalAdapter{
       client: userinternalpb.NewUserInternalServiceClient(conn),
    }
}

func (ui *UserInternalAdapter) UserInfo(ctx context.Context, req *userinternalpb.UserInfoRequest,
    opts ...grpc.CallOption) (*userinternalpb.UserInfoResponse, error) {
    // Sends request over the network
    return ui.client.UserInfo(ctx, req, opts...)
}
Enter fullscreen mode Exit fullscreen mode

B) Local Adapter (In-Memory Adapter):
When the system runs as a Code-Level Monolith, this adapter directly calls the other service's method in memory (RAM). There is no network and no latency involved:

package manager

import (
    "context"
    "github.com/syntaxfa/quick-connect/app/managerapp/service/userservice"
    "github.com/syntaxfa/quick-connect/protobuf/manager/golang/userinternalpb"
    "github.com/syntaxfa/quick-connect/types"
    "google.golang.org/grpc"
)

// UserInternalLocalAdapter acts as a client local adapter with func calls.
type UserInternalLocalAdapter struct {
    userSvc *userservice.Service // Direct reference to the service struct
}

func NewUserInternalLocalAdapter(userSvc *userservice.Service) *UserInternalLocalAdapter {
    return &UserInternalLocalAdapter{
       userSvc: userSvc,
    }
}

func (uil *UserInternalLocalAdapter) UserInfo(ctx context.Context, req *userinternalpb.UserInfoRequest, _ ...grpc.CallOption) (
    *userinternalpb.UserInfoResponse, error) {
    // Direct Function Call (No Network)
    resp, sErr := uil.userSvc.UserInfo(ctx, types.ID(req.GetUserId()))
    if sErr != nil {
       return nil, sErr
    }

    // Convert Domain Entity back to Protobuf to satisfy the contract
    return convertUserInfoToPB(resp), nil
}
Enter fullscreen mode Exit fullscreen mode

A Smart Architectural Decision: Protobuf as the Single Contract
There is a subtle point in the code above: the input and output of both adapters are Protobuf.
In Quick Connect, we accepted that Protobuf plays the role of the final Contract. Even in local mode, domain data (types.ID) is converted to Protobuf format. This makes switching between local and network modes completely transparent, requiring no changes to the calling service layer.

4.4. The Climax: The All-in-One Binary

All these puzzle pieces come together in the cmd/all-in-one/main.go file.
This file plays the role of the Composition Root. Here:

  1. All services (Chat, Manager, Notification) are instantiated in memory.
  2. Instead of injecting gRPC clients into services, LocalAdapters are created and passed to them.
  3. The final result is a single executable file that possesses all system capabilities, but communication between components happens at Function Call speed.

4.5. Why Did Quick Connect Choose This Path?

The answer is summed up in one word: Realism.
More than 90% of software projects never reach massive scale. Starting a project with 10 microservices, 5 databases, and Kubernetes clusters only results in wasted resources and futile complexity.

The Quick Connect architecture allows you to:

  1. Today: Develop and deploy the product with the simplicity of an all-in-one (Easy Deployment).
  2. Tomorrow: If a part of the system (e.g., Chat) comes under heavy traffic, you can separate it into an independent microservice just by changing the main file (Scalability without Rewrite).

This means Deployment Architecture is decoupled from Code Architecture; and this is the freedom of action every software engineer dreams of.


5. Returning Speed and Simplicity

Perhaps the biggest victim of Microservice architecture is Developer Experience (DX). In a typical microservice setup, to test a simple feature, a programmer has to spin up 10 Docker containers, manage ports, and fight with their system's 32GB RAM usage.

But in Quick Connect, the story is different.

5.1. The End of Localhost Hell

In the Code-Level Monolith architecture, your development environment isn't exactly like your production environment; and that is Good!
For a developer working on the Chat service, it doesn't matter if the Auth service is running in a separate Kubernetes pod or not. They just want their code to work.

In this project, the whole system comes up with a simple command:

go run cmd/all-in-one/main.go
Enter fullscreen mode Exit fullscreen mode

This magic command runs all services (Manager, Chat, Notification) in a single process.

  • Resource Usage: Less than 100MB of RAM (compared to gigabytes for microservices).
  • Hot Reload: Code changes and server restarts take less than 1 second.
  • Debugging: Placing a breakpoint in the Chat service and stepping into the Manager service code is possible without any Remote Debugging complexity.

5.2. Integration Tests: Fast, Stable, Cheap

End-to-End tests in the microservices world are often slow and flaky because they depend on the network and all services being up.
In the Quick Connect approach, we can run integration tests in memory.
Since modules are connected via interfaces and LocalAdapters, we can write a scenario where:

  1. A user registers in Manager.
  2. Sends a message in Chat with the same token.
  3. Receives a notification in Notification.

All of this happens in a fraction of a second without a single byte crossing the network card. This means fast feedback for the developer and CI Pipelines that pass in 2 minutes instead of 20.

5.3. Eliminating "Latency" in Chatty Modules

In chat systems, some services are inherently "chatty." For example, the WebSocket service might need to request the manager service for every incoming message.
If these requests require a gRPC call (2ms) and you have 10,000 messages per second, the network overhead will choke the system.
In All-in-One mode, this check becomes a Function Call taking nanoseconds. This means we have multiplied performance without any complex optimization.


6. The Future of Architecture

What we did manually (Explicitly) in Quick Connect—defining interfaces and writing two adapter versions (Local vs. Remote)—is a demonstration of the future where software engineering is heading.

6.1. The Rise of Poly-Deployment Frameworks (Google Service Weaver)

Google recently introduced a framework called Service Weaver for Go that follows this exact philosophy, but with a difference: "Magical Automation."

In Quick Connect, we decide ourselves whether to use UserLocalAdapter or UserGrpcAdapter. But in Service Weaver:

  1. You write code as if everything is a Monolith.
  2. Components are defined with standard Go interfaces.
  3. At Deploy time, you tell the system via a simple config file (weaver.toml): "Separate this component and that component and run them on different servers."

The Service Weaver framework scans the code, and if it detects two components are in the same process, it uses Function Calls (with zero serialization); if they are separate, it generates and runs the gRPC and Protobuf code itself.

This implies:

"Complete separation of Logical Architecture from Physical Architecture."


7. Final Recommendation: The Winning Strategy for 99% of Projects

And finally, what I want to say is this:

"Start with Monolith, but code as if it were Microservices."

The history of software engineering is a graveyard full of startups buried under the complexity of managing 50 microservices before reaching their first 1000 users. And also companies choked by the "Big Ball of Mud."

Our Proposed Roadmap (Based on Quick Connect experience):

  • Day 1 (Project Start):
    • Create a Monorepo structure.
    • Use internal folders in Go to lock down boundaries (which, essentially, we didn't use directly in this project, but strict discipline was applied :)).
    • Establish communication between modules only and exclusively through Interfaces.
    • Deploy the system as All-in-One (like cmd/all-in-one/main.go).
  • Day 100 (Traffic Growth):
    • Since your code is isolated, you have no technical debt.
    • Continue deploying on a stronger server (Vertical Scaling). (The cost of a strong server is cheaper than the cost of a DevOps team).
  • Day 1000 (Massive Scalability):
    • The profiler (pprof) shows that the Chat module has become a bottleneck.
    • You write a separate main.go just for the Chat module (like cmd/chat/main.go).
    • You replace the Local Adapter with the gRPC Adapter.
    • Now you have a microservice system, exactly where you needed it and exactly when you needed it.

Code-Level Monolith is not a regression; it is engineering maturity. This architecture allows you to have the speed of a startup and the order of an enterprise simultaneously.

If you enjoyed this article, I would appreciate your support for Quick Connect by starring the repo and sharing it with others:
https://github.com/syntaxfa/quick-connect

Top comments (0)