Legacy Monolith vs Microservices Split
tektree intermediate 5 min read
ELI5
The legacy app is the old all-in-one supermarket: one building, every aisle. The new layout is a row of specialty shops. Both are open during the move — customers walk into either, but each shop only sells one thing and shares one parking lot (Mongo).
Technical Deep Dive
services/api (port :8000, entrypoint main.go) predates the gateway+microservice split. It still serves production today and shares the MongoDB cluster with the new services. The reorganisation is incremental: each new microservice peels one bounded context off the monolith.
Side-by-Side
flowchart LR subgraph Legacy["services/api :8000 (monolith)"] L1[main.go] L2[controllers/] L3[routes/] L4[models/] L5[middleware/auth.middleware.go] L1 --> L2 --> L3 L1 --> L4 L1 --> L5 end subgraph Modern["api-gateway :8080 + services/*-service"] M1[api-gateway routes] M2[user-service :8081] M3[knowledge-service :8082] M4[gamification-service :8083] M5[payment-service :8084] M6[realtime-service :8085] M1 --> M2 M1 --> M3 M1 --> M4 M1 --> M5 M1 --> M6 end Mongo[(MongoDB shared)] Legacy --> Mongo Modern --> MongoDifferences at a Glance
| Aspect | Legacy services/api | Microservices |
|---|---|---|
| Entrypoint | main.go (no cmd/) | cmd/server/main.go |
| Layout | top-level controllers/, routes/, models/, middleware/ | internal/handlers, internal/models, internal/config |
| Router | mux.Router + gin.Engine (both declared) | Gin only |
| CORS | rs/cors with GET, POST, PUT, DELETE | Gateway-side CORS only |
| Auth | middleware.AuthenticateUser() reading access_token cookie or Authorization: Bearer, calls utils.ValidateToken() locally | JWT verified once at gateway; X-User-ID / X-User-Tier forwarded |
| Identity injection | c.Set("user_id", ...) from local validation | trusted gateway header |
| Routes | os.Getenv("API_VERSION") prefix + 22 route groups | per-service /api/v1/... paths |
| Collections | 23 in one binary (awards, users, …, votes) | partitioned across services |
Legacy Collections (in one binary)
awards, users, educations, experiences, collaborationrequests, references, competencyrequests, connectionrequests, cvs, developments, goals, notes, issues, insights, pinboards, feedbacks, questions, discussions, resources, likes, comments, votes — most are not yet represented in any new microservice. The new microservices cover the green-field subset: identity, knowledge core, gamification, payments, realtime.
Migration Strategy
Per CLAUDE.md: “treat as legacy unless a task explicitly targets it.” New work goes to a services/<name>-service subrepo. The migration cadence is: pick a bounded context (e.g. goals), build a goal-service microservice, dual-write from the monolith, cut reads over, retire the monolith controller.
The shared MongoDB makes dual-write trivial but creates schema-drift risk: legacy *Document structs use *string pointers everywhere, new structs use values. Reconciliation must handle null vs empty-string explicitly.
Test Endpoint
GET /test on the legacy app returns {"data": "SOME COOL SERVER DATA"} (main.go:259-261). It is a smoke route, not a contract.
Key Terms
- Bounded context → the unit of extraction (one bounded context → one microservice).
- Dual-write → the migration period when both systems persist the same change.
access_tokencookie → legacy auth token; not the same wallet path as the gateway JWT.- Schema drift → divergent representations of the same Mongo collection across the two codebases.
Q&A
Q: A bug needs fixing in the goals feature. Where do I edit?
A: Today: services/api/controllers/goal.controller.go and services/api/models/goal.models.go — there is no goal-service yet. If the bug touches authentication or rate limiting, that is a gateway-side change instead.
Q: The legacy app declares both mux.Router and gin.Engine — which actually serves traffic?
A: Gin. The mux import is residual scaffolding; routes are registered on the Gin engine.
Q: Both stacks share Mongo. What stops the new user-service from corrupting the legacy users collection?
A: Discipline, plus the indexes (unique on email, handle, uid). The new struct must marshal compatibly with UserDocument; the safest path during migration is to read-only via the new service and write via the legacy one until cutover.
Examples
A new “goals stats” panel: read goals from legacy Mongo via a microservice that imports the legacy GoalDocument shape, but write any new aggregated fields to a side collection (e.g. goal_stats) so the legacy app keeps its schema clean.
neighbors on the map
- Two-Service Architecture onboarding to the chronicle-hq monorepo