CRUMB a card from devarno-cloud

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 --> Mongo

Differences at a Glance

AspectLegacy services/apiMicroservices
Entrypointmain.go (no cmd/)cmd/server/main.go
Layouttop-level controllers/, routes/, models/, middleware/internal/handlers, internal/models, internal/config
Routermux.Router + gin.Engine (both declared)Gin only
CORSrs/cors with GET, POST, PUT, DELETEGateway-side CORS only
Authmiddleware.AuthenticateUser() reading access_token cookie or Authorization: Bearer, calls utils.ValidateToken() locallyJWT verified once at gateway; X-User-ID / X-User-Tier forwarded
Identity injectionc.Set("user_id", ...) from local validationtrusted gateway header
Routesos.Getenv("API_VERSION") prefix + 22 route groupsper-service /api/v1/... paths
Collections23 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_token cookie → 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