PracHub
QuestionsPremiumCoachesLearningGuidesInterview Prep

Quick Overview

This question evaluates event-driven scheduling, deterministic time-based ordering, stateful account updates for plan changes and renewals, and algorithmic merging of notification streams in the Coding & Algorithms domain.

  • Medium
  • Stripe
  • Coding & Algorithms
  • Software Engineer

Design a subscription email scheduler with changes and renewals

Company: Stripe

Role: Software Engineer

Category: Coding & Algorithms

Difficulty: Medium

Interview Round: Technical Screen

Implement an email notification scheduler for subscription accounts. Inputs: - send_schedule: a mapping whose keys include: • "start": label to send on an account’s begin_date. • Integer negative offsets (e.g., - 15): labels to send at end_date + offset, where end_date = begin_date + duration. • "end": label to send on end_date. - user_accounts: a list of objects {name: string, plan: string, account_date: int (days), duration: int (days)}. Output: - Emit all notifications in chronological order as lines: "<time>: [<Label>] Subscription for <name> (<plan>)" and terminate after the last message. - When multiple notifications share the same timestamp, apply this deterministic ordering: 1) State changes (defined below) in the order: Changed, then Renewed. 2) "start" events. 3) Relative-offset events sorted by their numeric offset (more negative first). 4) "end" events. - For ties within the same priority bucket, sort by name lexicographically. Sub-questions: (a) Baseline scheduling: Using only send_schedule and user_accounts, generate all scheduled notifications for each account (welcome on begin_date, each negative-offset notification relative to end_date, and expired on end_date) and merge them into the global, ordered output. (b) Plan changes: Add a list changes1 of entries {name: string, new_plan: string, change_date: int}. At change_date, emit "[Changed] Subscription for <name> (<new_plan>)" and update the account’s plan effective immediately; any notifications at the same timestamp and all future notifications must use the updated plan. (c) Renewals/extensions: Now allow a change list where each entry is either a plan change as above or a renewal entry {name: string, extension: int (days), change_date: int}. On a renewal, emit "[Renewed] Subscription for <name> (<current_plan>)" and extend the account’s end_date by extension days from its current end_date. Recompute all future relative-offset and end events to reflect the new end_date. If a renewal and a plan change occur for the same account on the same timestamp, apply the ordering rules above. Constraints/expectations: - Support up to N accounts and M change/renewal events efficiently; describe data structures and time complexity. - Your API design is flexible (e.g., a Notifier class with send_emails(user_accounts, send_schedule, changes=None)). - Clearly state any additional assumptions you make and ensure deterministic output under all tie cases.

Quick Answer: This question evaluates event-driven scheduling, deterministic time-based ordering, stateful account updates for plan changes and renewals, and algorithmic merging of notification streams in the Coding & Algorithms domain.

Part 1: Baseline Subscription Email Scheduling

Implement the baseline notification scheduler for subscription accounts. Given a send_schedule mapping and a list of user_accounts, generate every scheduled notification and return them in global chronological order. The send_schedule may contain 'start', 'end', and negative integer offsets. A 'start' notification is sent on account_date. A negative offset k is sent at end_date + k, where end_date = account_date + duration. An 'end' notification is sent on end_date. When multiple notifications share a timestamp, output 'start' events first, then relative-offset events sorted by numeric offset from most negative to least negative, then 'end' events. Within the same bucket, sort by account name lexicographically.

Constraints

  • 0 <= len(user_accounts) <= 100000
  • Account names are unique.
  • 0 <= duration <= 1000000000
  • account_date may be any integer day value.
  • send_schedule contains at most 100 negative integer offset keys.
  • Negative offset keys are strictly less than 0.

Examples

Input: ({'start': 'Welcome', -15: 'Upcoming expiration', 'end': 'Expired'}, [{'name': 'Alice', 'plan': 'Basic', 'account_date': 0, 'duration': 30}, {'name': 'Bob', 'plan': 'Pro', 'account_date': 10, 'duration': 20}])

Expected Output:

Explanation: Alice and Bob have the same end date, so their relative and end notifications tie by timestamp and are sorted by name.

Input: ({'start': 'Welcome', -10: 'Ten days left', -5: 'Five days left', 'end': 'Expired'}, [{'name': 'Ann', 'plan': 'Basic', 'account_date': 0, 'duration': 10}, {'name': 'Zoe', 'plan': 'Plus', 'account_date': 5, 'duration': 5}])

Expected Output:

Explanation: At time 0, Ann's start event comes before relative-offset events. At time 5, Zoe's start event comes before all relative-offset events.

Input: ({'start': 'Welcome', -1: 'Reminder', 'end': 'Expired'}, [])

Expected Output:

Explanation: No accounts means no notifications.

Input: ({'start': 'Started', 'end': 'Ended'}, [{'name': 'Solo', 'plan': 'Free', 'account_date': 7, 'duration': 0}])

Expected Output:

Explanation: A zero-duration subscription starts and ends at the same timestamp, and start events come before end events.

Solution

def solution(send_schedule, user_accounts):
    events = []

    for account in user_accounts:
        name = account['name']
        plan = account['plan']
        begin = account['account_date']
        end = begin + account['duration']

        if 'start' in send_schedule:
            events.append((begin, 1, 0, name, send_schedule['start'], plan))

        for key, label in send_schedule.items():
            if type(key) is int and key < 0:
                events.append((end + key, 2, key, name, label, plan))

        if 'end' in send_schedule:
            events.append((end, 3, 0, name, send_schedule['end'], plan))

    events.sort(key=lambda event: (event[0], event[1], event[2], event[3]))

    result = []
    for time, _priority, _offset, name, label, plan in events:
        result.append(f'{time}: [{label}] Subscription for {name} ({plan})')
    return result

Time complexity: O(NK log(NK)), where N is the number of accounts and K is the number of scheduled labels per account, including start/end and relative offsets.. Space complexity: O(NK) for the generated event list..

Hints

  1. Convert every account notification into a sortable event tuple.
  2. The sort key should encode both timestamp and the required same-timestamp priority rules.

Part 2: Subscription Scheduling with Plan Changes

Extend the baseline scheduler to support plan changes. In addition to send_schedule and user_accounts, you are given changes1, a list of entries with name, new_plan, and change_date. At change_date, output '[Changed] Subscription for <name> (<new_plan>)' and update that account's plan immediately. Any scheduled notification at the same timestamp, and all future scheduled notifications, must use the updated plan. When multiple events share a timestamp, output Changed events first, then start events, then relative-offset events sorted by numeric offset from most negative to least negative, then end events. Within each bucket, sort by name lexicographically.

Constraints

  • 0 <= len(user_accounts) <= 100000
  • 0 <= len(changes1) <= 100000
  • Account names are unique.
  • Every change references an existing account.
  • At most one plan change exists for the same account at the same timestamp.
  • send_schedule contains at most 100 negative integer offset keys.
  • Negative offset keys are strictly less than 0.

Examples

Input: ({'start': 'Welcome', -5: 'Reminder', 'end': 'Expired'}, [{'name': 'Alice', 'plan': 'Basic', 'account_date': 0, 'duration': 10}], [{'name': 'Alice', 'new_plan': 'Pro', 'change_date': 7}])

Expected Output:

Explanation: The reminder is before the plan change, while the end notification is after it.

Input: ({'start': 'Welcome', 'end': 'Expired'}, [{'name': 'Bob', 'plan': 'Basic', 'account_date': 5, 'duration': 5}], [{'name': 'Bob', 'new_plan': 'Gold', 'change_date': 5}])

Expected Output:

Explanation: The change and start happen at the same timestamp, so the change is emitted first and the start uses the new plan.

Input: ({'start': 'Welcome', 'end': 'Expired'}, [{'name': 'Bob', 'plan': 'Basic', 'account_date': 0, 'duration': 10}, {'name': 'Alice', 'plan': 'Free', 'account_date': 0, 'duration': 10}], [{'name': 'Bob', 'new_plan': 'Enterprise', 'change_date': 10}, {'name': 'Alice', 'new_plan': 'Premium', 'change_date': 10}])

Expected Output:

Explanation: Events are sorted by name within the Changed bucket and within the end bucket. End notifications use the updated plans.

Input: ({'start': 'Welcome', -3: 'Reminder', 'end': 'Expired'}, [], [])

Expected Output:

Explanation: No accounts and no changes produce no output.

Solution

def solution(send_schedule, user_accounts, changes1):
    current_plan = {}
    events = []

    for account in user_accounts:
        name = account['name']
        current_plan[name] = account['plan']
        begin = account['account_date']
        end = begin + account['duration']

        if 'start' in send_schedule:
            events.append((begin, 1, 0, name, 'scheduled', send_schedule['start'], None))

        for key, label in send_schedule.items():
            if type(key) is int and key < 0:
                events.append((end + key, 2, key, name, 'scheduled', label, None))

        if 'end' in send_schedule:
            events.append((end, 3, 0, name, 'scheduled', send_schedule['end'], None))

    for change in changes1:
        name = change['name']
        new_plan = change['new_plan']
        change_date = change['change_date']
        events.append((change_date, 0, 0, name, 'change', None, new_plan))

    events.sort(key=lambda event: (event[0], event[1], event[2], event[3]))

    result = []
    for time, _priority, _offset, name, kind, label, new_plan in events:
        if kind == 'change':
            current_plan[name] = new_plan
            result.append(f'{time}: [Changed] Subscription for {name} ({new_plan})')
        else:
            result.append(f'{time}: [{label}] Subscription for {name} ({current_plan[name]})')

    return result

Time complexity: O((NK + M) log(NK + M)), where N is the number of accounts, K is the number of scheduled labels per account, and M is the number of plan changes.. Space complexity: O(NK + M) for the event list and O(N) for current plans..

Hints

  1. You can still create a single global event list, but change events must be processed before scheduled notifications at the same time.
  2. Store the current plan for each account separately from the event list so later events see updates.

Part 3: Subscription Scheduling with Plan Changes and Renewals

Extend the scheduler to support both plan changes and renewals. Each entry in changes is either a plan change with name, new_plan, and change_date, or a renewal with name, extension, and change_date. At a plan change, emit '[Changed]' and update the plan immediately. At a renewal, emit '[Renewed]' using the account's current plan, then extend the account's current end_date by extension days. Future relative-offset and end notifications must be recomputed from the new end_date. Events already emitted before the renewal are not revoked. Events at the same timestamp as a state change are treated as future events because state changes are processed before start, relative-offset, and end notifications. Ordering at the same timestamp is: Changed events, Renewed events, start events, relative-offset events sorted by numeric offset from most negative to least negative, then end events. Within each bucket, sort by name lexicographically.

Constraints

  • 0 <= len(user_accounts) <= 100000
  • 0 <= len(changes) <= 100000
  • Account names are unique.
  • Every change or renewal references an existing account.
  • At most one plan change and at most one renewal exist for the same account at the same timestamp.
  • extension is a positive integer.
  • send_schedule contains at most 100 negative integer offset keys.
  • Negative offset keys are strictly less than 0.

Examples

Input: ({'start': 'Welcome', -5: 'Reminder', 'end': 'Expired'}, [{'name': 'Alice', 'plan': 'Basic', 'account_date': 0, 'duration': 10}], [{'name': 'Alice', 'extension': 10, 'change_date': 4}])

Expected Output:

Explanation: The renewal at time 4 moves the end date from 10 to 20, so the old reminder at 5 and old end at 10 are canceled.

Input: ({'start': 'Welcome', -2: 'Reminder', 'end': 'Expired'}, [{'name': 'Alice', 'plan': 'Basic', 'account_date': 0, 'duration': 10}], [{'name': 'Alice', 'extension': 5, 'change_date': 8}, {'name': 'Alice', 'new_plan': 'Pro', 'change_date': 8}])

Expected Output:

Explanation: Even though the renewal appears first in the input, Changed is processed before Renewed at the same timestamp. The old reminder at time 8 is canceled and recomputed for the new end date.

Input: ({'start': 'Welcome', -5: 'Reminder', 'end': 'Expired'}, [{'name': 'Bob', 'plan': 'Basic', 'account_date': 0, 'duration': 10}], [{'name': 'Bob', 'extension': 5, 'change_date': 10}])

Expected Output:

Explanation: The original reminder at 5 has already been emitted. At time 10, renewal happens before the old end event, so the old end is canceled. The new end is 15, which creates a new -5 reminder at time 10.

Input: ({'start': 'Welcome', -1: 'Reminder', 'end': 'Expired'}, [{'name': 'Cara', 'plan': 'Free', 'account_date': 1, 'duration': 2}], [{'name': 'Cara', 'new_plan': 'Plus', 'change_date': 2}])

Expected Output:

Explanation: A plan change at the same timestamp as a relative-offset notification is emitted first, so the reminder uses the new plan.

Input: ({'start': 'Welcome', -3: 'Reminder', 'end': 'Expired'}, [], [])

Expected Output:

Explanation: No accounts and no changes produce no output.

Solution

def solution(send_schedule, user_accounts, changes):
    import heapq

    accounts = {}
    heap = []
    sequence = 0

    offsets = []
    for key, label in send_schedule.items():
        if type(key) is int and key < 0:
            offsets.append((key, label))

    def push(time, priority, offset_order, name, kind, label=None, value=None, version=None):
        nonlocal sequence
        heapq.heappush(heap, (time, priority, offset_order, name, sequence, kind, label, value, version))
        sequence += 1

    def schedule_end_relative_events(name, current_time=None):
        account = accounts[name]
        end = account['end']
        version = account['version']

        for offset, label in offsets:
            event_time = end + offset
            if current_time is None or event_time >= current_time:
                push(event_time, 3, offset, name, 'offset', label, None, version)

        if 'end' in send_schedule:
            if current_time is None or end >= current_time:
                push(end, 4, 0, name, 'end', send_schedule['end'], None, version)

    for account in user_accounts:
        name = account['name']
        begin = account['account_date']
        end = begin + account['duration']
        accounts[name] = {
            'plan': account['plan'],
            'end': end,
            'version': 0
        }

        if 'start' in send_schedule:
            push(begin, 2, 0, name, 'start', send_schedule['start'], None, None)

        schedule_end_relative_events(name)

    for change in changes:
        name = change['name']
        change_date = change['change_date']
        if 'new_plan' in change:
            push(change_date, 0, 0, name, 'change', None, change['new_plan'], None)
        else:
            push(change_date, 1, 0, name, 'renew', None, change['extension'], None)

    result = []

    while heap:
        time, _priority, _offset_order, name, _seq, kind, label, value, version = heapq.heappop(heap)

        if name not in accounts:
            continue

        account = accounts[name]

        if kind in ('offset', 'end') and version != account['version']:
            continue

        if kind == 'change':
            account['plan'] = value
            result.append(f'{time}: [Changed] Subscription for {name} ({account["plan"]})')
        elif kind == 'renew':
            result.append(f'{time}: [Renewed] Subscription for {name} ({account["plan"]})')
            account['end'] += value
            account['version'] += 1
            schedule_end_relative_events(name, time)
        elif kind == 'start':
            result.append(f'{time}: [{label}] Subscription for {name} ({account["plan"]})')
        else:
            result.append(f'{time}: [{label}] Subscription for {name} ({account["plan"]})')

    return result

Time complexity: O((NK + M + RK) log(NK + M + RK)), where N is accounts, K is scheduled labels per account, M is total changes, and R is the number of renewals. Each renewal may reschedule all relative/end events for that account.. Space complexity: O(NK + M + RK) for heap entries, including stale entries that are later skipped, plus O(N) account state..

Hints

  1. A min-heap works well because renewals can create newly scheduled future events while the simulation is running.
  2. Use a per-account version number to invalidate old end-relative events after a renewal.
Last updated: Jun 16, 2026

Loading coding console...

PracHub

Master your tech interviews with 8,000+ real questions from top companies.

Product

  • Questions
  • Learning Tracks
  • Interview Guides
  • Resources
  • Premium
  • For Universities
  • Student Access

Browse

  • By Company
  • By Role
  • By Category
  • Topic Hubs
  • SQL Questions
  • Compare Platforms
  • Discord Community

Support

  • support@prachub.com
  • (916) 541-4762

Legal

  • Privacy Policy
  • Terms of Service
  • About Us

© 2026 PracHub. All rights reserved.

Related Coding Questions

  • Assign Reviewers from Changed Files - Stripe (medium)
  • Generate Account Email Notifications - Stripe (medium)
  • Calculate Transaction Fees - Stripe (medium)
  • Build an Account Transfer Ledger - Stripe (medium)
  • Implement Validation and String Compression - Stripe (hard)