-
-
Notifications
You must be signed in to change notification settings - Fork 46
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. |
|
Reduced cyclomatic complexity on all non-test functions to less than 9. |
…mplified place/sell into focused helpers.
* Add checkpoint for git commit timing experiment * Adding SRP explainer checkpoint. This brief description of the Single Responsibility Principle (SRP) is intended for "vibe coding" tools like ChatGPT Codex to read through and use as a basis for evaluating and generating code. * Codex evaluation checkpoint Codex evaluation of SRP adherence in CMS service.go * Evaluate SRP adherence Evaluating SRP adherence of `/internal/domain/users/service.go` using Codex * initial redesign for /users/service.go?
|
OK, starting project management for remaining tasks: SRP
|
OCP
|
LSP
|
ISP
|
DIP
|
|
I have a question about It's very short Is this violating SRP because different types of errors are combined into the same variable, or is it OK because it's one big "error" type that belongs to the Similarly for |
* SRP for bets, markets and math. * Updating with passed tests. * Updating SRP for financials, leaderboard, analytics * Simplifying positionsmath, valuation, wpam * SRP on selldusst calculator. --------- Co-authored-by: Patrick W. Delaney <[email protected]>
In Go, it’s common (and encouraged) to have a structure like this: Go documentation typically doesn’t frame guidance in SOLID terms; it frames it as:
That said, there’s overlap in spirit with SRP, small packages/files by responsibility ... errors.go is “one reason to change: the package error contract”. Hypothetically if someone changes this errors.go, there will be one reason for it, basically the user contract changed. So while there are multiple things going on in this one file, it's all centered around the, "thing," ... user. Some of this is opinionated. You could hypothetically make a file for each one of those errors to make it ultra-super SRP but I think that's over-engineering. |
|
@pwdel thanks, just wanted to check with you - I'll focus on |
* Updating OCP for bets * Refactoring markets for OCP * Expanded dust.go to allow custom sell dust policies, include ProbabilityProvider, PayoutModel, PositionCalculator to encapsulate strategies, existing APIs now delegate through calculator, default WPAM-DBPM implementations wired for new interfaces. * Added MarketPositionCalculator interface with WPAM DBPM implementation, wired strategy injection into both analytics Service, WithPositionCalculator option, and GormRepositoroy, default calculator, nil-safety, Refactored leaderboard data loading --------- Co-authored-by: Patrick W. Delaney <[email protected]>
* Exported sale calculation data and switched to SaleCalculator.Calculate to return it, updating Sell flow, added interface conformance assertions for sergice and hardened validator dependency handling so alternate implemntnations cant trigger panics or rely on stronger preconditions. * Applying liskov substitution. Default clock implementation, guard repo fetches against nil markets and invalid ids, normalize search and overview helpers to handle nil slices. * Removed panic on missing seeds, getSeeds now supplies and defaults, PositionCalculator toleratoes nil dependencies by falling back to default probability / payout models, preventing panics from zero value calculators. * Added LSP safeguards, service entrypoints against missing dependencies, ComputeUserFinancials and ComputeSystemMetrics, service construction seeds default strategies via ensureStrategyDefaults, interface assertions, Gorm repo methods now fail fast with descriptive error. --------- Co-authored-by: Patrick W. Delaney <[email protected]>
…sitory, MarketBetRepository and update resolution policy and helpers. (#613) Co-authored-by: Patrick W. Delaney <[email protected]>
* Updating. * Updating. * Updating. --------- Co-authored-by: Patrick W. Delaney <[email protected]>
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