Manual subscription management works with twenty customers. It breaks at two hundred. Automating the full subscription lifecycle from trial to active to past-due to cancellation and back is not a nice-to-have feature. It is the foundation that determines whether a SaaS platform can actually scale without adding billing headcount.
Manual subscription management works with twenty customers. It breaks at two hundred. The team member who used to track overdue accounts, manually update plans, and send payment reminders is now spending their entire week on billing administration instead of anything that compounds. Automating the full subscription lifecycle from trial to active to past-due to cancellation is not a billing feature. It is the foundation that determines whether the platform can scale without adding headcount specifically to manage billing edge cases.
Why manual subscription management breaks at scale
The failure mode is consistent across SaaS platforms that do not automate their billing lifecycle. At a small customer count, the team handles exceptions manually: a customer whose card failed gets a reminder email and a few days to update it, a downgrade request gets processed by hand, a trial extension gets approved case by case.
As the customer base grows, the volume of these exceptions grows proportionally but the team capacity does not. Failed payments start slipping through without follow-up. Customers on canceled plans retain access because no automated system disabled it. Subscription state in the database drifts from the actual billing state in the payment provider. Reconciling these systems requires manual investigation.
The correct solution is a state machine that defines subscription states formally and makes every transition explicit, auditable, and automated.
Designing the subscription lifecycle state machine
A state machine is the formal definition of every state a subscription can be in, and every transition allowed between states. Without it, billing logic accumulates as if/else branches scattered across the codebase, each one added to handle a specific edge case, none of them considering the full picture.
The states for a typical SaaS subscription:
trial
trial expires without payment method: canceled
payment method added successfully: active
active
payment fails at renewal: past_due
customer requests cancellation: canceled
customer requests pause: paused
past_due
retry payment succeeds: active
grace period expires: canceled
paused
customer resumes: active
customer cancels: canceled
canceled
customer resubscribes: active
In Go, implementing this as an explicit transition function:
type Status string
const (
StatusTrial Status = "trial"
StatusActive Status = "active"
StatusPastDue Status = "past_due"
StatusPaused Status = "paused"
StatusCanceled Status = "canceled"
)
var allowedTransitions = map[Status][]Status{
StatusTrial: {StatusActive, StatusCanceled},
StatusActive: {StatusPastDue, StatusCanceled, StatusPaused},
StatusPastDue: {StatusActive, StatusCanceled},
StatusPaused: {StatusActive, StatusCanceled},
StatusCanceled: {StatusActive},
}
func (s *SubscriptionService) Transition(ctx context.Context, sub *Subscription, next Status, reason string) error {
allowed := allowedTransitions[sub.Status]
for _, a := range allowed {
if a == next {
return s.applyTransition(ctx, sub, next, reason)
}
}
return fmt.Errorf("transition not allowed: %s -> %s", sub.Status, next)
}
Every transition records an event in a subscription_events table with the before state, after state, timestamp, and reason. This table is the audit trail that lets you reconstruct exactly what happened to any subscription at any point in time.
Handling payment provider webhooks correctly
Webhook events from the payment provider are the primary driver of subscription state changes. When a payment succeeds or fails, the provider fires a webhook to your API. The reliability of your subscription lifecycle depends on handling these correctly.
Three rules for correct webhook handling:
Verify the signature before processing anything. A webhook endpoint that does not verify signatures can be exploited to trigger subscription state changes.
func verifyWebhookSignature(payload []byte, sig, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expected := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(sig), []byte(expected))
}
Make processing idempotent. The payment provider may send the same event more than once. Check whether you have already processed a given event ID before doing anything:
func (h *WebhookHandler) Process(ctx context.Context, event *PaymentEvent) error {
exists, err := h.repo.EventProcessed(ctx, event.ID)
if err != nil { return err }
if exists { return nil } // Already handled
return h.db.Transaction(ctx, func(tx *DB) error {
if err := tx.MarkEventProcessed(ctx, event.ID); err != nil {
return err
}
return h.handleEvent(ctx, tx, event)
})
}
Respond 200 immediately and process in the background. The payment provider expects a fast response. If your handler takes too long, the provider retries, and you end up processing the same event twice despite your idempotency check.
Dunning: what happens when payments fail
Dunning is the system that manages failed payment retry schedules. Poor dunning design is one of the most common sources of preventable churn in SaaS.
A retry schedule that works in practice:
| Day | Action | |-----|--------| | 0 | Payment fails, subscription moves to past_due, customer notified | | 3 | Automatic retry | | 5 | Reminder with link to update payment method | | 7 | Automatic retry | | 10 | Warning that access will be suspended | | 14 | Final retry, subscription canceled if it fails |
The 14-day grace period is important. Many payment failures in MENA are technical rather than intentional: expired cards, temporarily insufficient credit, bank-side issues. A two-week window with clear communication recovers a large portion of these without any action from the customer.
Do not suspend service immediately on day 0. Suspending a paying customer's account due to a transient technical failure creates support load and damages trust disproportionate to the risk.
Proration for upgrades and downgrades mid-cycle
When a customer changes their plan mid-billing-cycle, you owe them an accurate calculation of what they have already paid for versus what they owe for the new plan.
func CalculateProration(currentPrice, newPrice float64, daysRemaining, cycleLength int) float64 {
dailyCurrent := currentPrice / float64(cycleLength)
dailyNew := newPrice / float64(cycleLength)
unusedCredit := dailyCurrent * float64(daysRemaining)
newCharge := dailyNew * float64(daysRemaining)
return newCharge - unusedCredit // positive for upgrade, negative for downgrade
}
For upgrades, charge the difference immediately to unlock the additional features. For downgrades, apply the credit to the next billing cycle rather than issuing a refund. Record the full proration calculation in your database, not just the final amount. When a customer calls about a confusing invoice charge, you need to be able to explain every number.
Testing subscription edge cases
Subscription logic testing is harder than most application testing because the interesting cases are the edge cases. The scenarios that matter most:
- Upgrade on the last day of the billing cycle
- Downgrade on the first day of a new cycle
- Cancellation during the grace period after a failed payment
- Resubscription with a different plan after cancellation
- Payment fails exactly at the renewal date
- Customer changes payment method while in past_due status
Use your payment provider's sandbox environment with a time manipulation helper to test scenarios that require specific timing. Never test billing logic against real customer data.
Key lessons from production in MENA
Payment failure rates in Lebanon and the broader MENA region run higher than in Western markets for structural reasons: prepaid cards are common, banking infrastructure has reliability issues, and some customers prefer to load specific amounts rather than keeping ongoing credit. Design your dunning logic to be patient and communicative rather than immediately punitive.
Support alternative payment methods. Cash on delivery, kiosk payment networks, and local digital wallets are used by customers who do not have reliable card payment access. These require different lifecycle handling but open your platform to a customer segment that would otherwise be excluded.
Build the audit trail first. Every dispute about billing comes down to needing a complete record of what happened and when. The subscription events table is not a nice-to-have; it is how you resolve disputes without guesswork.
Enjoying this article?
Enter your email and get a clean, formatted PDF of this article - free, no spam.
Not sure where to start?
Voxire builds subscription billing infrastructure and SaaS backends for teams in Lebanon and the MENA region. If you are implementing subscription management for a new platform or fixing a manual process that has outgrown your team, we can help.
https://voxire.com/get-a-quote/



