-
-
Notifications
You must be signed in to change notification settings - Fork 29
Refactoring Handlers and Business Logic, Middleware into Service Architecture #581
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
Migration Plan for Migrating to Microservices: 6-step extraction plan (no big-bang) 1) Freeze the interfaceMake sure your domain service interface is what other code depends on (not concrete types): Keep callers elsewhere (bets, positions, stats) talking to this interface. 2) Publish a contract (OpenAPI)Document the HTTP endpoints the markets-api will expose. You can mirror your existing routes or evolve them slightly. Place it at README/BACKEND/API/markets/openapi.yaml. This spec lets you: Validate server and client. Generate a client adapter for the monolith (go/types). Minimal example (sketch): 3) Create the new service processNew directories in a separate repo or under services/markets: Server skeleton (reuses your handler package or a slimmed copy): Dockerfile (simple): 4) Add an HTTP client adapter to the monolithImplement the same Service interface but backed by HTTP calls to markets-api. Put it here: Example: You can generate this client from your OpenAPI spec and wrap it to return your domain models—very little code. 5) Toggle via configIn your internal/app/container.go (monolith), add a flag to pick local vs. remote: Local (default): monolith behaves as now. Remote: monolith calls the external markets-api. No calling code changes—still uses the service interface. 6) Deploy side-by-side (strangler pattern)Run the new markets-api (port 8081). Flip monolith’s env: MARKETS_MODE=remote and MARKETS_BASE_URL=http://markets-api:8081. Verify endpoints through the monolith still work. When stable, stop including the local repo path in the monolith build, or remove it later. Cross-service concerns (add once, reap forever)DB per service: markets-api owns the markets DB tables. No other service writes them. Auth: monolith forwards JWT to markets-api (shared public key). Verify in both. Idempotency: write endpoints (POST /markets, POST /resolve) accept an Idempotency-Key; server dedups. Timeouts & retries: client uses timeouts + limited retries with jitter. Observability: propagate Traceparent/X-Request-ID; log JSON; emit RED metrics; add /health and /ready. Backpressure: paginate list/search; document limits in OpenAPI. Contracts in CI: validate openapi.yaml; run client/server contract tests; block breaking changes. Events (optional): if others react to market changes, publish domain events (outbox) to a bus (Kafka/NATS) when markets change. What changes for other modules (bets, positions, users, stats)Repeat the exact pattern: Make each a clean slice (handler → domain → repository). Publish an OpenAPI per service. Extract to services/ with its own Dockerfile and DB. In the monolith, replace local repo/service with HTTP client that implements the same interface you established during the refactor. Toggle with config; migrate one domain at a time. TL;DR (why this works with minimal churn) All callers depend on the Service interface you already defined. Extraction = swap the implementation, not rewrite the callers. OpenAPI + generated client keeps the HTTP plumbing small and safe. Config flag lets you migrate & rollback safely. |
…njection in the server.
|
Ok so, part of what I'm learning here is ... for anything we migrate over to a service model, the remainder of any endpoints that are entangled with that monolithic service also have to migrated, or if not, at least there has to be some kind of adapter in order for everything to work properly as the app had previously. |
|
testing on localhost...
this is an unclear error message when trying to bet on a resolved market, but this is a nitpick
overall, looks good to me, happy to approve if you have tested this on kconfs @pwdel |
No, I have never seen this. Odd. @ntoufoudis do you get this on the branch we're discussing? |
63e91a3 to
663b137
Compare
|
There so much here, rather than producing a full review, I'll make some general comments here. When I review another engineer's work, I try to look at everything as holistically as I can, using standard development Best Practices as my polestar. I look specifically for things like
The goal of this evaluation is to help make the codebase:
Note that of the worst offenders listed below, most of the files/functions appear in multiple categories. Cyclomatic ComplexityAn old but I feel still pertinent measure of how many paths there are out of a function/method/etc. I view anything above 8 to be unnecessarily complex. (In my own work, I strive for 4 or less.) Worst offenders:
Single Responsibility Principle"A function/module/etc. should only have one reason to change." The example I use to describe this is a function that retrieves data from some source, transforms it into printable form, and sends it to a printer. In this function, there are many "reasons to change": if
Three reasons to change, in one function. (This is related to Open/Closed below.) There are lots of violations, but the worst offenders are:
Open/Closed Principle"A function/module/etc. should be open to extension but closed to modification." Any time you need to go back into the code to fix a bug, without strict adherence to this principle, you will cause more bugs to pop up based on your modifications to existing code. (This is another instance of "coupling".) The best way to ameliorate this problem is to "lock" existing work and use extension to apply modifications. The worst offenders:
Other Violations of SOLID Principles
Comments on Coupling/CohesionCoupling is how strongly one module depends on another’s details. High coupling means changes in one piece force changes elsewhere (shared state, concrete types, hidden assumptions); low coupling keeps modules talking through stable abstractions so they can evolve independently. Cohesion is how tightly the responsibilities within a module relate to a single purpose. High cohesion means the code in a unit works toward one focused task; low cohesion means a grab bag of unrelated duties living together, making the unit harder to understand, test, and change. Good engineers alway strive for Low Coupling and High Cohesion. |
|
Working on Cyclomatic Complexity. I found that there's a Go tool for this. Here's my current report for 5 or more. |
|
In pure functional programming, every function has only one entry and one exit (return). Shoot for complexity of 8 or less to start. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
docs/openapi.yaml is missing five routes:
GET /healthGET /v0/setup/frontendGET /v0/global/leaderboardGET /v0/markets/status/{status}- see note below.GET /v0/markets/{id}/projection
And still documents four legacy shortcuts:
GET /v0/markets/activeGET /v0/markets/closedGET /v0/markets/resolvedGET /v0/marketprojection/{marketId}/{amount}/{outcome}- the code now uses
GET /v0/markets/{id}/projectioninstead
- the code now uses
Note:
Re: GET /v0/markets/status/{status} - accepts status values of "active", "closed", "resolved", "all" but the underlying code accepts "" as well. If this is intended, you should also have a GET /v0/markets/status route as well and drop the "all" status.





BACKGROUND
handlers → domain service → repositoryis will allow us to liftmarkets/into its own microservice with minimal churn.HIGH LEVEL OVERVIEW
Markets Flow (new layered architecture)
flowchart TB client["Client"] handlers["handlers/markets/*.go<br/>• JSON ⇄ DTO mapping<br/>• call service interface"] dto["handlers/markets/dto<br/>• DTO definitions"] container["internal/app/container<br/>• wires handler → service → repository"] domain["internal/domain/markets<br/>• business logic<br/>• validation & orchestration"] repo["internal/repository/markets<br/>• persistence (GORM)"] db["Database"] client --> handlers handlers --> dto dto --> container container --> domain domain --> repo repo --> dbMarkets Flow (Legacy Architecture)
flowchart TB client["Client"] handlers["handlers/bets/*.go<br/>• request parsing<br/>• business logic<br/>• direct util.GetDB access"] db["Database"] client --> handlers --> dbExecution Plan
Execution Plan for Migration to Microservices
💡 ARCHITECTURAL STRUCTURE - PER DIRECTORY, PART OF THE CODE
OVERVIEW:
Microservices Readiness:
The architecture now supports easy service extraction:
📋 COMPLETION GUIDE
For each remaining handler file, follow this proven pattern:
Import Changes:
Handler Signature:
Logic Movement:
EXAMPLE
createmarket.goPATTERN✅ FULLY COMPLETED HANDLER
createmarket.go- COMPLETE REFACTOR ✅dto.CreateMarketRequest→dto.CreateMarketResponsedmarkets.Service.CreateMarket()GUARD CHECK PROGRESS
🏗️ ARCHITECTURAL PATTERN PROVEN
Complete Clean Handler Pattern (createmarket.go):
1. HTTP-Only Handler:
2. Domain Service (business logic):
3. Repository (data access):
FILES STATUS
MARKETS
svc.List/ListByStatuswith DTO mappingsvc.ListMarketsfor backward compatProjectProbabilitycomputes real projections via WPAM mathUSERS
internal/domain/users.Service.ApplyTransactionvia handlers/servicesutil.GetDBdirectly.svc.UpdateDescription; auth via auth façadesvc.UpdateDisplayName; auth via auth façadesvc.UpdateEmoji; auth via auth façadesvc.ChangePassword; hashing handled in domainsvc.UpdatePersonalLinks; auth via auth façadesvc.GetUserFinancialssvc.ListUserMarketssvc.GetPublicUsersvc.GetUserPortfoliosvc.GetUserCreditwith missing-user fallbackFINANCIALS and MATH
internal/domain/analytics.Service.ComputeUserFinancials; metrics + user endpoints consume itanalytics.Service.ComputeSystemMetrics; metrics handler injects analytics serviceinternal/domain/math/market; buy/sell flows reference domain service wrappersinternal/domain/math/outcomes/dbpm; consumed via positions math serviceinternal/domain/markets.Service.ResolveMarketBETS
MIDDLEWARE
Authenticatorinterface andAuthServicein placeAuthServicefacadeAuthServiceAuthServicefor any new handlersinternal/app)AuthService; handlers receive it via DIinternal/service/auth; mixed responsibilities resolvedOpenAPI Update
server.godocs/openapi.yaml