Як побудувати ядро онлайн-казино: архітектура, модулі та інтеграції

Розробка ядра онлайн-казино — це складна інженерна задача, яка вимагає глибокого розуміння фінансових транзакцій, регуляторних вимог та високого навантаження. У цій статті розглянемо ключові компоненти, які складають серце будь-якого сучасного iGaming …


This content originally appeared on DEV Community and was authored by Maksim

Розробка ядра онлайн-казино — це складна інженерна задача, яка вимагає глибокого розуміння фінансових транзакцій, регуляторних вимог та високого навантаження. У цій статті розглянемо ключові компоненти, які складають серце будь-якого сучасного iGaming платформи.

Загальна архітектура

Ядро казино складається з декількох критичних сервісів, які працюють разом для забезпечення безперервної роботи:

┌─────────────────────────────────────────────────────┐
│                   API Gateway                        │
│              (Rate Limiting, Auth)                   │
└─────────────────┬───────────────────────────────────┘
                  │
        ┌─────────┴─────────┬──────────────┬──────────┐
        │                   │              │          │
   ┌────▼────┐      ┌──────▼─────┐   ┌───▼────┐  ┌──▼──────┐
   │  Game   │      │   Wallet   │   │Session │  │  User   │
   │ Engine  │◄────►│  Service   │   │Service │  │ Service │
   └────┬────┘      └──────┬─────┘   └────────┘  └─────────┘
        │                  │
        │           ┌──────▼─────┐
        └──────────►│Transaction │
                    │  Pipeline  │
                    └──────┬─────┘
                           │
                    ┌──────▼─────┐
                    │  Journal   │
                    │  Service   │
                    └────────────┘

1. Game Engine — мозок казино

Game Engine відповідає за логіку ігор, комунікацію з провайдерами та обробку результатів.

Основні відповідальності:

Управління провайдерами ігор:

type GameProvider interface {
    GetGameList() ([]Game, error)
    LaunchGame(ctx context.Context, req LaunchRequest) (*LaunchResponse, error)
    ProcessCallback(ctx context.Context, callback ProviderCallback) error
    ValidateSignature(payload []byte, signature string) bool
}

Каталог ігор:

CREATE TABLE games (
    id UUID PRIMARY KEY,
    provider_id VARCHAR(50) NOT NULL,
    game_code VARCHAR(100) NOT NULL,
    name VARCHAR(255) NOT NULL,
    category VARCHAR(50),
    rtp DECIMAL(5,2),
    volatility VARCHAR(20),
    min_bet DECIMAL(10,2),
    max_bet DECIMAL(10,2),
    is_active BOOLEAN DEFAULT true,
    metadata JSONB,
    created_at TIMESTAMP DEFAULT NOW(),
    UNIQUE(provider_id, game_code)
);

CREATE INDEX idx_games_provider ON games(provider_id);
CREATE INDEX idx_games_category ON games(category) WHERE is_active = true;

Життєвий цикл гри:

type GameSession struct {
    ID          string
    UserID      string
    GameID      string
    ProviderID  string
    Currency    string
    SessionURL  string
    StartedAt   time.Time
    ExpiresAt   time.Time
    Status      SessionStatus
}

func (ge *GameEngine) LaunchGame(ctx context.Context, userID, gameID string) (*GameSession, error) {
    // 1. Валідація користувача та гри
    user, err := ge.userService.GetUser(ctx, userID)
    if err != nil {
        return nil, err
    }

    game, err := ge.getGame(ctx, gameID)
    if err != nil {
        return nil, err
    }

    // 2. Перевірка балансу
    balance, err := ge.walletService.GetBalance(ctx, userID, user.Currency)
    if err != nil {
        return nil, err
    }

    if balance.Available < game.MinBet {
        return nil, ErrInsufficientBalance
    }

    // 3. Створення сесії
    session := &GameSession{
        ID:         generateUUID(),
        UserID:     userID,
        GameID:     gameID,
        ProviderID: game.ProviderID,
        Currency:   user.Currency,
        StartedAt:  time.Now(),
        ExpiresAt:  time.Now().Add(4 * time.Hour),
        Status:     SessionStatusActive,
    }

    // 4. Запит до провайдера
    provider := ge.getProvider(game.ProviderID)
    launchResp, err := provider.LaunchGame(ctx, LaunchRequest{
        UserID:     userID,
        GameCode:   game.GameCode,
        Currency:   user.Currency,
        SessionID:  session.ID,
        ReturnURL:  ge.config.ReturnURL,
        Language:   user.Language,
    })
    if err != nil {
        return nil, err
    }

    session.SessionURL = launchResp.URL

    // 5. Збереження сесії
    if err := ge.sessionRepo.Save(ctx, session); err != nil {
        return nil, err
    }

    return session, nil
}

2. RGS (Remote Game Server) — віддалений сервер ігор

RGS — це сервер, який обробляє callback'и від провайдерів ігор (ставки, виграші, скасування).

Endpoint структура:

type RGSHandler struct {
    transactionPipeline *TransactionPipeline
    journalService      *JournalService
    walletService       *WalletService
}

// POST /rgs/provider/{provider_id}/callback
func (h *RGSHandler) HandleCallback(w http.ResponseWriter, r *http.Request) {
    providerID := chi.URLParam(r, "provider_id")

    // 1. Валідація підпису
    if !h.validateSignature(r, providerID) {
        respondError(w, http.StatusUnauthorized, "Invalid signature")
        return
    }

    // 2. Парсинг callback
    var callback ProviderCallback
    if err := json.NewDecoder(r.Body).Decode(&callback); err != nil {
        respondError(w, http.StatusBadRequest, "Invalid payload")
        return
    }

    // 3. Обробка транзакції
    result, err := h.processTransaction(r.Context(), providerID, &callback)
    if err != nil {
        // Повертаємо специфічну помилку провайдеру
        respondProviderError(w, err)
        return
    }

    // 4. Відповідь провайдеру
    respondJSON(w, http.StatusOK, result)
}

Типи callback'ів:

type CallbackType string

const (
    CallbackTypeBet       CallbackType = "bet"
    CallbackTypeWin       CallbackType = "win"
    CallbackTypeRefund    CallbackType = "refund"
    CallbackTypeRollback  CallbackType = "rollback"
)

type ProviderCallback struct {
    TransactionID string       `json:"transaction_id"`
    RoundID       string       `json:"round_id"`
    UserID        string       `json:"user_id"`
    GameID        string       `json:"game_id"`
    Type          CallbackType `json:"type"`
    Amount        float64      `json:"amount"`
    Currency      string       `json:"currency"`
    Timestamp     time.Time    `json:"timestamp"`
    Reference     string       `json:"reference"`
    Metadata      interface{}  `json:"metadata"`
}

3. Wallet Service — управління балансом

Wallet Service — це критичний компонент, який управляє грошима користувачів.

Структура балансу:

CREATE TABLE wallets (
    id UUID PRIMARY KEY,
    user_id UUID NOT NULL,
    currency VARCHAR(3) NOT NULL,
    balance DECIMAL(20,8) NOT NULL DEFAULT 0,
    locked_balance DECIMAL(20,8) NOT NULL DEFAULT 0,
    version INT NOT NULL DEFAULT 0,
    updated_at TIMESTAMP DEFAULT NOW(),
    UNIQUE(user_id, currency),
    CONSTRAINT balance_positive CHECK (balance >= 0),
    CONSTRAINT locked_positive CHECK (locked_balance >= 0)
);

CREATE INDEX idx_wallets_user ON wallets(user_id);

Optimistic locking для конкурентності:

type WalletService struct {
    db *sql.DB
}

func (ws *WalletService) UpdateBalance(
    ctx context.Context,
    userID string,
    currency string,
    amount decimal.Decimal,
    txType TransactionType,
) error {
    maxRetries := 3

    for attempt := 0; attempt < maxRetries; attempt++ {
        // 1. Читаємо поточний баланс з версією
        wallet, err := ws.getWalletForUpdate(ctx, userID, currency)
        if err != nil {
            return err
        }

        // 2. Обчислюємо новий баланс
        newBalance := wallet.Balance.Add(amount)
        if newBalance.IsNegative() {
            return ErrInsufficientBalance
        }

        // 3. Оновлюємо з перевіркою версії
        result, err := ws.db.ExecContext(ctx, `
            UPDATE wallets
            SET balance = $1,
                version = version + 1,
                updated_at = NOW()
            WHERE user_id = $2
              AND currency = $3
              AND version = $4
        `, newBalance, userID, currency, wallet.Version)

        if err != nil {
            return err
        }

        rowsAffected, _ := result.RowsAffected()
        if rowsAffected == 1 {
            return nil // Успіх!
        }

        // Version conflict - retry
        time.Sleep(time.Millisecond * time.Duration(10*(attempt+1)))
    }

    return ErrVersionConflict
}

4. Transaction Pipeline — конвеєр транзакцій

Transaction Pipeline забезпечує атомарну обробку всіх фінансових операцій.

Етапи обробки:

type TransactionPipeline struct {
    journalService *JournalService
    walletService  *WalletService
    eventBus       *EventBus
}

func (tp *TransactionPipeline) Process(
    ctx context.Context,
    tx *Transaction,
) (*TransactionResult, error) {
    // 1. Перевірка дубліката (idempotency)
    if existing, err := tp.journalService.GetByIdempotencyKey(
        ctx, tx.IdempotencyKey,
    ); err == nil {
        return existing.Result, nil // Вже оброблено
    }

    // 2. Валідація
    if err := tp.validate(ctx, tx); err != nil {
        return nil, err
    }

    // 3. Запис у журнал (pending)
    journal := &JournalEntry{
        ID:              generateUUID(),
        IdempotencyKey:  tx.IdempotencyKey,
        TransactionType: tx.Type,
        UserID:          tx.UserID,
        Amount:          tx.Amount,
        Currency:        tx.Currency,
        Status:          StatusPending,
        CreatedAt:       time.Now(),
    }

    if err := tp.journalService.Create(ctx, journal); err != nil {
        return nil, err
    }

    // 4. Оновлення балансу
    err := tp.walletService.UpdateBalance(
        ctx,
        tx.UserID,
        tx.Currency,
        tp.getAmountWithSign(tx),
        tx.Type,
    )

    // 5. Оновлення статусу журналу
    if err != nil {
        journal.Status = StatusFailed
        journal.Error = err.Error()
    } else {
        journal.Status = StatusCompleted
    }

    if err := tp.journalService.Update(ctx, journal); err != nil {
        return nil, err
    }

    // 6. Публікація події
    tp.eventBus.Publish(TransactionCompletedEvent{
        JournalID: journal.ID,
        UserID:    tx.UserID,
        Type:      tx.Type,
        Amount:    tx.Amount,
        Status:    journal.Status,
    })

    return &TransactionResult{
        Success:    err == nil,
        Balance:    journal.BalanceAfter,
        JournalID:  journal.ID,
    }, err
}

5. RTP та математика ігор

RTP (Return to Player) — це відсоток ставок, який повертається гравцям у вигляді виграшів.

Моніторинг RTP:

CREATE TABLE rtp_tracking (
    game_id UUID NOT NULL,
    provider_id VARCHAR(50) NOT NULL,
    period_start TIMESTAMP NOT NULL,
    period_end TIMESTAMP NOT NULL,
    total_bets DECIMAL(20,2) NOT NULL,
    total_wins DECIMAL(20,2) NOT NULL,
    rounds_count BIGINT NOT NULL,
    actual_rtp DECIMAL(5,2) GENERATED ALWAYS AS (
        CASE 
            WHEN total_bets > 0 
            THEN (total_wins / total_bets * 100)
            ELSE 0 
        END
    ) STORED,
    PRIMARY KEY (game_id, period_start)
);

CREATE INDEX idx_rtp_tracking_period ON rtp_tracking(period_start, period_end);

Агрегація метрик:

type RTPCalculator struct {
    db *sql.DB
}

func (rc *RTPCalculator) CalculateRTP(
    ctx context.Context,
    gameID string,
    period time.Duration,
) (*RTPMetrics, error) {
    var metrics RTPMetrics

    err := rc.db.QueryRowContext(ctx, `
        SELECT 
            COALESCE(SUM(amount), 0) as total_bets,
            COALESCE(SUM(CASE WHEN type = 'win' THEN amount ELSE 0 END), 0) as total_wins,
            COUNT(DISTINCT round_id) as rounds_count,
            COUNT(*) as transactions_count
        FROM transactions
        WHERE game_id = $1
          AND created_at > $2
          AND type IN ('bet', 'win')
    `, gameID, time.Now().Add(-period)).Scan(
        &metrics.TotalBets,
        &metrics.TotalWins,
        &metrics.RoundsCount,
        &metrics.TransactionsCount,
    )

    if err != nil {
        return nil, err
    }

    if metrics.TotalBets > 0 {
        metrics.ActualRTP = (metrics.TotalWins / metrics.TotalBets) * 100
    }

    return &metrics, nil
}

6. Journal Service — журнал подій та idempotency

Journal Service зберігає всі транзакції та забезпечує idempotency.

Структура журналу:

CREATE TABLE transaction_journal (
    id UUID PRIMARY KEY,
    idempotency_key VARCHAR(255) UNIQUE NOT NULL,
    user_id UUID NOT NULL,
    transaction_type VARCHAR(50) NOT NULL,
    amount DECIMAL(20,8) NOT NULL,
    currency VARCHAR(3) NOT NULL,
    balance_before DECIMAL(20,8),
    balance_after DECIMAL(20,8),
    status VARCHAR(20) NOT NULL,
    error_message TEXT,
    provider_id VARCHAR(50),
    game_id UUID,
    round_id VARCHAR(255),
    reference VARCHAR(255),
    metadata JSONB,
    created_at TIMESTAMP DEFAULT NOW(),
    updated_at TIMESTAMP DEFAULT NOW()
);

CREATE INDEX idx_journal_idempotency ON transaction_journal(idempotency_key);
CREATE INDEX idx_journal_user ON transaction_journal(user_id, created_at DESC);
CREATE INDEX idx_journal_round ON transaction_journal(round_id) WHERE round_id IS NOT NULL;

Реалізація idempotency:

type JournalService struct {
    db *sql.DB
}

func (js *JournalService) GetByIdempotencyKey(
    ctx context.Context,
    key string,
) (*JournalEntry, error) {
    var entry JournalEntry

    err := js.db.QueryRowContext(ctx, `
        SELECT id, idempotency_key, user_id, transaction_type,
               amount, currency, balance_after, status, error_message,
               created_at
        FROM transaction_journal
        WHERE idempotency_key = $1
    `, key).Scan(
        &entry.ID,
        &entry.IdempotencyKey,
        &entry.UserID,
        &entry.TransactionType,
        &entry.Amount,
        &entry.Currency,
        &entry.BalanceAfter,
        &entry.Status,
        &entry.Error,
        &entry.CreatedAt,
    )

    if err == sql.ErrNoRows {
        return nil, ErrNotFound
    }

    return &entry, err
}

Інтеграції з провайдерами

Адаптер для провайдера:

type ProviderAdapter interface {
    Name() string
    LaunchGame(ctx context.Context, req LaunchRequest) (*LaunchResponse, error)
    ProcessCallback(ctx context.Context, callback ProviderCallback) (*CallbackResponse, error)
    ValidateSignature(payload []byte, signature string) bool
}

type PragmaticPlayAdapter struct {
    config PragmaticConfig
    client *http.Client
}

func (pp *PragmaticPlayAdapter) ProcessCallback(
    ctx context.Context,
    callback ProviderCallback,
) (*CallbackResponse, error) {
    // Специфічна логіка для Pragmatic Play
    switch callback.Type {
    case CallbackTypeBet:
        return pp.processBet(ctx, callback)
    case CallbackTypeWin:
        return pp.processWin(ctx, callback)
    case CallbackTypeRefund:
        return pp.processRefund(ctx, callback)
    default:
        return nil, ErrUnsupportedCallbackType
    }
}

Моніторинг та алерти

type MetricsCollector struct {
    prometheus *prometheus.Registry
}

var (
    transactionDuration = prometheus.NewHistogramVec(
        prometheus.HistogramOpts{
            Name: "casino_transaction_duration_seconds",
            Help: "Transaction processing duration",
            Buckets: []float64{0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1},
        },
        []string{"type", "status"},
    )

    activeGameSessions = prometheus.NewGauge(
        prometheus.GaugeOpts{
            Name: "casino_active_game_sessions",
            Help: "Number of active game sessions",
        },
    )

    walletBalance = prometheus.NewGaugeVec(
        prometheus.GaugeOpts{
            Name: "casino_wallet_balance_total",
            Help: "Total wallet balance by currency",
        },
        []string{"currency"},
    )
)

Висновок

Побудова ядра онлайн-казино вимагає:

  1. Надійної архітектури з чітким розділенням відповідальності
  2. Atomic транзакцій з гарантією idempotency
  3. Масштабованості для high-load сценаріїв
  4. Моніторингу всіх критичних метрик
  5. Аудиту кожної транзакції через журнал

У наступних статтях детально розглянемо:

  • Як уникнути подвійних ставок та виплат
  • Побудову session service для high-load

Питання? Пишіть у коментарях! 🎰


This content originally appeared on DEV Community and was authored by Maksim


Print Share Comment Cite Upload Translate Updates
APA

Maksim | Sciencx (2025-11-24T18:41:22+00:00) Як побудувати ядро онлайн-казино: архітектура, модулі та інтеграції. Retrieved from https://www.scien.cx/2025/11/24/%d1%8f%d0%ba-%d0%bf%d0%be%d0%b1%d1%83%d0%b4%d1%83%d0%b2%d0%b0%d1%82%d0%b8-%d1%8f%d0%b4%d1%80%d0%be-%d0%be%d0%bd%d0%bb%d0%b0%d0%b9%d0%bd-%d0%ba%d0%b0%d0%b7%d0%b8%d0%bd%d0%be-%d0%b0%d1%80%d1%85%d1%96/

MLA
" » Як побудувати ядро онлайн-казино: архітектура, модулі та інтеграції." Maksim | Sciencx - Monday November 24, 2025, https://www.scien.cx/2025/11/24/%d1%8f%d0%ba-%d0%bf%d0%be%d0%b1%d1%83%d0%b4%d1%83%d0%b2%d0%b0%d1%82%d0%b8-%d1%8f%d0%b4%d1%80%d0%be-%d0%be%d0%bd%d0%bb%d0%b0%d0%b9%d0%bd-%d0%ba%d0%b0%d0%b7%d0%b8%d0%bd%d0%be-%d0%b0%d1%80%d1%85%d1%96/
HARVARD
Maksim | Sciencx Monday November 24, 2025 » Як побудувати ядро онлайн-казино: архітектура, модулі та інтеграції., viewed ,<https://www.scien.cx/2025/11/24/%d1%8f%d0%ba-%d0%bf%d0%be%d0%b1%d1%83%d0%b4%d1%83%d0%b2%d0%b0%d1%82%d0%b8-%d1%8f%d0%b4%d1%80%d0%be-%d0%be%d0%bd%d0%bb%d0%b0%d0%b9%d0%bd-%d0%ba%d0%b0%d0%b7%d0%b8%d0%bd%d0%be-%d0%b0%d1%80%d1%85%d1%96/>
VANCOUVER
Maksim | Sciencx - » Як побудувати ядро онлайн-казино: архітектура, модулі та інтеграції. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/11/24/%d1%8f%d0%ba-%d0%bf%d0%be%d0%b1%d1%83%d0%b4%d1%83%d0%b2%d0%b0%d1%82%d0%b8-%d1%8f%d0%b4%d1%80%d0%be-%d0%be%d0%bd%d0%bb%d0%b0%d0%b9%d0%bd-%d0%ba%d0%b0%d0%b7%d0%b8%d0%bd%d0%be-%d0%b0%d1%80%d1%85%d1%96/
CHICAGO
" » Як побудувати ядро онлайн-казино: архітектура, модулі та інтеграції." Maksim | Sciencx - Accessed . https://www.scien.cx/2025/11/24/%d1%8f%d0%ba-%d0%bf%d0%be%d0%b1%d1%83%d0%b4%d1%83%d0%b2%d0%b0%d1%82%d0%b8-%d1%8f%d0%b4%d1%80%d0%be-%d0%be%d0%bd%d0%bb%d0%b0%d0%b9%d0%bd-%d0%ba%d0%b0%d0%b7%d0%b8%d0%bd%d0%be-%d0%b0%d1%80%d1%85%d1%96/
IEEE
" » Як побудувати ядро онлайн-казино: архітектура, модулі та інтеграції." Maksim | Sciencx [Online]. Available: https://www.scien.cx/2025/11/24/%d1%8f%d0%ba-%d0%bf%d0%be%d0%b1%d1%83%d0%b4%d1%83%d0%b2%d0%b0%d1%82%d0%b8-%d1%8f%d0%b4%d1%80%d0%be-%d0%be%d0%bd%d0%bb%d0%b0%d0%b9%d0%bd-%d0%ba%d0%b0%d0%b7%d0%b8%d0%bd%d0%be-%d0%b0%d1%80%d1%85%d1%96/. [Accessed: ]
rf:citation
» Як побудувати ядро онлайн-казино: архітектура, модулі та інтеграції | Maksim | Sciencx | https://www.scien.cx/2025/11/24/%d1%8f%d0%ba-%d0%bf%d0%be%d0%b1%d1%83%d0%b4%d1%83%d0%b2%d0%b0%d1%82%d0%b8-%d1%8f%d0%b4%d1%80%d0%be-%d0%be%d0%bd%d0%bb%d0%b0%d0%b9%d0%bd-%d0%ba%d0%b0%d0%b7%d0%b8%d0%bd%d0%be-%d0%b0%d1%80%d1%85%d1%96/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.