From c03ae3f0f0d5fa6fd0d4472d51bd04b9a92933dc Mon Sep 17 00:00:00 2001 From: oabrivard Date: Fri, 6 Feb 2026 11:37:11 +0100 Subject: [PATCH] Refactor backend code to remove duplicate functions and factorize service startup --- backend/services/admin-service/cmd/main.go | 19 +++--- backend/services/admin-service/go.mod | 5 +- .../services/game-session-service/cmd/main.go | 19 +++--- backend/services/game-session-service/go.mod | 5 +- backend/services/gateway-service/cmd/main.go | 19 +++--- backend/services/gateway-service/go.mod | 5 +- .../services/leaderboard-service/cmd/main.go | 19 +++--- backend/services/leaderboard-service/go.mod | 5 +- .../question-bank-service/cmd/main.go | 19 +++--- backend/services/question-bank-service/go.mod | 5 +- backend/services/user-service/cmd/main.go | 19 +++--- backend/services/user-service/go.mod | 5 +- .../shared/infra/database/postgres/client.go | 59 ++++--------------- backend/shared/infra/database/redis/client.go | 55 ++++------------- backend/shared/infra/utils/envutil/env.go | 46 +++++++++++++++ .../shared/infra/utils/envutil/env_test.go | 38 ++++++++++++ .../shared/infra/utils/serviceboot/fiber.go | 52 ++++++++++++++++ .../infra/utils/serviceboot/fiber_test.go | 40 +++++++++++++ docs/3_guidelines/development-guidelines.md | 4 ++ .../1.2-shared-backend-packages.md | 16 ++--- 20 files changed, 301 insertions(+), 153 deletions(-) create mode 100644 backend/shared/infra/utils/envutil/env.go create mode 100644 backend/shared/infra/utils/envutil/env_test.go create mode 100644 backend/shared/infra/utils/serviceboot/fiber.go create mode 100644 backend/shared/infra/utils/serviceboot/fiber_test.go diff --git a/backend/services/admin-service/cmd/main.go b/backend/services/admin-service/cmd/main.go index a02fb69..a4b916a 100644 --- a/backend/services/admin-service/cmd/main.go +++ b/backend/services/admin-service/cmd/main.go @@ -3,17 +3,20 @@ package main import ( "log" - "github.com/gofiber/fiber/v3" + "knowfoolery/backend/shared/infra/utils/serviceboot" ) func main() { - app := fiber.New(fiber.Config{ - AppName: "Know Foolery - Admin Service", - }) + cfg := serviceboot.Config{ + AppName: "Know Foolery - Admin Service", + ServiceSlug: "admin", + PortEnv: "ADMIN_SERVICE_PORT", + DefaultPort: 8085, + } - app.Get("/health", func(c fiber.Ctx) error { - return c.JSON(fiber.Map{"status": "healthy", "service": "admin"}) - }) + app := serviceboot.NewFiberApp(cfg) + serviceboot.RegisterHealth(app, cfg.ServiceSlug) - log.Fatal(app.Listen(":8085")) + addr := serviceboot.ListenAddress(cfg.PortEnv, cfg.DefaultPort) + log.Fatal(serviceboot.Run(app, addr)) } diff --git a/backend/services/admin-service/go.mod b/backend/services/admin-service/go.mod index c04c702..229d6bc 100644 --- a/backend/services/admin-service/go.mod +++ b/backend/services/admin-service/go.mod @@ -2,7 +2,10 @@ module knowfoolery/backend/services/admin-service go 1.25.5 -require github.com/gofiber/fiber/v3 v3.0.0-beta.3 +require ( + github.com/gofiber/fiber/v3 v3.0.0-beta.3 + knowfoolery/backend/shared v0.0.0 +) require ( github.com/andybalholm/brotli v1.1.0 // indirect diff --git a/backend/services/game-session-service/cmd/main.go b/backend/services/game-session-service/cmd/main.go index e91aef4..a02ddbe 100644 --- a/backend/services/game-session-service/cmd/main.go +++ b/backend/services/game-session-service/cmd/main.go @@ -3,17 +3,20 @@ package main import ( "log" - "github.com/gofiber/fiber/v3" + "knowfoolery/backend/shared/infra/utils/serviceboot" ) func main() { - app := fiber.New(fiber.Config{ - AppName: "Know Foolery - Game Session Service", - }) + cfg := serviceboot.Config{ + AppName: "Know Foolery - Game Session Service", + ServiceSlug: "game-session", + PortEnv: "GAME_SESSION_PORT", + DefaultPort: 8080, + } - app.Get("/health", func(c fiber.Ctx) error { - return c.JSON(fiber.Map{"status": "healthy", "service": "game-session"}) - }) + app := serviceboot.NewFiberApp(cfg) + serviceboot.RegisterHealth(app, cfg.ServiceSlug) - log.Fatal(app.Listen(":8080")) + addr := serviceboot.ListenAddress(cfg.PortEnv, cfg.DefaultPort) + log.Fatal(serviceboot.Run(app, addr)) } diff --git a/backend/services/game-session-service/go.mod b/backend/services/game-session-service/go.mod index dfac2ba..8cb1aaa 100644 --- a/backend/services/game-session-service/go.mod +++ b/backend/services/game-session-service/go.mod @@ -2,7 +2,10 @@ module knowfoolery/backend/services/game-session-service go 1.25.5 -require github.com/gofiber/fiber/v3 v3.0.0-beta.3 +require ( + github.com/gofiber/fiber/v3 v3.0.0-beta.3 + knowfoolery/backend/shared v0.0.0 +) require ( github.com/andybalholm/brotli v1.1.0 // indirect diff --git a/backend/services/gateway-service/cmd/main.go b/backend/services/gateway-service/cmd/main.go index 3a8cbe9..0d48066 100644 --- a/backend/services/gateway-service/cmd/main.go +++ b/backend/services/gateway-service/cmd/main.go @@ -3,17 +3,20 @@ package main import ( "log" - "github.com/gofiber/fiber/v3" + "knowfoolery/backend/shared/infra/utils/serviceboot" ) func main() { - app := fiber.New(fiber.Config{ - AppName: "Know Foolery - Gateway Service", - }) + cfg := serviceboot.Config{ + AppName: "Know Foolery - Gateway Service", + ServiceSlug: "gateway", + PortEnv: "GATEWAY_PORT", + DefaultPort: 8086, + } - app.Get("/health", func(c fiber.Ctx) error { - return c.JSON(fiber.Map{"status": "healthy", "service": "gateway"}) - }) + app := serviceboot.NewFiberApp(cfg) + serviceboot.RegisterHealth(app, cfg.ServiceSlug) - log.Fatal(app.Listen(":8086")) + addr := serviceboot.ListenAddress(cfg.PortEnv, cfg.DefaultPort) + log.Fatal(serviceboot.Run(app, addr)) } diff --git a/backend/services/gateway-service/go.mod b/backend/services/gateway-service/go.mod index 5630949..629118c 100644 --- a/backend/services/gateway-service/go.mod +++ b/backend/services/gateway-service/go.mod @@ -2,7 +2,10 @@ module knowfoolery/backend/services/gateway-service go 1.25.5 -require github.com/gofiber/fiber/v3 v3.0.0-beta.3 +require ( + github.com/gofiber/fiber/v3 v3.0.0-beta.3 + knowfoolery/backend/shared v0.0.0 +) require ( github.com/andybalholm/brotli v1.1.0 // indirect diff --git a/backend/services/leaderboard-service/cmd/main.go b/backend/services/leaderboard-service/cmd/main.go index 7ce9538..93d023e 100644 --- a/backend/services/leaderboard-service/cmd/main.go +++ b/backend/services/leaderboard-service/cmd/main.go @@ -3,17 +3,20 @@ package main import ( "log" - "github.com/gofiber/fiber/v3" + "knowfoolery/backend/shared/infra/utils/serviceboot" ) func main() { - app := fiber.New(fiber.Config{ - AppName: "Know Foolery - Leaderboard Service", - }) + cfg := serviceboot.Config{ + AppName: "Know Foolery - Leaderboard Service", + ServiceSlug: "leaderboard", + PortEnv: "LEADERBOARD_PORT", + DefaultPort: 8083, + } - app.Get("/health", func(c fiber.Ctx) error { - return c.JSON(fiber.Map{"status": "healthy", "service": "leaderboard"}) - }) + app := serviceboot.NewFiberApp(cfg) + serviceboot.RegisterHealth(app, cfg.ServiceSlug) - log.Fatal(app.Listen(":8083")) + addr := serviceboot.ListenAddress(cfg.PortEnv, cfg.DefaultPort) + log.Fatal(serviceboot.Run(app, addr)) } diff --git a/backend/services/leaderboard-service/go.mod b/backend/services/leaderboard-service/go.mod index f0ddea4..a853eeb 100644 --- a/backend/services/leaderboard-service/go.mod +++ b/backend/services/leaderboard-service/go.mod @@ -2,7 +2,10 @@ module knowfoolery/backend/services/leaderboard-service go 1.25.5 -require github.com/gofiber/fiber/v3 v3.0.0-beta.3 +require ( + github.com/gofiber/fiber/v3 v3.0.0-beta.3 + knowfoolery/backend/shared v0.0.0 +) require ( github.com/andybalholm/brotli v1.1.0 // indirect diff --git a/backend/services/question-bank-service/cmd/main.go b/backend/services/question-bank-service/cmd/main.go index 646716b..59fe662 100644 --- a/backend/services/question-bank-service/cmd/main.go +++ b/backend/services/question-bank-service/cmd/main.go @@ -3,17 +3,20 @@ package main import ( "log" - "github.com/gofiber/fiber/v3" + "knowfoolery/backend/shared/infra/utils/serviceboot" ) func main() { - app := fiber.New(fiber.Config{ - AppName: "Know Foolery - Question Bank Service", - }) + cfg := serviceboot.Config{ + AppName: "Know Foolery - Question Bank Service", + ServiceSlug: "question-bank", + PortEnv: "QUESTION_BANK_PORT", + DefaultPort: 8081, + } - app.Get("/health", func(c fiber.Ctx) error { - return c.JSON(fiber.Map{"status": "healthy", "service": "question-bank"}) - }) + app := serviceboot.NewFiberApp(cfg) + serviceboot.RegisterHealth(app, cfg.ServiceSlug) - log.Fatal(app.Listen(":8081")) + addr := serviceboot.ListenAddress(cfg.PortEnv, cfg.DefaultPort) + log.Fatal(serviceboot.Run(app, addr)) } diff --git a/backend/services/question-bank-service/go.mod b/backend/services/question-bank-service/go.mod index 3281976..333c0bd 100644 --- a/backend/services/question-bank-service/go.mod +++ b/backend/services/question-bank-service/go.mod @@ -2,7 +2,10 @@ module knowfoolery/backend/services/question-bank-service go 1.25.5 -require github.com/gofiber/fiber/v3 v3.0.0-beta.3 +require ( + github.com/gofiber/fiber/v3 v3.0.0-beta.3 + knowfoolery/backend/shared v0.0.0 +) require ( github.com/andybalholm/brotli v1.1.0 // indirect diff --git a/backend/services/user-service/cmd/main.go b/backend/services/user-service/cmd/main.go index 529929b..0f36ac5 100644 --- a/backend/services/user-service/cmd/main.go +++ b/backend/services/user-service/cmd/main.go @@ -3,17 +3,20 @@ package main import ( "log" - "github.com/gofiber/fiber/v3" + "knowfoolery/backend/shared/infra/utils/serviceboot" ) func main() { - app := fiber.New(fiber.Config{ - AppName: "Know Foolery - User Service", - }) + cfg := serviceboot.Config{ + AppName: "Know Foolery - User Service", + ServiceSlug: "user", + PortEnv: "USER_SERVICE_PORT", + DefaultPort: 8082, + } - app.Get("/health", func(c fiber.Ctx) error { - return c.JSON(fiber.Map{"status": "healthy", "service": "user"}) - }) + app := serviceboot.NewFiberApp(cfg) + serviceboot.RegisterHealth(app, cfg.ServiceSlug) - log.Fatal(app.Listen(":8082")) + addr := serviceboot.ListenAddress(cfg.PortEnv, cfg.DefaultPort) + log.Fatal(serviceboot.Run(app, addr)) } diff --git a/backend/services/user-service/go.mod b/backend/services/user-service/go.mod index 7439712..937de81 100644 --- a/backend/services/user-service/go.mod +++ b/backend/services/user-service/go.mod @@ -2,7 +2,10 @@ module knowfoolery/backend/services/user-service go 1.25.5 -require github.com/gofiber/fiber/v3 v3.0.0-beta.3 +require ( + github.com/gofiber/fiber/v3 v3.0.0-beta.3 + knowfoolery/backend/shared v0.0.0 +) require ( github.com/andybalholm/brotli v1.1.0 // indirect diff --git a/backend/shared/infra/database/postgres/client.go b/backend/shared/infra/database/postgres/client.go index dc6c194..c96c573 100644 --- a/backend/shared/infra/database/postgres/client.go +++ b/backend/shared/infra/database/postgres/client.go @@ -5,11 +5,11 @@ import ( "context" "errors" "fmt" - "os" - "strconv" "time" "github.com/jackc/pgx/v5/pgxpool" + + "knowfoolery/backend/shared/infra/utils/envutil" ) // Config holds the configuration for the PostgreSQL client. @@ -95,17 +95,17 @@ func HealthCheck(ctx context.Context, config Config) error { func ConfigFromEnv() Config { cfg := DefaultConfig() - cfg.Host = getenvString("POSTGRES_HOST", cfg.Host) - cfg.Port = getenvInt("POSTGRES_PORT", cfg.Port) - cfg.User = getenvString("POSTGRES_USER", cfg.User) - cfg.Password = getenvString("POSTGRES_PASSWORD", cfg.Password) - cfg.Database = getenvString("POSTGRES_DB", cfg.Database) - cfg.SSLMode = getenvString("POSTGRES_SSLMODE", cfg.SSLMode) + cfg.Host = envutil.String("POSTGRES_HOST", cfg.Host) + cfg.Port = envutil.Int("POSTGRES_PORT", cfg.Port) + cfg.User = envutil.String("POSTGRES_USER", cfg.User) + cfg.Password = envutil.String("POSTGRES_PASSWORD", cfg.Password) + cfg.Database = envutil.String("POSTGRES_DB", cfg.Database) + cfg.SSLMode = envutil.String("POSTGRES_SSLMODE", cfg.SSLMode) - cfg.MaxOpenConns = getenvInt("POSTGRES_MAX_OPEN_CONNS", cfg.MaxOpenConns) - cfg.MaxIdleConns = getenvInt("POSTGRES_MAX_IDLE_CONNS", cfg.MaxIdleConns) - cfg.ConnMaxLifetime = getenvDuration("POSTGRES_CONN_MAX_LIFETIME", cfg.ConnMaxLifetime) - cfg.ConnMaxIdleTime = getenvDuration("POSTGRES_CONN_MAX_IDLE_TIME", cfg.ConnMaxIdleTime) + cfg.MaxOpenConns = envutil.Int("POSTGRES_MAX_OPEN_CONNS", cfg.MaxOpenConns) + cfg.MaxIdleConns = envutil.Int("POSTGRES_MAX_IDLE_CONNS", cfg.MaxIdleConns) + cfg.ConnMaxLifetime = envutil.Duration("POSTGRES_CONN_MAX_LIFETIME", cfg.ConnMaxLifetime) + cfg.ConnMaxIdleTime = envutil.Duration("POSTGRES_CONN_MAX_IDLE_TIME", cfg.ConnMaxIdleTime) return cfg } @@ -123,38 +123,3 @@ func validateConfig(c Config) error { } return nil } - -func getenvString(key string, fallback string) string { - if v := os.Getenv(key); v != "" { - return v - } - return fallback -} - -func getenvInt(key string, fallback int) int { - v := os.Getenv(key) - if v == "" { - return fallback - } - - i, err := strconv.Atoi(v) - if err != nil { - return fallback - } - - return i -} - -func getenvDuration(key string, fallback time.Duration) time.Duration { - v := os.Getenv(key) - if v == "" { - return fallback - } - - d, err := time.ParseDuration(v) - if err != nil { - return fallback - } - - return d -} diff --git a/backend/shared/infra/database/redis/client.go b/backend/shared/infra/database/redis/client.go index c5a8396..9fe4bd7 100644 --- a/backend/shared/infra/database/redis/client.go +++ b/backend/shared/infra/database/redis/client.go @@ -5,11 +5,11 @@ import ( "context" "errors" "fmt" - "os" - "strconv" "time" redisv9 "github.com/redis/go-redis/v9" + + "knowfoolery/backend/shared/infra/utils/envutil" ) // Config holds the configuration for the Redis client. @@ -131,15 +131,15 @@ func (c *Client) Expire(ctx context.Context, key string, expiration time.Duratio func ConfigFromEnv() Config { cfg := DefaultConfig() - cfg.Host = getenvString("REDIS_HOST", cfg.Host) - cfg.Port = getenvInt("REDIS_PORT", cfg.Port) - cfg.Password = getenvString("REDIS_PASSWORD", cfg.Password) - cfg.DB = getenvInt("REDIS_DB", cfg.DB) - cfg.PoolSize = getenvInt("REDIS_POOL_SIZE", cfg.PoolSize) - cfg.MinIdleConns = getenvInt("REDIS_MIN_IDLE_CONNS", cfg.MinIdleConns) - cfg.DialTimeout = getenvDuration("REDIS_DIAL_TIMEOUT", cfg.DialTimeout) - cfg.ReadTimeout = getenvDuration("REDIS_READ_TIMEOUT", cfg.ReadTimeout) - cfg.WriteTimeout = getenvDuration("REDIS_WRITE_TIMEOUT", cfg.WriteTimeout) + cfg.Host = envutil.String("REDIS_HOST", cfg.Host) + cfg.Port = envutil.Int("REDIS_PORT", cfg.Port) + cfg.Password = envutil.String("REDIS_PASSWORD", cfg.Password) + cfg.DB = envutil.Int("REDIS_DB", cfg.DB) + cfg.PoolSize = envutil.Int("REDIS_POOL_SIZE", cfg.PoolSize) + cfg.MinIdleConns = envutil.Int("REDIS_MIN_IDLE_CONNS", cfg.MinIdleConns) + cfg.DialTimeout = envutil.Duration("REDIS_DIAL_TIMEOUT", cfg.DialTimeout) + cfg.ReadTimeout = envutil.Duration("REDIS_READ_TIMEOUT", cfg.ReadTimeout) + cfg.WriteTimeout = envutil.Duration("REDIS_WRITE_TIMEOUT", cfg.WriteTimeout) return cfg } @@ -163,36 +163,3 @@ func validateConfig(c Config) error { } return nil } - -func getenvString(key string, fallback string) string { - if v := os.Getenv(key); v != "" { - return v - } - return fallback -} - -func getenvInt(key string, fallback int) int { - v := os.Getenv(key) - if v == "" { - return fallback - } - - i, err := strconv.Atoi(v) - if err != nil { - return fallback - } - return i -} - -func getenvDuration(key string, fallback time.Duration) time.Duration { - v := os.Getenv(key) - if v == "" { - return fallback - } - - d, err := time.ParseDuration(v) - if err != nil { - return fallback - } - return d -} diff --git a/backend/shared/infra/utils/envutil/env.go b/backend/shared/infra/utils/envutil/env.go new file mode 100644 index 0000000..95f1aa9 --- /dev/null +++ b/backend/shared/infra/utils/envutil/env.go @@ -0,0 +1,46 @@ +// Package envutil provides environment variable parsing helpers with fallback values. +package envutil + +import ( + "os" + "strconv" + "time" +) + +// String returns the environment variable value or fallback when empty. +func String(key string, fallback string) string { + if v := os.Getenv(key); v != "" { + return v + } + return fallback +} + +// Int returns the parsed integer environment variable or fallback on parse error. +func Int(key string, fallback int) int { + v := os.Getenv(key) + if v == "" { + return fallback + } + + i, err := strconv.Atoi(v) + if err != nil { + return fallback + } + + return i +} + +// Duration returns the parsed duration environment variable or fallback on parse error. +func Duration(key string, fallback time.Duration) time.Duration { + v := os.Getenv(key) + if v == "" { + return fallback + } + + d, err := time.ParseDuration(v) + if err != nil { + return fallback + } + + return d +} diff --git a/backend/shared/infra/utils/envutil/env_test.go b/backend/shared/infra/utils/envutil/env_test.go new file mode 100644 index 0000000..741fbae --- /dev/null +++ b/backend/shared/infra/utils/envutil/env_test.go @@ -0,0 +1,38 @@ +package envutil + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestString(t *testing.T) { + t.Setenv("APP_VALUE", "configured") + require.Equal(t, "configured", String("APP_VALUE", "fallback")) +} + +func TestStringFallback(t *testing.T) { + t.Setenv("APP_VALUE", "") + require.Equal(t, "fallback", String("APP_VALUE", "fallback")) +} + +func TestInt(t *testing.T) { + t.Setenv("APP_PORT", "8080") + require.Equal(t, 8080, Int("APP_PORT", 3000)) +} + +func TestIntFallbackInvalid(t *testing.T) { + t.Setenv("APP_PORT", "oops") + require.Equal(t, 3000, Int("APP_PORT", 3000)) +} + +func TestDuration(t *testing.T) { + t.Setenv("APP_TIMEOUT", "7s") + require.Equal(t, 7*time.Second, Duration("APP_TIMEOUT", time.Second)) +} + +func TestDurationFallbackInvalid(t *testing.T) { + t.Setenv("APP_TIMEOUT", "oops") + require.Equal(t, 2*time.Second, Duration("APP_TIMEOUT", 2*time.Second)) +} diff --git a/backend/shared/infra/utils/serviceboot/fiber.go b/backend/shared/infra/utils/serviceboot/fiber.go new file mode 100644 index 0000000..818c479 --- /dev/null +++ b/backend/shared/infra/utils/serviceboot/fiber.go @@ -0,0 +1,52 @@ +// Package serviceboot provides shared service bootstrap helpers. +package serviceboot + +import ( + "fmt" + + "github.com/gofiber/fiber/v3" + + "knowfoolery/backend/shared/infra/utils/envutil" +) + +// Config defines basic service runtime settings. +type Config struct { + AppName string + ServiceSlug string + PortEnv string + DefaultPort int +} + +// NewFiberApp creates a Fiber app with shared defaults. +func NewFiberApp(cfg Config) *fiber.App { + return fiber.New(fiber.Config{ + AppName: cfg.AppName, + }) +} + +// RegisterHealth registers a standard health endpoint. +func RegisterHealth(app *fiber.App, serviceSlug string) { + app.Get("/health", func(c fiber.Ctx) error { + return c.JSON(fiber.Map{ + "status": "healthy", + "service": serviceSlug, + }) + }) +} + +// ListenAddress resolves the listen port from environment with fallback. +func ListenAddress(portEnv string, defaultPort int) string { + port := envutil.Int(portEnv, defaultPort) + if port < 1 || port > 65535 { + port = defaultPort + } + if port < 1 || port > 65535 { + port = 8080 + } + return fmt.Sprintf(":%d", port) +} + +// Run starts the Fiber app. +func Run(app *fiber.App, addr string) error { + return app.Listen(addr) +} diff --git a/backend/shared/infra/utils/serviceboot/fiber_test.go b/backend/shared/infra/utils/serviceboot/fiber_test.go new file mode 100644 index 0000000..c32de88 --- /dev/null +++ b/backend/shared/infra/utils/serviceboot/fiber_test.go @@ -0,0 +1,40 @@ +package serviceboot + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestRegisterHealth(t *testing.T) { + app := NewFiberApp(Config{AppName: "test-service"}) + RegisterHealth(app, "svc") + + req := httptest.NewRequest(http.MethodGet, "/health", nil) + resp, err := app.Test(req) + require.NoError(t, err) + defer resp.Body.Close() + + var body map[string]string + require.NoError(t, json.NewDecoder(resp.Body).Decode(&body)) + require.Equal(t, "healthy", body["status"]) + require.Equal(t, "svc", body["service"]) +} + +func TestListenAddressFromEnv(t *testing.T) { + t.Setenv("SERVICE_PORT", "9090") + require.Equal(t, ":9090", ListenAddress("SERVICE_PORT", 8080)) +} + +func TestListenAddressFallback(t *testing.T) { + t.Setenv("SERVICE_PORT", "bad") + require.Equal(t, ":8080", ListenAddress("SERVICE_PORT", 8080)) +} + +func TestListenAddressOutOfRangeFallback(t *testing.T) { + t.Setenv("SERVICE_PORT", "70000") + require.Equal(t, ":8080", ListenAddress("SERVICE_PORT", 8080)) +} diff --git a/docs/3_guidelines/development-guidelines.md b/docs/3_guidelines/development-guidelines.md index 3d2464c..fb14357 100644 --- a/docs/3_guidelines/development-guidelines.md +++ b/docs/3_guidelines/development-guidelines.md @@ -973,6 +973,10 @@ cd backend/services/{service-name} # Start service in development mode with hot reload go run cmd/main.go +# Service ports are env-driven via shared bootstrap helpers: +# ADMIN_SERVICE_PORT, GAME_SESSION_PORT, GATEWAY_PORT, +# LEADERBOARD_PORT, QUESTION_BANK_PORT, USER_SERVICE_PORT + # Run tests for a specific service go test ./... -v diff --git a/docs/4_work_plan/1.2-shared-backend-packages.md b/docs/4_work_plan/1.2-shared-backend-packages.md index 21af302..419aabd 100644 --- a/docs/4_work_plan/1.2-shared-backend-packages.md +++ b/docs/4_work_plan/1.2-shared-backend-packages.md @@ -168,11 +168,11 @@ backend/shared/ - `backend/shared/infra/database/redis/client.go` **Postgres implementation:** -- Add dependency: `github.com/jackc/pgx/v5/pgxpool` -- Replace placeholder client with wrapper around `*pgxpool.Pool` -- Implement `NewClient(ctx, cfg)` with config validation -- Implement `Ping`, `Close`, `HealthCheck` +- Keep shared package minimal: config/env parsing and connection string helpers - Implement `ConfigFromEnv` using `os.Getenv` with defaults +- Add lightweight `HealthCheck` helper for connectivity validation +- Do **not** introduce a shared runtime Postgres data client +- Each service initializes and manages Ent directly for Postgres access **Redis implementation:** - Add dependency: `github.com/redis/go-redis/v9` @@ -181,8 +181,8 @@ backend/shared/ - Implement `ConfigFromEnv` using `os.Getenv` with defaults **Acceptance criteria:** -- Clients connect successfully via `ConfigFromEnv` -- Health checks are real and return errors on failure +- Postgres helper is minimal and does not duplicate Ent responsibilities +- Postgres and Redis health checks are real and return errors on failure - Redis CRUD methods implemented and tested --- @@ -268,7 +268,7 @@ backend/shared/ - `infra/auth/zitadel.Client.ValidateToken` becomes fully implemented with JWKS validation - `infra/auth/zitadel.Client.RefreshToken` and `RevokeToken` implemented -- `infra/database/postgres.Client` wraps `*pgxpool.Pool` +- `infra/database/postgres` remains a minimal helper package (config/env + health check) - `infra/database/redis.Client` wraps `*redis.Client` - `infra/observability/tracing.Tracer` uses OpenTelemetry and returns real spans - New helper in metrics: `metrics.Handler()` or `metrics.PrometheusHandler()` @@ -280,7 +280,7 @@ backend/shared/ Add or extend unit tests for: 1. JWT validation with JWKS (mocked discovery) 2. MFA detection via `amr` claim -3. Postgres config validation and connection errors +3. Postgres config parsing and health check error behavior 4. Redis CRUD behavior (use `miniredis` or mocked client) 5. Tracing initialization and shutdown behavior 6. Metrics handler creation