internal/website/blog/8.md
March 7, 2026 — By the Go Micro Team
We set out to answer a question: how fast can you go from a feature list to a working, production-grade application using Go Micro? The answer surprised us.
We built Micro Chat — a full-featured chat platform with real-time messaging, AI integration, SSO, webhooks, full-text search, file uploads, and more. Thirteen domain services. One binary. One afternoon.
Here's how we did it, and what it says about Go Micro's role in modern application architecture.
We started with a list. Not a design doc, not a spec — a list of things a real chat app needs:
That's a lot. In a traditional microservices setup, you'd spend a week just on the infrastructure — service mesh, message broker, API gateway, deploy pipelines, Kubernetes manifests. We spent zero time on that.
Each feature maps to a service. Each service is a Go package under service/:
service/
├── agent/ # Claude AI integration
├── audit/ # Audit logging
├── chats/ # Channels, DMs, messages, WebSocket hub
├── export/ # Data export
├── files/ # File uploads
├── groups/ # User groups
├── invites/ # Invite links
├── mcp/ # Model Context Protocol server
├── search/ # Full-text search (FTS5)
├── sso/ # SSO/OIDC
├── threads/ # Threaded replies
├── users/ # Auth, profiles, roles
└── webhooks/ # Outbound webhooks
Every service follows the same pattern: a struct, a constructor, and methods. No framework magic, no code generation, no annotations. Just Go.
// service/search/search.go
type Service struct{}
func NewService() *Service { return &Service{} }
func (s *Service) Search(filter SearchFilter) ([]SearchResult, int, error) {
// FTS5 query against SQLite
}
The simplicity is the point. A new team member can read any service top to bottom in five minutes.
Here's where Go Micro earns its keep. Each domain is declared as a micro.Service, and they're all composed into a single runnable group:
gateway := micro.New("gateway",
micro.BeforeStart(func() error {
database.Init()
auth.Init()
searchSvc.InitFTS()
go wsHub.Run()
go httpServer.ListenAndServe()
return nil
}),
micro.AfterStop(func() error {
httpServer.Close()
database.Close()
return nil
}),
)
usersSvc := micro.New("users")
chatsSvc := micro.New("chats")
groupsSvc := micro.New("groups")
agentSvc := micro.New("agent")
mcpSvc := micro.New("mcp")
searchSvc := micro.New("search")
threadsSvc := micro.New("threads")
webhooksSvc := micro.New("webhooks")
ssoSvc := micro.New("sso")
auditSvc := micro.New("audit")
g := micro.NewGroup(gateway, usersSvc, chatsSvc, groupsSvc, agentSvc,
mcpSvc, searchSvc, threadsSvc, webhooksSvc, ssoSvc, auditSvc)
g.Run()
micro.NewGroup handles lifecycle management — ordered startup, signal handling, graceful shutdown. You declare your services, compose them, and run. That's the entire main.go.
The startup banner tells the story:
Micro Chat - Modular Monolith (go-micro.dev/v5)
─────────────────────────────────────────
Server: http://localhost:8080
Claude AI: Configured (with tools)
MCP: Enabled
SSO/OIDC: Enabled
─────────────────────────────────────────
We could have built this as 13 separate microservices from the start. We deliberately didn't. Here's why:
Velocity. A single binary means go build && ./server. No Docker Compose, no service discovery config, no inter-service networking. We went from zero to a working app in hours, not days.
Simplicity. One database (SQLite), one process, one deploy. You can run this on a $5 VPS or your laptop. The operational overhead is effectively zero.
Clean boundaries anyway. The service packages don't know about each other. service/webhooks has no idea service/search exists. The API layer composes them, but the domains are fully isolated. We get the architectural benefits of microservices without the infrastructure tax.
Cheap iteration. Want to add audit logging? Create service/audit, add a few methods, wire it into the API handler. The cost of a new service is one package and two lines in main.go. We added SSO/OIDC support the same way — the pattern is always identical.
This is the real power of the modular monolith: it's not a dead end, it's a starting point. When scale or team structure demands it, the extraction path is clear.
Step 1: The interface already exists. Every service has a clean method-based API. search.Service.Search(filter) doesn't change whether it's an in-process call or an RPC endpoint.
Step 2: Go Micro makes it native. Replace the in-process call with a micro.Client call. The service moves to its own binary, registers with service discovery, and the caller barely changes.
Step 3: Extract incrementally. Maybe agent (the AI service) needs its own deployment because it's making expensive API calls. Pull it out. Everything else stays in the monolith. You don't have to go all-or-nothing.
Step 4: The database splits last. Each service already accesses only its own tables — users has users, search has messages_fts, SSO has oidc_providers and oidc_users. When you extract a service, you move its tables to a dedicated database. The code barely changes.
The progression looks like this:
Day 1: Modular monolith (single binary, SQLite)
Month 3: Extract agent service (expensive AI calls)
Month 6: Add message broker for webhooks and audit (async events)
Year 1: Split database per service, full microservices where needed
You grow into microservices. You don't start there.
For the curious:
net/http, crypto, encoding/json — minimal dependenciesTotal external dependencies: a handful. Total services: 13. Total binaries: 1.
Define services early, split them late. Drawing domain boundaries at the start costs nothing. Deploying 13 separate services on day one costs everything.
Go Micro's group primitive is underrated. micro.NewGroup is a small API with a big impact. It turns "a bunch of services" into "a managed application" with lifecycle hooks, signal handling, and graceful shutdown.
The modular monolith is not a compromise. It's often the right architecture for most of a product's lifetime. You get the modularity of microservices, the simplicity of a monolith, and a clear path forward when you need to break things apart.
AI integration is just another service. The agent service wraps the Claude API. The mcp service exposes tools over JSON-RPC. They're not special — they're domain services with the same constructor-and-methods pattern as everything else. That's how it should be.
The full source is at github.com/micro/chat. Clone it, run go build ./cmd/server && ./server, and you have a working chat app with 13 services in a single binary.
Then start thinking about which service you'd extract first — and notice how easy the answer is, because the boundaries are already there.
That's the modular monolith. That's Go Micro.