FastPrepAsynchronous Event Processing

Asynchronous Event Processing

Stripe logoStripe● MediumNEW GRADINTERNOA

Problem statement

Stripe handles millions of payments per day, amounting to billions of dollars of transactions. As you can imagine, this has a huge compute load as a consequence. Because of that, Stripe has been optimized to handle a lot of asynchronous events (think API calls). This allows us to distribute the load over time and leaves our architecture reliable against traffic spikes.

For this question, you will be building a small-scale system that can receive these events, parse and aggregate them into useful states and data. Events with the same event_id should not be processed again.

The question is broken into 4 parts, and we recommend you work on them in order one at a time.

Input (STDIN) For Custom Testing:

  • 1 line with n, the number of events
  • n lines, each containing a single csv shaped string representing one event

Output:

  • One line per aggregated payment, with comma-separated values (no header row)
  • Column order:
    payment_id,merchant_id,state,amount_authorized,amount_captured,last_event_at
  • Numeric columns default to 0 when no value has been accumulated (e.g. payment_1,merchant_1,created,0,0,0)
  • Output payments in the order they were created (i.e., the order their create event was first processed)

Part 1 - Aggregating Payment States

You will be completing the function:

process_events(events: list[str]) -> list[str]

The function receives a list of events that need to be processed, in the order they were received. Each event has the following properties:

  • event_id: string: unique id of the event
  • event_time: int
  • event_type: string: payment | refund | merchant_update
  • payment_id: string: payment id of the event
  • payment_event_type: string: create| authorize | capture
  • merchant_id: string: merchant id associated to that payment event
  • amount: int

If an input property is null or empty it will be represented with a -. The output should not include - and leave no characters if the property is not present.

Aggregate all payment events in order to keep the most up-to-date information of a payment, having the correct state for it. For this part, you will only be working with payment event types.

A payment will only ever be attached to a single merchant.

For context, a basic payment has 2 stages: authorization and capture. Authorization for a card means that you, the buyer, have authorized the payment for your card, but no money has moved yet. A merchant (a.k.a. seller) can capture the payment later, and that is when the money actually moves.

A payment is considered partially_captured if a capture has been made, but the amount_captured < amount_authorized. A payment is considered successful after the full amount authorized has been captured, basically when amount_authorized == amount_captured. You can assume that the amount captured will never exceed the amount authorized.

The expected transition states between payment events:

Current Payment StateValid Events
Payment non-existentcreate
createdauthorize
authorizedauthorize, capture
partially_capturedcapture
successful-

For this part, you can assume that the events will always come in the correct order and multiple authorize and capture events can exist for the same payment.

After all events have been processed, we want the following string for each payment_id:

payment_id,merchant_id,state,amount_authorized,amount_captured,last_event_at

A payment event comes in the shape:

event_id,event_time,event_type,payment_id,payment_event_type,merchant_id,amount
  • merchant_id is only present on create events
  • amount is only present on authorize and capture events

Part 2 - Ignoring out-of-order events

Great, our system is processing millions of events. But now we have noticed a new issue: we are getting some events out-of-order of what a valid payment state should accept. For example, a payment that is in partially_captured should not accept an authorize event.

We need to make sure that only valid events are processed. events that are invalid should simply be ignored.

Current Payment StateValid Events
Payment non-existentcreate
createdauthorize
authorizedauthorize, capture
partially_capturedcapture
successful-

Part 3 - Processing refunds

Now that our payments events can handle the correct and incorrect states, we want to also support refunds.

Refunds come in via a separate event type and come in only with the payment_id as the event_data. After receiving a refund event only the state and the last_event_at properties change.

event_id,event_time,event_type,payment_id,payment_event_type,merchant_id,amount

Now the new acceptable state transitions are:

Payment StateValid Events
Payment non-existentcreate
createdauthorize
authorizedauthorize, capture
partially_capturedcapture
successfulrefund
refunded-

Part 4 - Receiving merchant events

In addition to payment and refund events, your system must now process merchant risk updates.

Each merchant has a risk score represented by an integer from 0 through 100, inclusive. A higher score indicates greater risk.

A merchant update uses the same CSV column order as the previous parts:

event_id,event_time,event_type,payment_id,payment_event_type,merchant_id,amount

For a merchant update:

  • event_type is merchant_update.
  • merchant_id identifies the merchant.
  • amount contains the merchant's risk score.
  • payment_id and payment_event_type are empty (-).

For example:

evt_1,0,merchant_update,-,-,merchant_1,95

In order to process the payments correctly, we want to flag high risk merchants, and block all new authorizations from being processed.

When a valid merchant_update event has a risk score greater than or equal to 80, the merchant becomes blocked.

Blocking takes effect immediately:

  • Ignore every authorize event received for that merchant after the merchant becomes blocked.
  • This applies whether the payment was created before or after the merchant became blocked.
  • Continue to accept create events for the merchant.
  • Continue to process valid capture and refund events. This allows payments authorized before the merchant was blocked to complete their lifecycle.
  • A merchant update does not directly change any existing payment's state, amounts, or last_event_at.

If an authorization is ignored for a payment in the created state, that payment remains created. Payments that were already authorized or partially captured keep their current state and can continue to receive valid capture events.

Once blocked, a merchant remains blocked for the rest of the input. A later merchant update with a risk score below 80 does not unblock the merchant.

FastPrep callable adapter

For the FastPrep callable adapter, implement processEvents with one events array parameter. The array contains the n event strings (n == events.length), so the separate count line used by custom STDIN is not passed to the function. Language-specific starter code provides the corresponding parameter type.

Function

processEvents(events: String[]) → String[]

Examples

Example 1

events = ["evt_1,0,payment,payment_1,create,merchant_1,-","evt_2,1,payment,payment_2,create,merchant_2,-","evt_3,2,payment,payment_1,authorize,-,10","evt_4,3,payment,payment_2,authorize,-,25","evt_5,4,payment,payment_1,capture,-,10","evt_6,5,payment,payment_2,capture,-,5"]return = ["payment_1,merchant_1,successful,10,10,4","payment_2,merchant_2,partially_captured,25,5,5"]

Example 2

events = ["evt_1,0,payment,payment_1,create,merchant_1,-","evt_2,1,payment,payment_2,create,merchant_2,-","evt_3,2,payment,payment_1,authorize,-,10","evt_4,3,payment,payment_1,authorize,-,10","evt_5,4,payment,payment_2,authorize,-,15","evt_6,5,payment,payment_1,capture,-,10","evt_6,5,payment,payment_1,capture,-,10","evt_7,6,payment,payment_2,capture,-,15"]return = ["payment_1,merchant_1,partially_captured,20,10,5","payment_2,merchant_2,successful,15,15,6"]

Note: The amount_captured is not updated twice since the last event has an ID that was already processed.

Example 3

events = ["evt_1,0,payment,payment_1,create,merchant_1,-","evt_2,1,payment,payment_2,capture,-,20","evt_3,2,payment,payment_1,capture,-,10","evt_4,3,payment,payment_2,create,merchant_2,-","evt_5,4,payment,payment_1,authorize,-,10","evt_6,5,payment,payment_2,authorize,-,20","evt_7,6,payment,payment_1,capture,-,10","evt_8,7,payment,payment_1,authorize,-,5","evt_9,8,payment,payment_2,capture,-,10","evt_10,9,payment,payment_2,capture,-,10"]return = ["payment_1,merchant_1,successful,10,10,6","payment_2,merchant_2,successful,20,20,9"]

Note: evt_5 is ignored because a capture is not valid for a created payment. evt_6 is ignored because an authorize is not valid for a partially_captured payment.

Example 4

events = ["evt_1,0,payment,payment_1,create,merchant_1,-","evt_2,1,payment,payment_2,create,merchant_2,-","evt_3,2,payment,payment_1,authorize,-,10","evt_4,3,payment,payment_2,authorize,-,20","evt_5,4,payment,payment_1,capture,-,10","evt_6,5,payment,payment_2,capture,-,20","evt_7,6,refund,payment_1,-,-,-","evt_8,7,refund,payment_2,-,-,-","evt_9,8,refund,payment_1,-,-,-"]return = ["payment_1,merchant_1,refunded,10,10,6","payment_2,merchant_2,refunded,20,20,7"]

Example 5

events = ["evt_1,0,payment,payment_1,create,merchant_1,-","evt_2,1,payment,payment_1,authorize,-,10","evt_3,2,merchant_update,-,-,merchant_1,85","evt_4,3,payment,payment_2,create,merchant_1,-","evt_5,4,payment,payment_2,authorize,-,20","evt_6,5,payment,payment_1,capture,-,10","evt_7,6,merchant_update,-,-,merchant_1,10"]return = ["payment_1,merchant_1,successful,10,10,5","payment_2,merchant_1,created,0,0,3"]

Constraints

  • Events with the same event_id should not be processed again.
  • A payment will only ever be attached to a single merchant.
  • You can assume that the amount captured will never exceed the amount authorized.
  • Numeric columns default to 0 when no value has been accumulated (e.g. payment_1,merchant_1,created,0,0,0)
  • Output payments in the order they were created (i.e., the order their create event was first processed)
  • Each merchant has a risk score represented by an integer from 0 through 100, inclusive.
  • Once blocked, a merchant remains blocked for the rest of the input. A later merchant update with a risk score below 80 does not unblock the merchant.

More Stripe problems

See Stripe hiring insights
Saved
public String[] processEvents(String[] events) {
    // Write your code here.
}
events["evt_1,0,payment,payment_1,create,merchant_1,-","evt_2,1,payment,payment_2,create,merchant_2,-","evt_3,2,payment,payment_1,authorize,-,10","evt_4,3,payment,payment_2,authorize,-,25","evt_5,4,payment,payment_1,capture,-,10","evt_6,5,payment,payment_2,capture,-,5"]
expected["payment_1,merchant_1,successful,10,10,4", "payment_2,merchant_2,partially_captured,25,5,5"]
Checking account…