Event Flows
The Subscription Service participates in the FloatMe event-driven architecture in four ways: it publishes subscription lifecycle events to a Kinesis stream via a DynamoDB Stream feeder, it consumes scheduled EventBridge rules to trigger collection and notification runs, it reacts to real-time balance signals via an SQS webhook worker, and it consumes Kinesis streams from the User Service and Payments Service for membership and ACH callbacks.
Overview
Subscription Service
│
┌───────────────────────┼─────────────────────────┐
│ │ │
▼ ▼ ▼
Publishes Consumes Consumes
Kinesis events scheduled EventBridge SQS queues and
(outbound) rules (inbound) Kinesis streams
│ │ (inbound)
▼ ▼ │
{env}-subscriptions collections-job ┌─────┴──────┐
Kinesis stream notifier-scheduler │ │
subscription- invoked on schedule Webhook Kinesis
updated events to queue work Worker Consumers
consumed by to SQS (balance) (memberships,
downstream ach-handler)
services
Events Published
subscription-updated
Published to the site-subscriptions Kinesis stream by the kinesis-feeder Lambda. The feeder is triggered by a DynamoDB Stream on the billing-activity table and forwards every INSERT or MODIFY change to Kinesis as a wrapped FMSDK event. REMOVE events are silently skipped.
| Field | Value |
|---|---|
Kinesis stream |
|
Event source |
|
Event version |
|
Event type |
|
Payload |
Full |
Trigger |
Every |
The event type is overridden when a membership lifecycle event (such as CANCEL) writes a specific updated_event value into the DynamoDB record before the feeder sees the stream record. For example, a CANCEL membership event sets updated_event = "PENDING_CANCELLATION" and the feeder emits that as the Kinesis event type instead of the default.
Downstream consumers (e.g. analytics pipelines, other services) subscribe to this Kinesis stream independently. The Subscription Service does not manage those subscriptions.
Consumed Kinesis Streams
site-user-service-users (Memberships)
Consumed by the memberships Lambda. The stream carries FMSDK-wrapped membership lifecycle events published by the User Service. The processor unmarshals each record into a Membership struct and routes on event_type.
| Event Type | Action |
|---|---|
|
Locks the subscription; updates |
|
Applies pause logic — advances the subscription date by the pause duration months. |
|
Retracts a previously applied membership change. |
|
Unpauses the subscription and schedules the next billing date. |
|
Unpauses and immediately triggers a charge attempt. |
|
Handles account closure — cancels any open subscriptions. |
Any event type not in the list above is logged as unsupported and the record is skipped.
site-payments (ACH Handler)
Consumed by the ach-handler Lambda. The stream carries payment outcome events from the Payments Service. The handler filters for subscription-related ACH debit results and updates billing-activity accordingly, then emits notifications via Segment, Iterable, and AppsFlyer.
See ACH Processing for the full event-type-to-status mapping.
Scheduled Collections
Three CloudWatch EventBridge rules trigger the collections-job Lambda on weekdays. Each rule injects a process field into the event detail that tells the job which collection run to perform. The job queries DynamoDB for qualifying subscriptions and enqueues them to the appropriate SQS queue for the collections-worker Lambda to process.
EventBridge Rules
| Rule | Schedule (UTC) | Injected Detail | Effect |
|---|---|---|---|
|
8:00 AM Mon–Fri |
|
Queries for subscriptions in |
|
7:00 AM Mon–Fri |
|
Queries for subscriptions in |
|
10:00 PM Mon–Fri UTC (16:00 CST / 17:00 CDT) |
|
Queries for subscriptions in |
Scheduler → Worker Flow
The collections-job Lambda supports two trigger types — CloudWatch (initial schedule fire) and SQS (pagination self-invocation when results exceed one page). The pattern is identical for all three process types.
EventBridge rule fires (scheduled / retry / pause)
│
▼
collections-job Lambda invoked via CloudWatch event
detail.process = "scheduled" | "retry" | "pause"
│
▼
Query DynamoDB (billing-activity) for qualifying subscriptions
scheduled: status=SCHEDULED, subscription_date <= today
retry: status=ERROR, subscription_date passed
pause: status=PAUSED, ready for collection
│
▼
For each page of results (up to PAGES_PER_INVOKE):
Enqueue subscriptions to the process-specific SQS queue
│ │
│ └─ If more pages remain:
│ Enqueue pagination message to
│ the process-specific page queue
│ (collections-job re-invoked via SQS)
▼
collections-worker Lambda dequeues from:
{env}-subscription-service-collections-scheduled
{env}-subscription-service-collections-retry
{env}-subscription-service-collections-pause
│
▼
Attempt collection for each subscription
(see xref:collections.adoc[Collections] for decision logic)
Balance Webhook Worker
The webhook-balance-worker Lambda reacts to account balance update events from the Transaction Service. It applies a rich set of balance and collection rules before attempting collection.
Event Path
Transaction Service publishes account balance update
(filter: detail-type=new_account, source=txn-service.feeder,
is_main=true,
available >= 0 OR current >= 0 OR calc_available >= 0)
│
▼
EventBridge rule: {env}-subscription-service-balance-detected
│
▼
SQS: {env}-subscription-service-balance-event-tap
(visibility timeout: 900s, max receive count: 1, DLQ present)
│
▼
webhook-balance-worker Lambda invoked (batch size 10, max concurrency 5)
Event payload: CloudWatch envelope wrapping txn-service Account object
│
▼
Acquire DynamoDB distributed lock for user
│
▼
Populate collection state from account update:
- available balance, current balance, calc_available balance
- institution ID
- user status (short-circuit if inactive)
- subscriptions in ERROR status (within last 2 months)
- subscription ACH history per subscription
- debit card validity
- blocklist status
│
├── no collectable subs ──► release lock, return
│
▼
For each collectable subscription, evaluate collection rules:
ruleUserIsActive
ruleDefaultPinlessIfNotSet
ruleAchIfInvalidDebitCard
rulePinlessIfBlocklisted
ruleDontRetryAchIfBlocklisted
ruleHasValidDebitCard
rulePinlessBalanceCheck (threshold from GB flag)
ruleAchBalanceCheck (threshold from GB flag)
ruleACHLimitNotExceeded
│
├── rules fail ──► emit sub_check metric with reason, skip
│
▼
Attempt collection (pinless debit or ACH)
error code 51 (NSF) + achIf51 flag enabled?
──► fallback to ACH attempt
│
▼
Update subscription status in DynamoDB
Emit notifications: Segment, AppsFlyer (iOS + Android)
Emit Datadog metric: sub_collect
GrowthBook Flags
| Flag | Effect |
|---|---|
|
Pinless debit balance threshold (in cents) for standard-tier subscriptions. Defaults to |
|
Pinless debit balance threshold (in cents) for premium-tier subscriptions. Defaults to |
|
ACH balance threshold (in cents). Defaults to |
|
When |
|
When |
|
When set to |
Notifier
The three-day pre-billing notifier runs as two cooperating Lambdas: notifier-scheduler and notifier-worker.
Flow
EventBridge rule fires (daily, 12:00 UTC — prod only)
rule: pre_subscription_notifier
detail: {"status": "SCHEDULED"}
│
▼
notifier-scheduler Lambda invoked via CloudWatch
Reads target date = today + 4 calendar days
│
▼
Query DynamoDB (billing-activity) via GSI
status = SCHEDULED, subscription_date in [target_date, target_date+1)
Up to PAGES_PER_INVOKE pages per invocation
│
▼
For each page of results:
Wrap each subscription in an FMSDK event
event_type = "three_day_notification"
Batch-send to SQS:
{env}-subscription-service-pre-subscription-notifier-worker
│
│ If more pages remain:
└─► Enqueue pagination cursor to:
{env}-subscription-service-pre-subscription-notifier-scheduler
(notifier-scheduler re-invoked via SQS to continue paging)
│
▼
notifier-worker Lambda invoked (max concurrency 5)
Dequeues from pre-subscription-notifier-worker
│
▼
For each subscription:
Skip if updated_event = PENDING_CANCELLATION
Get user — skip if inactive
Skip non-test-domain emails in test environments
│
▼
Send Iterable track event
event_name: "three_day_notification"
properties: membership_tier, billing_date, amount, account (mask)
│
▼
Prenote user (GrowthBook-gated: subscriptions.prenotes defaults true)
User < 30 days old:
submit prenote to payments service
User >= 30 days old:
require a JPM COMPLETED payment in last 40 days
require active Plaid item
if eligible: submit prenote to payments service
provider = "usio" (GB flag subscriptions.payments.provider)?
──► SubmitUsioPrenote
default:
──► SubmitJpmPrenote
GrowthBook Flags
| Flag | Effect |
|---|---|
|
Master switch for prenote submissions in the notifier worker. Defaults to |
|
When set to |
Related Pages
-
Collections — Full decision logic for scheduled, retry, pause, and webhook collection paths
-
ACH Processing — Kinesis-based ACH payment outcome callbacks and status transitions
-
Memberships — Membership Kinesis consumer and state transitions
-
Notifications — Three-day notifier scheduler and worker in detail
-
Architecture — System context diagram showing all inbound and outbound event paths
-
Infrastructure — SQS queues, EventBridge rules, and Lambda configuration