--- layout: blog title: "We Built a Full Chat App in a Day — Here's How" permalink: /blog/8 description: "How we defined 13 services, built a production-grade chat app, and shipped it as a single binary using Go Micro's modular monolith pattern." --- # We Built a Full Chat App in a Day — Here's How *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. ## The Feature List We started with a list. Not a design doc, not a spec — a list of things a real chat app needs: - User registration, authentication, profiles, and roles - Channels and direct messages - Real-time messaging with WebSockets — typing indicators, read receipts, reactions, edit/delete - User groups with membership and permissions - Threaded replies on messages - Full-text search across all messages - Invite links with expiration and usage limits - File uploads and message attachments - Data export (JSON and CSV) - Outbound webhooks with event subscriptions and HMAC signing - AI assistant powered by Claude with tool use and vision - MCP server exposing tools over JSON-RPC 2.0 - SSO/OIDC with external identity providers - Audit logging for admin and security events 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. ## One Service Per Domain 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. ```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. ## Go Micro Ties It Together 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: ```go 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 ───────────────────────────────────────── ``` ## Why a Modular Monolith? 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. ## How It Breaks Out 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. ## The Stack For the curious: - **Go Micro v5** — service lifecycle, composition, and the future extraction path - **SQLite + FTS5** — embedded database with full-text search (swap for Postgres when ready) - **Gorilla WebSocket** — real-time messaging with typing indicators and read receipts - **Claude API** — AI agent with tool use, vision, and streaming - **MCP (JSON-RPC 2.0)** — Model Context Protocol for AI tool integration - **Go standard library** — `net/http`, `crypto`, `encoding/json` — minimal dependencies Total external dependencies: a handful. Total services: 13. Total binaries: 1. ## What We Learned **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. ## Try It Yourself The full source is at [github.com/micro/chat](https://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.