Recurring Subscription Detection
Role: Software Engineer
Given a list of financial transactions, identify which ones represent recurring subscriptions by detecting consistent weekly or monthly charge patterns.
Note: Ramp places heavy emphasis on code cleanliness. Interviewers expect well-named variables, clear logic flow, and minimal complexity. Write production-quality code, not just working code.
Problem Statement
You are given transaction records as CSV with the following fields:
merchant_name, amount, currency, created_atA transaction is a subscription if the same (merchant_name, amount, currency) triple appears at least k = 3 times (configurable) at a consistent interval — either exactly 7 days apart (weekly) or exactly 1 calendar month apart (monthly).
def find_subscriptions(csv_data: str, min_occurrences: int = 3) -> list[Subscription]:
...Examples
# Weekly — same charge every 7 days, 3+ times
Netflix $15.99 2024-01-01
Netflix $15.99 2024-01-08
Netflix $15.99 2024-01-15
→ Subscription: Netflix $15.99/week
# Monthly — same day each calendar month, 3+ times
Spotify $9.99 2024-01-05
Spotify $9.99 2024-02-05
Spotify $9.99 2024-03-05
→ Subscription: Spotify $9.99/month
# Not detected — gaps are inconsistent
Starbucks $5.00 2024-01-01
Starbucks $5.00 2024-01-10
Starbucks $5.00 2024-02-20
→ (skipped)Data Model
from dataclasses import dataclass
from enum import Enum
class Interval(Enum):
WEEKLY = "week"
MONTHLY = "month"
@dataclass(frozen=True)
class Transaction:
merchant_name: str
amount: int # stored in cents
currency: str
def formatted_amount(self) -> str:
return f"${self.amount / 100:.2f}"
@dataclass(frozen=True)
class Subscription:
transaction: Transaction
interval: Interval
def __str__(self) -> str:
return (
f"{self.transaction.merchant_name}: "
f"{self.transaction.formatted_amount()} / {self.interval.value}"
)Implementation
import csv
import io
from collections import defaultdict
from datetime import datetime, date
def _consecutive_pairs(dates: list[date]):
return zip(dates, dates[1:])
def _is_weekly(a: date, b: date) -> bool:
return (b - a).days == 7
def _is_monthly(a: date, b: date) -> bool:
return (b.year - a.year) * 12 + (b.month - a.month) == 1
def _has_consecutive_streak(dates: list[date], length: int, interval_check) -> bool:
unique_sorted = sorted(set(dates))
if len(unique_sorted) < length:
return False
streak = 1
for a, b in _consecutive_pairs(unique_sorted):
if interval_check(a, b):
streak += 1
if streak >= length:
return True
else:
streak = 1
return False
def _detect_interval(charge_dates: list[date], min_occurrences: int) -> Interval | None:
if _has_consecutive_streak(charge_dates, min_occurrences, _is_weekly):
return Interval.WEEKLY
if _has_consecutive_streak(charge_dates, min_occurrences, _is_monthly):
return Interval.MONTHLY
return None
def _parse_transactions(csv_data: str) -> dict[Transaction, list[date]]:
charges: dict[Transaction, list[date]] = defaultdict(list)
for row in csv.DictReader(io.StringIO(csv_data)):
txn = Transaction(
merchant_name=row["merchant_name"],
amount=int(row["amount"]),
currency=row["currency"],
)
charges[txn].append(datetime.fromisoformat(row["created_at"]).date())
return charges
def find_subscriptions(csv_data: str, min_occurrences: int = 3) -> list[Subscription]:
charges = _parse_transactions(csv_data)
results = []
for txn, dates in charges.items():
interval = _detect_interval(dates, min_occurrences)
if interval:
results.append(Subscription(txn, interval))
return resultsKey Design Notes
Grouping key: (merchant_name, amount, currency) — the same merchant charging different amounts is treated as a distinct transaction type.
Deduplication: Duplicate charges on the same date are collapsed via set() before streak detection.
Weekly vs monthly priority: Weekly is checked first. A charge every 28 days would match monthly; a charge every 7 days matches weekly.
Calendar month: Detected via month difference ((b.year - a.year) * 12 + (b.month - a.month) == 1), which handles month-length variation correctly.
Edge Cases
- Duplicate same-day charges: Deduplicated — one occurrence per day
- Gap in streak: Counter resets; multiple sub-streaks within the data are each evaluated
- Fewer than
kcharges total: Short-circuits early — can't form a streak - Monthly boundary (e.g., Jan 31 → Feb 28): Month-diff check passes; day mismatch is ignored
Follow-ups
- How would you handle subscriptions where the amount fluctuates slightly (e.g., due to tax or proration)?
- How would you detect biweekly or quarterly intervals?
- How would you process this as a live stream of transactions rather than a static file?
- How would you identify a cancelled subscription — one that used to recur but recently stopped?
Reported sample data: https://assets.ramp.com/interview/recurring_transactions/sample_transactions.txt
Variant (independent report — verbatim prompt)
A community report shared a screenshot of the prompt as actually delivered (reporter's context: they had this at Ramp, "but they changed it"). The delivered version asks only for weekly charges:
Your company runs a personal finance app that helps its users track how they spend their money. Your goal is to identify recurring subscriptions so that a user may cancel unused ones.
You have been provided a CSV file with one user's transactions. Each row corresponds to one transaction and contains the timestamp the transaction occurred, formatted as an ISO-8601 string. Find all recurring charges that happen WEEKLY, then print the merchant, amount, and interval.
Example output:
"OrangeNews: $10.00 / week"
The starter snippet fetches the sample data over HTTP:
import requests
"https://assets.ramp.com/interview/recurring_transactions/sample_transactions.txt")Treat the weekly-only phrasing as one delivered variant of a rotating prompt — the general solution above (weekly + monthly) subsumes it.
Source: community report, March 2026