🧩 CH4 - Building a Modern Payment Gateway with a Rebill Scheduler - Chapter 4 - The Rebill Scheduler (with Subscriptions)
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:
-
Generates Item #2 with new PeriodStart and PeriodEnd.
-
Triggers a rebill (MIT) at PeriodEnd.
-
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:
-
The payment creation,
-
The item status update,
-
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:
| Attempt | Grace Period | Action |
|---|---|---|
| 1st failure | 24h | Retry |
| 2nd failure | 3 days | Notify customer |
| 3rd failure | 7 days | Suspend 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
| Entity | Purpose |
|---|---|
| Subscription | Defines frequency, amount, and customer relationship |
| SubscriptionItem | Represents each billing cycle and its payment |
| Scheduler | Detects due items and enqueues rebill jobs |
| Worker | Executes MIT payments atomically |
| Store | Tracks items, statuses, and audit trails |
| Notifier | Communicates renewal and failure events |
| Vault + PSP | Execute 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.