🔧 Dev

OffreThe Augmented Engineering Programdès 2 500 € / mois

Tech Lead & équipe · ou MVP livré en 3 mois + recrutement

Découvrir

🧩 CH4 - Building a Modern Payment Gateway with a Rebill Scheduler - Chapter 4 - The Rebill Scheduler (with Subscriptions)

Sébastien Techer31/10/2025

Why you might want to build your own payment gateway — and how to do it safely with an external PCI-compliant vault.

🔁 Introducing the Subscription Model

Before we can automate rebills, we need a data model that represents time.

A subscription defines:

  • Who is subscribed (tenant, customer),
  • What they pay for (plan or SKU),
  • How often they are billed (frequency),
  • When the current billing period started and ends,
  • Which payment method (vault token / stored reference).

Each subscription spawns a chain of subscription items
each representing a billing cycle (month, week, etc.),
each generating a rebill event at the exact renewal time.


🧩 Core Entities

Subscription

Represents the overall recurring relationship.

type FrequencyType string
const (
  FrequencyDay   FrequencyType = "day"
  FrequencyWeek  FrequencyType = "week"
  FrequencyMonth FrequencyType = "month"
  FrequencyYear  FrequencyType = "year"
)

type Subscription struct {
  ID          string
  TenantID    string
  CustomerID  string
  PSP         string
  PlanID      string
  Amount      Money
  Frequency   FrequencyType
  Interval    int // e.g., every 1 month or every 3 months
  PaymentRef  PaymentMethodRef
  StartDate   time.Time
  EndDate     *time.Time
  Status      string // active, paused, canceled
  NextItemAt  time.Time
  CreatedAt   time.Time
  UpdatedAt   time.Time
}

SubscriptionItem

Represents a single billing cycle.

type SubscriptionItem struct {
  ID              string
  SubscriptionID  string
  PeriodStart     time.Time
  PeriodEnd       time.Time
  RebillStatus    string // pending, succeeded, failed
  PSPRef          string // reference from the PSP
  PaymentIntentID string // link to gateway intent
  CreatedAt       time.Time
}

🧠 How It Works

Every subscription generates its next item at the end of the current period:

Subscription (monthly)
 ├─ Item #1: 2025-09-01 → 2025-09-30 → Rebill executed ✅
 ├─ Item #2: 2025-10-01 → 2025-10-31 → Rebill scheduled ⏳
 ├─ Item #3: 2025-11-01 → 2025-11-30 → Next in queue

When Item #1 reaches its PeriodEnd, the scheduler automatically:

  1. Generates Item #2 with new PeriodStart and PeriodEnd.

  2. Triggers a rebill (MIT) at PeriodEnd.

  3. Updates the subscription’s NextItemAt to PeriodEnd + 1 interval.

This model ensures:

  • Precision (rebills happen exactly at the correct timestamp),

  • Traceability (each cycle is an auditable entity),

  • Flexibility (different frequencies per customer).


⚙️ Rebill Scheduler with Subscription Awareness

We can extend our scheduler logic to operate on SubscriptionItems instead of just subscriptions.

Step 1 – Enqueue Due Items

func (s *RebillService) EnqueueDueItems(ctx context.Context) error {
  dueItems, err := s.Store.ListDueItems(ctx, time.Now())
  if err != nil {
    return err
  }

  for _, item := range dueItems {
    job := RebillJob{
      SubscriptionID: item.SubscriptionID,
      SubscriptionItemID: item.ID,
    }
    _ = s.Queue.Publish(ctx, "rebill.jobs", job)
  }
  return nil
}

Step 2 – Process Rebill Job

func (s *RebillWorker) ProcessJob(ctx context.Context, job RebillJob) error {
  sub, err := s.Store.GetSubscription(ctx, job.SubscriptionID)
  if err != nil { return err }

  item, err := s.Store.GetSubscriptionItem(ctx, job.SubscriptionItemID)
  if err != nil { return err }

  req := ports.PaymentRequest{
    TenantID:   sub.TenantID,
    Amount:     sub.Amount,
    PaymentRef: sub.PaymentRef,
    Mode:       ports.MIT,
    Capture:    true,
    Metadata: map[string]string{
      "subscription_id": sub.ID,
      "subscription_item_id": item.ID,
    },
  }

  resp, err := s.PSPs[sub.PSP].CreateIntent(ctx, req)
  if err != nil {
    return s.HandleFailure(ctx, sub, item, err)
  }

  // Mark success
  _ = s.Store.UpdateItemStatus(ctx, item.ID, "succeeded")
  _ = s.Store.AppendAudit(ctx, sub.ID, "rebill", fmt.Sprintf("item %s %s", item.ID, resp.Intent.Status))

  // Schedule next item if subscription is still active
  if sub.Status == "active" {
    nextStart := item.PeriodEnd
    nextEnd := nextStart.Add(s.frequencyToDuration(sub.Frequency, sub.Interval))
    _ = s.Store.CreateSubscriptionItem(ctx, sub.ID, nextStart, nextEnd)
    _ = s.Store.UpdateSubscriptionNextItem(ctx, sub.ID, nextEnd)
  }
  return nil
}

Step 3 – Frequency to Duration Helper

func (s *RebillWorker) frequencyToDuration(freq FrequencyType, interval int) time.Duration {
  switch freq {
  case FrequencyDay:
    return time.Hour * 24 * time.Duration(interval)
  case FrequencyWeek:
    return time.Hour * 24 * 7 * time.Duration(interval)
  case FrequencyMonth:
    return time.Hour * 24 * 30 * time.Duration(interval)
  case FrequencyYear:
    return time.Hour * 24 * 365 * time.Duration(interval)
  default:
    return time.Hour * 24 * 30
  }
}

⚖️ Atomicity & Consistency

Each rebill cycle must be atomic:

  1. The payment creation,

  2. The item status update,

  3. The next item scheduling.

Use transactions:

tx, err := s.DB.BeginTx(ctx, nil)
if err != nil { return err }

defer tx.Rollback()

// Create payment intent, update item, schedule next
if err := s.execRebillTx(ctx, tx, job); err != nil {
  return err
}
return tx.Commit()

This guarantees that no period is skipped or double-charged, even under concurrency or system restarts.


🧠 Grace Periods & Retries

When a rebill fails, you don’t want to cancel the subscription immediately.

You can apply a grace period before suspension:

AttemptGrace PeriodAction
1st failure24hRetry
2nd failure3 daysNotify customer
3rd failure7 daysSuspend subscription

This provides flexibility and prevents unnecessary churn.

📬 Notifications & Communication

Automate user communication for transparency:

  • On success: “Your subscription was renewed successfully.”

  • On failure: “We couldn’t process your renewal. Please update your payment info.”

  • Before expiration: “Your subscription will renew on Oct 31.”

Use the same notifier system introduced earlier, extended with templates for subscription events.

Recap

EntityPurpose
SubscriptionDefines frequency, amount, and customer relationship
SubscriptionItemRepresents each billing cycle and its payment
SchedulerDetects due items and enqueues rebill jobs
WorkerExecutes MIT payments atomically
StoreTracks items, statuses, and audit trails
NotifierCommunicates renewal and failure events
Vault + PSPExecute secure recurring transactions

🚀 Coming Next

In Chapter 5 – Monitoring & Scalability, we’ll focus on production readiness:

  • Real-time monitoring and metrics,

  • Distributed scaling for the rebill workers,

  • Audit trails and key rotation.

Your gateway is now more than a processor — it’s an autonomous financial system that understands time.

Respect de votre vie privée

Nous utilisons des cookies pour améliorer votre expérience, analyser le trafic et personnaliser le contenu. Vous pouvez choisir quels cookies accepter.