Contrarian View: Microservices Are Dead – Use Modular Monoliths with Go 1.23 and gRPC
The software industry has spent the last decade evangelizing microservices as the gold standard for scalable, maintainable systems. But for 80% of teams, microservices introduce more problems than they solve. It’s time to embrace a better alternative: modular monoliths, supercharged by Go 1.23 and gRPC.
The Microservices Hangover
Microservices promised independent deployments, fault isolation, and technology heterogeneity. But in practice, most teams adopting microservices face a laundry list of pain points:
- Distributed complexity: Tracing requests across 10+ services requires expensive tooling like Jaeger or Zipkin, and debugging becomes a nightmare.
- Network overhead: Every inter-service call adds latency, serialization costs, and potential points of failure.
- Operational bloat: Managing service discovery, load balancers, container orchestration, and CI/CD for dozens of services requires a dedicated platform team most small/medium orgs can’t afford.
- Data consistency woes: Distributed transactions are hard; eventual consistency often leads to subtle bugs that are impossible to reproduce locally.
For startups, mid-sized companies, and even enterprise teams building line-of-business apps, microservices are almost always overkill.
What Is a Modular Monolith?
A modular monolith is a single deployable unit with strict, enforced boundaries between internal modules. Unlike a traditional "big ball of mud" monolith, modular monoliths:
- Prohibit circular dependencies between modules
- Expose only public interfaces for cross-module communication
- Allow independent development of modules by different teams
- Can be incrementally split into microservices later if scaling demands it
The key is that module boundaries are enforced at the code level, not the network level. You get the simplicity of a single deployment with the structure of microservices.
Why Go 1.23 Is the Perfect Fit
Go’s design philosophy aligns perfectly with modular monoliths: simple, fast, strongly typed, and easy to refactor. Go 1.23 adds several features that make building modular monoliths even easier:
- Enhanced go test tooling: Improved coverage reporting and test caching reduce friction for module-level testing.
- The new unique package: Efficiently intern strings and other comparable types, reducing memory overhead for high-throughput monoliths handling gRPC payloads.
- Better generics support: Mature generics make it easier to build reusable module interfaces without code duplication.
- Fast compilation: Go’s sub-second compilation times mean you can iterate on modules quickly, even in large codebases.
Where gRPC Fits In
You might think gRPC is only for cross-service communication, but it’s a secret weapon for modular monoliths. Define every module’s public interface using Protocol Buffers and gRPC service definitions:
- Strict contracts: Protobuf enforces type safety across module boundaries, eliminating the "I thought that field was optional" bugs common in REST or in-process function calls.
- Future-proofing: If you ever need to extract a module to a standalone microservice, you already have a gRPC endpoint defined. Just deploy the module as a separate service and update clients to call it over the network instead of in-process.
- In-process efficiency: Use an in-process gRPC client for inter-module calls, so you get the benefits of gRPC’s contracts without network latency or serialization overhead.
For example, a user module might expose a GetUser gRPC method. In the monolith, other modules call it via an in-process client. If the user module needs to scale independently later, you deploy it as a separate gRPC service, update the client to point to the network endpoint, and nothing else changes.
Getting Started
Building a modular monolith with Go 1.23 and gRPC is straightforward:
- Define your module boundaries using Protobuf service definitions. Keep modules small, focused on a single business capability.
- Enforce module isolation using Go’s package visibility (uppercase for public, lowercase for private) or a tool like golangci-lint to ban circular imports.
- Use in-process gRPC clients for inter-module communication. The grpc-go library supports in-process transport out of the box.
- Write module-level tests using Go 1.23’s improved testing tools, mocking gRPC clients for dependent modules.
When Should You Still Use Microservices?
Microservices aren’t dead for everyone. If you have:
- Teams of 50+ developers working on the same system
- Modules with wildly different scaling requirements (e.g., a high-throughput ingestion service and a low-traffic admin panel)
- A need for technology heterogeneity (e.g., one module in Python for ML, another in Go for APIs)
Then microservices might make sense. For everyone else, modular monoliths with Go 1.23 and gRPC deliver better velocity, lower costs, and easier maintenance.
Conclusion
The microservices hype cycle is ending. Smart teams are realizing that most systems don’t need distributed complexity. By combining the structure of modular monoliths with Go 1.23’s simplicity and gRPC’s strict contracts, you can build systems that are easy to develop, deploy, and scale – without the microservices hangover.
Top comments (0)