Files
pos-system/services/ads-serving-service-net/SERVICE_DOCS.md
Ho Ngoc Hai f3779c4ebe docs: add SERVICE_DOCS.md for all 24 microservices from per-service code audit
Each SERVICE_DOCS.md documents: Overview, API Endpoints, Commands, Queries,
Domain Model, Database Schema, Integration Events, Dependencies, Configuration.
Generated by 23 parallel audit agents reading actual source code.

Key corrections from audit:
- inventory-service: 12 commands/6 queries (was listed as scaffold)
- promotion-service: 12 commands/10 queries (was listed as 0)
- mission-service: 4 commands/7 queries (was listed as 0)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-13 17:54:53 +07:00

20 KiB

AdsServingService - Service Documentation

1. Overview

Purpose: Real-time ad serving microservice that implements a second-price auction system. It receives ad serve requests, runs real-time bidding (RTB) auctions among eligible ad candidates, determines winners via eCPM scoring, and tracks impressions/clicks. It also provides admin endpoints for monitoring auctions, budget pacing, and frequency capping.

Port: 5012 (Development, via launchSettings.json) Docker Port: 8080 (Production, via ASPNETCORE_URLS=http://+:8080) Database: ads_serving_service (PostgreSQL, Neon cloud in appsettings.json) Framework: .NET 10.0, C# 14 Solution File: AdsServingService.slnx Initial Migration: 20260117181413_InitialCreate


2. API Endpoints

AdsController (api/v1/ads)

Method Route Description Request Body Response
POST /api/v1/ads/serve Serve an ad via real-time auction (target < 100ms) ServeAdRequest 200 OK with ServedAdDto or 204 NoContent if no eligible ads
POST /api/v1/ads/events/impression Track ad impression (fire-and-forget) ImpressionEvent 202 Accepted
POST /api/v1/ads/events/click Track ad click (fire-and-forget) ClickEvent 202 Accepted

Request/Response DTOs (AdsController):

ServeAdRequest {
    UserId: Guid
    PlacementType: string (default "feed")
    UserContext: Dictionary<string, string>? (optional)
}

ServedAdDto {
    AdId: Guid
    CampaignId: Guid
    AdFormat: string
    Headline: string?
    PrimaryText: string?
    CallToAction: string?
    CreativeUrl: string?
    DestinationUrl: string?
    FinalPrice: decimal
    ServedAt: DateTime
}

ImpressionEvent { AdId: Guid, UserId: Guid, Timestamp: DateTime }
ClickEvent { AdId: Guid, UserId: Guid, Timestamp: DateTime }

AdminAuctionsController (api/v1/admin/auctions)

Method Route Description Query Params Response
GET /api/v1/admin/auctions Paginated list of auctions with filters userId, placementType, startDate, endDate, page (default 1), pageSize (default 20) 200 OK with PagedResult<AuctionDto>
GET /api/v1/admin/auctions/statistics Aggregate auction statistics none 200 OK with AuctionStatisticsDto

AdminBudgetController (api/v1/admin/budget)

Method Route Description Query Params Response
GET /api/v1/admin/budget/pacers Paginated list of budget pacers campaignId, page (default 1), pageSize (default 20) 200 OK with PagedResult<BudgetPacerDto>
GET /api/v1/admin/budget/campaigns/{campaignId} Budget status for a specific campaign none 200 OK with BudgetPacerDto or 404
PUT /api/v1/admin/budget/campaigns/{campaignId}/reset Reset daily spend for a campaign none 200 OK or 404
GET /api/v1/admin/budget/statistics Budget utilization statistics none 200 OK with BudgetStatisticsDto

AdminFrequencyController (api/v1/admin/frequency)

Method Route Description Query Params Response
GET /api/v1/admin/frequency/caps List frequency caps adId (optional filter) 200 OK with List<FrequencyCapDto>
GET /api/v1/admin/frequency/caps/{id} Get frequency cap by ID none 200 OK with FrequencyCapDto or 404
POST /api/v1/admin/frequency/caps Create a new frequency cap body: CreateFrequencyCapRequest 201 Created with FrequencyCapDto
DELETE /api/v1/admin/frequency/caps/{id} Delete a frequency cap none 204 NoContent or 404

Admin DTOs:

AuctionDto { Id, UserId, PlacementType, AuctionTime, BidCount, WinningAdId?, FinalPrice?, WinningeCPM? }
AuctionStatisticsDto { TotalAuctions, AverageWinRate, AverageeCPM, TotalBidsPlaced }
BudgetPacerDto { Id, CampaignId, DailyBudget, SpentToday, RemainingBudget, UtilizationPercent, Strategy, LastUpdated }
BudgetStatisticsDto { TotalCampaigns, TotalDailyBudget, TotalSpentToday, AverageUtilization, CampaignsExceeded }
FrequencyCapDto { Id, AdId, MaxImpressionsPerUser, Window }
CreateFrequencyCapRequest { AdId: Guid, MaxImpressionsPerUser: int, Window: string (default "Day") }
PagedResult<T> { Items, TotalCount, Page, PageSize, TotalPages (computed) }

Health Check Endpoints

Route Description
/health Full health check (includes PostgreSQL)
/health/live Liveness probe (app is running, no dependency checks)
/health/ready Readiness probe (includes PostgreSQL check)

3. Commands

No MediatR Commands exist in this service. The service is primarily read/query-oriented. Write operations (frequency cap CRUD, budget reset) are handled directly in controllers via DbContext without MediatR commands.


4. Queries

ServeAdQuery

  • File: Application/Queries/ServeAdQuery.cs
  • Parameters: UserId (Guid), PlacementType (string), UserContext (Dictionary<string, string>)
  • Returns: ServedAdDto? (nullable - returns null if no eligible ads or auction fails)
  • Handler: ServeAdQueryHandler - orchestrates the full RTB auction pipeline:
    1. Fetches eligible ads via IEligibleAdsProvider
    2. Creates an Auction domain entity
    3. Scores each candidate via IAuctionScoringService (predicted CTR + quality score)
    4. Adds bids to auction
    5. Runs second-price auction (winner determined by highest eCPM)
    6. Persists auction to database (bids stored as JSONB)
    7. Publishes AdServedEvent via IAdServingEventPublisher
    8. Returns winning ad details with final price

GetAuctionsQuery

  • File: Application/Queries/GetAuctionsQuery.cs
  • Parameters: UserId? (Guid), PlacementType? (string), StartDate? (DateTime), EndDate? (DateTime), Page (int, default 1), PageSize (int, default 20)
  • Returns: PagedResult<AuctionDto> - paginated auction history with bid counts parsed from JSONB
  • Handler: GetAuctionsQueryHandler - queries auctions from DB with optional filters, ordered by auction_time descending

GetAuctionStatisticsQuery

  • File: Application/Queries/GetAuctionStatisticsQuery.cs
  • Parameters: none
  • Returns: AuctionStatisticsDto with TotalAuctions, AverageWinRate (%), AverageeCPM, TotalBidsPlaced
  • Handler: GetAuctionStatisticsQueryHandler - loads all auctions into memory and computes aggregates (bid counts parsed from JSONB)

GetBudgetPacersQuery

  • File: Application/Queries/GetBudgetPacersQuery.cs
  • Parameters: CampaignId? (Guid), Page (int, default 1), PageSize (int, default 20)
  • Returns: PagedResult<BudgetPacerDto> - paginated budget pacer list ordered by last_updated descending
  • Handler: GetBudgetPacersQueryHandler

5. Domain Model

Aggregates

Auction (AuctionAggregate)

  • File: Domain/AggregatesModel/AuctionAggregate/Auction.cs
  • Type: Entity + IAggregateRoot
  • Fields: _bids (List), _result (AuctionResult?), _auctionTime (DateTime)
  • Public Properties: UserId, PlacementType, Bids (IReadOnlyCollection), Result, AuctionTime
  • Constructor: Auction(Guid userId, string placementType) - generates new Id, sets auction time to UTC now
  • Behavior Methods:
    • AddBid(adId, campaignId, bidAmount, predictedCTR, qualityScore) - adds a Bid to the auction
    • RunAuction() - sorts bids by eCPM descending, selects winner, calculates second-price (second-highest eCPM + 0.01)

Bid (child entity of Auction)

  • Type: Entity (not aggregate root)
  • Properties: AdId, CampaignId, BidAmount, PredictedCTR, QualityScore
  • Computed: eCPM = BidAmount * PredictedCTR * QualityScore
  • Note: Bids are stored as JSONB in the auctions table, not as a separate table. The Bids navigation is ignored by EF Core.

FrequencyCap (FrequencyAggregate)

  • File: Domain/AggregatesModel/FrequencyAggregate/FrequencyCap.cs
  • Type: Entity + IAggregateRoot
  • Fields: _adId (Guid), _maxImpressionsPerUser (int), _window (FrequencyWindow)
  • Constructor: FrequencyCap(Guid adId, int maxImpressionsPerUser, FrequencyWindow window) - validates maxImpressions > 0
  • Behavior Methods:
    • IsUserCapped(int currentImpressions) - returns true if user has reached the cap
    • Daily(Guid adId, int maxImpressions) - static factory for daily caps

BudgetPacer (PacingAggregate)

  • File: Domain/AggregatesModel/PacingAggregate/BudgetPacer.cs
  • Type: Entity + IAggregateRoot
  • Fields: _campaignId (Guid), _dailyBudget (decimal), _spentToday (decimal), _strategy (PacingStrategy), _lastUpdated (DateTime)
  • Computed Properties: RemainingBudget = _dailyBudget - _spentToday, UtilizationPercent = (_spentToday / _dailyBudget) * 100
  • Constructor: BudgetPacer(Guid campaignId, decimal dailyBudget, PacingStrategy strategy) - validates dailyBudget > 0
  • Behavior Methods:
    • CanServeAd(decimal estimatedCost) - checks budget availability; Smooth strategy enforces hourly pacing with 20% buffer
    • RecordSpend(decimal amount) - adds spend, validates non-negative
    • ResetDailySpend() - resets spent to 0

Value Objects

AuctionResult

  • Properties: WinningAdId (Guid), WinningCampaignId (Guid), FinalPrice (decimal), WinningeCPM (decimal)
  • Equality: based on WinningAdId + FinalPrice
  • Mapped as: Owned type within Auction table

UserAdHistory

  • Properties: UserId (Guid), AdImpressionCounts (Dictionary<Guid, int>), Date (DateTime)
  • Methods: RecordImpression(Guid adId), GetImpressionCount(Guid adId)
  • Note: Designed for Redis caching (key format freq:{userId}:{date}), not persisted in PostgreSQL

Enumerations

FrequencyWindow (enum)

  • Hour = 1, Day = 2, Week = 3, Month = 4, Lifetime = 5

PacingStrategy (enum)

  • Smooth = 1 (spread evenly throughout the day)
  • Accelerated = 2 (spend as fast as possible)

Exceptions

  • AdsServingDomainException - business rule violations specific to ads serving
  • DomainException - generic domain exception base class

SeedWork (Base Classes)

  • Entity - base entity with Id (Guid), DomainEvents (IReadOnlyCollection), equality by Id
  • ValueObject - immutable value comparison base
  • Enumeration - type-safe enum pattern with parse/lookup
  • IAggregateRoot - marker interface
  • IRepository<T> - generic repository with IUnitOfWork
  • IUnitOfWork - SaveChangesAsync(), SaveEntitiesAsync() (dispatches domain events)

6. Database Schema

Database: PostgreSQL (Neon cloud) Migration: 20260117181413_InitialCreate

Table: auctions

Column Type Nullable Description
id uuid NOT NULL (PK) Auction ID
user_id uuid NOT NULL User who triggered the ad request
placement_type varchar(50) NOT NULL Ad placement type (feed, story, banner)
auction_time timestamp with time zone NOT NULL When the auction occurred
bids jsonb NULL All bids serialized as JSON array
winning_ad_id uuid NULL Winning ad ID (owned AuctionResult)
winning_campaign_id uuid NULL Winning campaign ID (owned AuctionResult)
final_price numeric(18,4) NULL Second-price auction final price (owned AuctionResult)
winning_ecpm numeric(18,4) NULL Winning bid eCPM (owned AuctionResult)

Indexes:

  • ix_auctions_user_id on user_id
  • ix_auctions_placement_type on placement_type
  • ix_auctions_auction_time on auction_time
  • ix_auctions_placement_user composite on (placement_type, user_id)

Table: budget_pacers

Column Type Nullable Description
id uuid NOT NULL (PK) Pacer ID
campaign_id uuid NOT NULL Associated campaign ID
daily_budget numeric(18,4) NOT NULL Daily budget limit
spent_today numeric(18,4) NOT NULL Amount spent today
strategy varchar(20) NOT NULL Pacing strategy ("Smooth" or "Accelerated")
last_updated timestamp with time zone NOT NULL Last update timestamp

Indexes:

  • ix_budget_pacers_campaign_id UNIQUE on campaign_id
  • ix_budget_pacers_last_updated on last_updated

Table: frequency_caps

Column Type Nullable Description
id uuid NOT NULL (PK) Frequency cap ID
ad_id uuid NOT NULL Associated ad ID
max_impressions_per_user integer NOT NULL Max impressions per user in window
window varchar(20) NOT NULL Time window ("Hour", "Day", "Week", "Month", "Lifetime")

Indexes:

  • ix_frequency_caps_ad_id on ad_id

7. Integration Events

The service defines an event publishing abstraction (IAdServingEventPublisher) with three event types. The current implementation (LoggingAdServingEventPublisher) only logs events -- no actual message broker integration exists yet.

Published Events

Event Topic/Key Payload Fields
AdServedEvent ad.served.v1 AuctionId, AdId, CampaignId, UserId, PlacementType, FinalPrice, WinningEcpm, ServedAt
AdImpressionTrackedEvent ads.impression.tracked.v1 AdId, UserId, Timestamp
AdClickTrackedEvent ads.click.tracked.v1 AdId, UserId, Timestamp

Consumed Events

None. The service does not consume any external events.

Application Services

IEligibleAdsProvider / InMemoryEligibleAdsProvider

  • Returns eligible ad candidates for a serve request
  • Current implementation: Returns 2 hardcoded in-memory candidates with deterministic IDs (based on userId + placementType hash)
  • Candidate fields: AdId, CampaignId, BidAmount, Format, Headline, PrimaryText, CallToAction, CreativeUrl, DestinationUrl
  • Note: This is a stub. Production would integrate with ads-manager-service to fetch real campaigns/ads.

IAuctionScoringService / DefaultAuctionScoringService

  • Scores ad candidates to produce AuctionBidSignals(PredictedCtr, QualityScore)
  • Predicted CTR heuristic: base CTR by placement type (story=0.018, banner=0.012, default=0.020), boosted by user segment ("high-intent" +25%) and recency (purchase within 14 days +10%)
  • Quality Score heuristic: by format (single_image=1.0, video=1.1, default=0.95)

8. Dependencies

NuGet Packages

API Layer:

Package Version
MediatR 12.4.1
FluentValidation 11.11.0
FluentValidation.DependencyInjectionExtensions 11.11.0
Swashbuckle.AspNetCore 7.2.0
Asp.Versioning.Mvc 8.1.0
Asp.Versioning.Mvc.ApiExplorer 8.1.0
AspNetCore.HealthChecks.NpgSql 8.0.2
AspNetCore.HealthChecks.Redis 8.0.1
Hellang.Middleware.ProblemDetails 6.5.1
Serilog.AspNetCore 8.0.3
Serilog.Sinks.Console 6.0.0
Serilog.Sinks.Seq 8.0.0
Microsoft.EntityFrameworkCore.Design 10.0.1

Domain Layer:

Package Version
MediatR.Contracts 2.0.1

Infrastructure Layer:

Package Version
Microsoft.EntityFrameworkCore 10.0.0
Npgsql.EntityFrameworkCore.PostgreSQL 10.0.0
Microsoft.EntityFrameworkCore.Tools 10.0.0
MediatR 12.4.1
Dapper 2.1.35
Microsoft.Extensions.Http.Polly 9.0.0
Polly 8.5.0
StackExchange.Redis 2.8.16

External Service Dependencies

  • PostgreSQL (Neon cloud) - primary data store
  • Redis (configured but not actively used in code) - intended for frequency cap caching (UserAdHistory)
  • ads-manager-service (not integrated yet) - intended source for eligible ad campaigns (currently stubbed with InMemoryEligibleAdsProvider)
  • ads-tracking-service (not integrated yet) - intended consumer of impression/click events (currently logged only)

9. Configuration

Connection Strings (appsettings.json)

Key Description
ConnectionStrings:DefaultConnection PostgreSQL connection string (Neon cloud endpoint in default config)
DATABASE_URL Alternative env var for connection string (fallback)

Redis

Key Default
Redis:ConnectionString localhost:6379

JWT (configured but not enforced -- no [Authorize] attributes on controllers)

Key Default
Jwt:Secret your-super-secret-key-min-32-characters
Jwt:Issuer goodgo-platform
Jwt:Audience goodgo-services
Jwt:AccessTokenExpiryMinutes 15
Jwt:RefreshTokenExpiryDays 7

Environment Variables (.env.example)

Variable Description
ASPNETCORE_ENVIRONMENT Runtime environment (Development/Production)
DATABASE_URL PostgreSQL connection string
REDIS_URL Redis connection string
REDIS_PASSWORD Redis password
JWT_SECRET JWT signing key
API_PORT API listening port
OTEL_EXPORTER_OTLP_ENDPOINT OpenTelemetry exporter endpoint
SEQ_URL Seq logging endpoint
RATE_LIMIT_PERMITS_PER_MINUTE Rate limiting config

Build Configuration (Directory.Build.props)

  • Target Framework: net10.0
  • Language Version: C# 14
  • Nullable: enabled
  • TreatWarningsAsErrors: true
  • GenerateDocumentationFile: true

Docker

  • Multi-stage build: sdk:10.0 (build) -> aspnet:10.0 (runtime)
  • Non-root user: dotnetuser (UID/GID 1001)
  • Port: 8080
  • Health check: curl -f http://localhost:8080/health/live (30s interval, 3 retries)

Startup Behavior

  • Auto-applies EF Core migrations on startup (catches and logs errors without crashing)
  • Swagger UI enabled in Development at /swagger
  • CORS: allows any origin/method/header (open)
  • MediatR pipeline: LoggingBehavior -> ValidatorBehavior -> TransactionBehavior (skips transactions for queries)

10. Tests

Functional Tests (tests/AdsServingService.FunctionalTests/)

Uses CustomWebApplicationFactory with InMemoryDatabase (unique DB per factory instance).

Test Description
ServeAd_ShouldReturnServedAdPayload POST /api/v1/ads/serve returns 200 with valid adId and positive finalPrice
GetAuctionStatistics_AfterServe_ShouldReflectPersistedAuctions After serving an ad, statistics show >= 1 auction and >= 2 bids
TrackImpression_ShouldReturnAccepted POST /api/v1/ads/events/impression returns 202 Accepted
HealthCheck_ShouldReturnHealthy GET /health/live returns 200

Unit Tests (tests/AdsServingService.UnitTests/)

Project exists but contains no test files yet (only generated assembly info).


11. Notable Architecture Decisions

  1. Second-Price Auction: Winner pays second-highest eCPM + $0.01 (standard RTB model)
  2. eCPM Formula: BidAmount * PredictedCTR * QualityScore
  3. Bids stored as JSONB: Instead of a separate bids table, bids are serialized to JSON in the auctions.bids column for high-throughput performance. EF Core OwnsMany + ToJson was avoided due to EF Core 10 compatibility issues.
  4. No authentication: Controllers have no [Authorize] attributes despite JWT being configured
  5. Stub eligible ads provider: InMemoryEligibleAdsProvider returns hardcoded candidates -- needs real integration with ads-manager-service
  6. Logging-only event publisher: Events are logged but not published to RabbitMQ or any message broker
  7. No repositories: The service accesses AdsServingServiceContext directly in handlers and controllers (repository pattern is commented out in DependencyInjection.cs)
  8. Idempotency infrastructure: IRequestManager / RequestManager / ClientRequest exist but are not used by any command or controller