Back to Ramp questions
CodingSoftware Engineer

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:

text
merchant_name, amount, currency, created_at

A 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).

python
def find_subscriptions(csv_data: str, min_occurrences: int = 3) -> list[Subscription]:
    ...

Examples

text
# 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

python
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

python
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 results

Key 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 k charges 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:

python
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