Upgrade to Pro — share decks privately, control downloads, hide ads and more …

State-machine Driven Payment Flow

Avatar for Yisong Wu Yisong Wu
November 25, 2019

State-machine Driven Payment Flow

As the world's top-grossing mobile app, payment processing is a crucial part of Tinder. Scaling the payment system is very challenging with a legacy, untestable code base. To resolve the problem, we've recently made the decision to build a state-machine-based, pluggable solution to represent a universal purchase flow, which is agnostic of any specific payment methods. It's powered by our own open source library: https://github.com/Tinder/StateMachine.

In this talk, we will share how our state machine / DSL approach ensures the flow is deterministic, self-documented and easily testable and how its plug-in system guarantees flexibility by allowing dynamic custom rules to be run along the state transitions.

Avatar for Yisong Wu

Yisong Wu

November 25, 2019
Tweet

Other Decks in Programming

Transcript

  1. State / Event - Driven Solutions Trending topic nowadays Huge

    benefits - we have some experience We’ll share our approach to solve real problems
  2. Tinder total subscribers, in millions 1.0M 2.0M 3.0M 4.0M 5.0M

    6.0M Q1 2015 Q2 2015 Q3 2015 Q4 2015 Q1 2016 Q2 2016 Q3 2016 Q4 2016 Q1 2017 Q2 2017 Q3 2017 Q4 2017 Q1 2018 Q2 2018 Q3 2018 Q4 2018 Q1 2019 Q2 2019 Q3 2019 5.7 5.2 4.7 4.3 4.1 3.8 3.4 3.1 2.6 2.1 1.9 1.6 1.4 1.1 0.9 0.7 0.5 0.4 0.1
  3. 1.0M 2.0M 3.0M 4.0M 5.0M 6.0M Q1 2015 Q2 2015

    Q3 2015 Q4 2015 Q1 2016 Q2 2016 Q3 2016 Q4 2016 Q1 2017 Q2 2017 Q3 2017 Q4 2017 Q1 2018 Q2 2018 Q3 2018 Q4 2018 Q1 2019 Q2 2019 Q3 2019 5.7 5.2 4.7 4.3 4.1 3.8 3.4 3.1 2.6 2.1 1.9 1.6 1.4 1.1 0.9 0.7 0.5 0.4 0.1 Q4 2019 Q1 2020 Q2 2020 Q3 2020 Q4 2020 Q1 2021 Q2 2021 Q3 2021 ... ... ... ... ?
  4. Great power, great responsibility Money-handling code NEEDS to be solid

    …and still we need to move fast How? We’re doing well but…
  5. • Predictable • Self-documented • Testable • Observable Finite State

    Machine Flexible Extensible Easy to maintain ? ? ?
  6. • Predictable • Self-documented • Testable • Observable Plug-in system

    Finite State Machine Flexible Extensible Easy to maintain
  7. Plug-in system • Flexible • Extensible • Easy to maintain

    Predictable Self-documented Testable Observable ? ? ?
  8. Finite State Machine Plug-in system • Flexible • Extensible •

    Easy to maintain Predictable Self-documented Testable Observable
  9. Highly used in compilers to specify the grammar and syntax

    One state at a given time Guaranteed output, upon every specific input Makes the code deterministic Leads devs to think of all cases Finite state machine (automaton)
  10. Finite state machine (automaton) Highly used in compilers to specify

    the grammar and syntax One state at a given time Guaranteed output, upon every specific input Makes the code deterministic Leads devs to think of all cases ac aaac acdc
  11. State Machine Driven Payment Flow Load payment data Validate payment

    data Charge for the purchase Verify the receipt
  12. Completed Idle State Machine Driven Payment Flow Pre Validating Running

    Biller Verifying Receipt Loading Data Pure State Representation sealed class PurchaseState { data class Idle( val purchaseData: PurchaseData ) : PurchaseState() … }
  13. fun create(): StateMachine<State, Event, SideEffect> { val stateMachine: StateMachine<State, Event,

    SideEffect> = create { initialState(Idle) State Machine Flow Idle
  14. State Machine Flow Idle Load Data Loading Data OnStart state<Idle>

    { on<OnStarted> { event -> transitionTo( state = LoadingData(event.initialData), sideEffect = LoadData ) } } fun create(): StateMachine<State, Event, SideEffect> { val stateMachine: StateMachine<State, Event, SideEffect> = create { initialState(Idle) Cache / Database https://github.com/Tinder/StateMachine
  15. State Machine Flow - Loading Data Idle Load Data Loading

    Data OnStart Loading Data Failed OnFailure Pre Validating OnSuccess Pre Validate state<LoadingData> { on<OnLoadingDataSucceeded> { event -> transitionTo( state = PreValidating(event.purchaseData), sideEffect = RunPreValidation ) } on<OnLoadingDataFailed> { event -> transitionTo( state = LoadingDataFailed( purchaseData = event.purchaseData, reason = event.reason ) } } Cache / Database
  16. Loading Data Failed Cache / Database State Machine Flow -

    Pre Validating Idle Load Data Loading Data OnStart Running Biller Run Biller OnSuccess Pre Validation Failed OnFailure OnFailure Pre Validating OnSuccess Pre Validate state<PreValidating> { on<OnPreValidationSucceeded> { event -> transitionTo( state = RunningBiller(event.purchaseData), sideEffect = RunBiller ) } on<OnPreValidationFailed> { event -> transitionTo( state = PreValidationFailed( purchaseData = event.purchaseData, interruption = event.interruption ) ) } }
  17. Loading Data Failed State Machine Flow - Running Biller Idle

    Load Data Loading Data OnStart Running Biller Run Biller OnSuccess Verifying Receipt Server OnSuccess Running Biller Failed Pre Validation Failed OnFailure OnFailure OnFailure Pre Validating OnSuccess Pre Validate state<RunningBiller> { on<OnBillingSucceeded> { event -> transitionTo( state = VerifyingReceipt(event.purchaseData), sideEffect = VerifyReceipt ) } on<OnBillingFailed> { event -> transitionTo( state = BillingFailed( purchaseData = purchaseData, reason = event.reason ) ) } }
  18. Loading Data Failed State Machine Flow -Verifying Receipt Idle Load

    Data Loading Data OnStart Running Biller Run Biller OnSuccess Verifying Receipt Server OnSuccess Completed Post Process OnSuccess Running Biller Failed Verifying Receipt Failed Pre Validation Failed OnFailure OnFailure OnFailure OnFailure Pre Validating OnSuccess Pre Validate state<VerifyingReceipt> { on<OnVerificationSucceeded> { transitionTo( state = Completed(purchaseData), sideEffect = RunPostPurchaseProcessing ) } on<OnVerificationFailed> { event -> transitionTo( state = FailedToVerifyReceipt( purchaseData = purchaseData, reason = event.reason ) ) } }
  19. Immutable Purchase Data class PurchaseData( val purchaseable: Purchasable, val billerType:

    BillerType, val subscriptionContext: SubscriptionContext, val receipt: Receipt ) interface Purchaseable { val id: String } interface BillerType interface SubscriptionContext interface Receipt
  20. Immutable Purchase Data class PurchaseData( val purchaseable: Purchasable, val billerType:

    BillerType, val subscriptionContext: SubscriptionContext, val receipt: Receipt ) interface Purchaseable { val id: String } interface BillerType interface SubscriptionContext interface Receipt Idle Pre Validating Running Biller Verifying Receipt Completed Loading Data Purchase Data
  21. when (transition.sideEffect) { is LoadData -> loadData(purchaseData) is RunPreValidation ->

    preValidatePurchase(purchaseData) is RunBiller -> runningBiller(purchaseData) … }.subscribeBy( onSuccess = { handleEvent(it) } ) } fun handleEvent(event: Event) { val transition = stateMachine.transition(event) Flow Coordinator
  22. internal class RunBiller @Inject constructor( private val billerResolver: BillerResolver )

    { override fun invoke(paymentData: PaymentData): Single<PaymentData> { val biller = billerResolver.resolve(paymentData.billerType) return biller.purchase(paymentData.purchasable) } } Delegate business logic Run Biller
  23. internal class RunBiller @Inject constructor( private val billerResolver: BillerResolver )

    { override fun invoke(paymentData: PaymentData): Single<PaymentData> { val biller = billerResolver.resolve(paymentData.billerType) return biller.purchase(paymentData.purchasable) } } Delegate business logic private fun resolve(billerType: BillerType): Biller { return when (billerType) { GoogleBillerType -> googleBiller GiftCardBillerType -> giftCardBiller } }
  24. internal class RunBiller @Inject constructor( private val billerResolver: BillerResolver )

    { override fun invoke(paymentData: PaymentData): Single<PaymentData> { val biller = billerResolver.resolve(paymentData.billerType) return biller.purchase(paymentData.purchasable) } } private fun resolve(billerType: BillerType): Biller { return when (billerType) { GoogleBillerType -> googleBiller GiftCardBillerType -> giftCardBiller } } class GoogleBiller: Biller { fun purchase(purchaseable: Purchasable) } Delegate business logic
  25. Testability @Test fun givenFromState_withTransitionEvent_shouldGoToExpectedState() { // Given val stateMachine =

    givenStateIs(fromState) // When val transition = stateMachine.transition(event) // Then assertThat(transition).isInstanceOf(Transition.Valid::class.java) assertThat(transition.toState).isEqualTo(toState) assertThat(transition.sideEffect).isEqualTo(sideEffect) }
  26. Testability companion object { @Parameterized.Parameters( name = "{index} - fromState:

    {0}; event: {1}; toState: {2}; sideEffect: {3}" ) @JvmStatic fun data() = listOf( testCase( fromState = State.Idle(), event = Event.OnStarted(purchaseData), toState = State.LoadingData(purchaseData), sideEffect = SideEffect.LoadData ) …
  27. Plug-in System Each payment method can define its own set

    of concrete dependencies Flexibility is guaranteed by the use of interfaces and DI <interface> <interface> <interface> <interface> <interface>
  28. Plug-in System Each payment method can define its own set

    of concrete dependencies Flexibility is guaranteed by the use of interfaces and DI Composable rules <interface> <interface> <interface> <interface> <interface> ? <interface> <interface> <interface> ?
  29. Idle Pre Validating Loading Data state<LoadingData> { on<OnLoadingDataSucceeded> { event

    -> transitionTo( state = PreValidating(event.purchaseData), sideEffect = RunPreValidation ) } } Composable rules
  30. Pre Validate Idle Pre Validating Loading Data state<LoadingData> { on<OnLoadingDataSucceeded>

    { event -> transitionTo( state = PreValidating(event.purchaseData), sideEffect = RunPreValidation ) } } Composable rules
  31. Rules Resolver Idle Pre Validating Loading Data Run Pre-Validation state<LoadingData>

    { on<OnLoadingDataSucceeded> { event -> transitionTo( state = PreValidating(event.purchaseData), sideEffect = RunPreValidation ) } } Coordinator Rules Processor Composable rules
  32. Rules Resolver Rule 1 Pre Validating tion Rule 2 Rule

    3 Pre-Validation Rules interface PreValidationRulesResolver { fun resolve(purchaseData: PurchaseData): PreValidationRulesChain } interface PreValidationRule { fun perform(purchaseData: PurchaseData): } ResultAction sealed class ResultAction { data class Proceed( val additionalInfoItem: CustomAdditionalInfoItem ) : ResultAction() object Interrupt : ResultAction() }
  33. Pre-Validation Rules Rules Resolver Rule 1 Rule 2 Rule 3

    Idle Pre Validating Loading Data Run Pre-Validation class @Inject constructor() : PreValidationRule { override fun perform( purchaseContext: PurchaseContext ): ResultAction { // ... some successful verification ... return Proceed() } } Rule1
  34. Pre-Validation Rules Rules Resolver Rule 1 Rule 2 Rule 3

    Idle Pre Validating Loading Data Run Pre-Validation Rule2 class @Inject constructor() : PreValidationRule { override fun perform( purchaseContext: PurchaseContext ): ResultAction { // ... some successful verification ... return Proceed() } } Rule1
  35. Pre-Validation Rules Rules Resolver Run Pre-Validation Rule 1 Rule 2

    Rule 3 Idle Loading Data Pre Validating Rule3 Rule2 Rule1 class @Inject constructor() : PreValidationRule { override fun perform( purchaseContext: PurchaseContext ): ResultAction { // ... some successful verification ... return Proceed() } }
  36. Rules Resolver Rule 1 Rule 2 Rule 3 Pre-Validation Rules

    state<PreValidating> { on<OnPreValidationSucceeded> { event -> transitionTo( state = RunningBiller(event.purchaseData), sideEffect = RunBiller ) } . . . Running Biller Idle Pre Validating Loading Data
  37. Pre-Validation Rules - Error handling Rules Resolver Rule 1 Rule

    2 Rule 3 Idle Pre Validating Loading Data Run Pre-Validation state<LoadingData> { on<OnLoadingDataSucceeded> { event -> transitionTo( state = PreValidating(event.purchaseData), sideEffect = RunPreValidation ) } }
  38. Pre-Validation Rules - Error handling Rules Resolver Rule 1 Rule

    2 Rule 3 Idle Pre Validating Loading Data Run Pre-Validation Rule1 // ... some successful verification ... return Proceed() class @Inject constructor() : PreValidationRule { override fun perform( purchaseContext: PurchaseContext ): ResultAction { } }
  39. Pre-Validation Rules - Error handling Rules Resolver Rule 1 Rule

    3 Idle Pre Validating Loading Data Rule 2 Rule2 Rule1 Run Pre-Validation class @Inject constructor() : PreValidationRule { override fun perform( purchaseContext: PurchaseContext ): ResultAction { } }
  40. Pre-Validation Rules - Error handling Rules Resolver Run Pre-Validation Rule

    1 Rule 2 Idle Loading Data Pre Validating // ... some verification which failed ... return Interrupt Rule2 Rule1 class @Inject constructor() : PreValidationRule { override fun perform( purchaseContext: PurchaseContext ): ResultAction { } }
  41. Pre-Validation Rules on<OnPreValidationFailed> { event -> transitionTo( state = PreValidationFailed(

    purchaseData = event.purchaseData, interruption = event.interruption ) ) } Pre-Validation Failed Idle Pre Validating Loading Data Rules Resolver Rule 1 Rule 2
  42. Pre-Validation Rules Resolution - concrete example class GooglePayPrePurchaseRuleResolver @Inject constructor(

    private val checkGoogleDataIntegrity: CheckGoogleDataIntegrity, private val checkPurchaseConsistency: CheckPurchaseConsistency, private val attachPreviousTransaction: AttachPreviousTransaction ) : PreValidationRulesResolver { override fun resolve(purchaseData: PurchaseData): PreValidationRulesChain { return PreValidationRulesChain() } }
  43. Pre-Validation Rules Resolution - concrete example checkGoogleDataIntegrity checkPurchaseConsistency attachPreviousTransaction Rules

    Resolver .addRule(attachPreviousTransaction) .addRule(checkPurchaseConsistency) .addRule(checkGoogleDataIntegrity) class GooglePayPrePurchaseRuleResolver @Inject constructor( private val checkGoogleDataIntegrity: CheckGoogleDataIntegrity, private val checkPurchaseConsistency: CheckPurchaseConsistency, private val attachPreviousTransaction: AttachPreviousTransaction ) : PreValidationRulesResolver { override fun resolve(purchaseData: PurchaseData): PreValidationRulesChain { return PreValidationRulesChain() } }
  44. @Test fun resolve_givenUpgradePurchase_shouldIncludeAllApplicableRules() { // When val rulesChain = rulesResolver.resolve(purchaseData)

    // Then assertThat(rulesChain.getRules()).containsExactly( checkGoogleDataIntegrity, checkPurchaseConsistency, attachPreviousTransaction ) } Pre-Validation Rules Resolution - concrete example checkGoogleDataInte checkPurchaseConsist attachPreviousTransac Rules Resolver .addRule(attachPreviousTransaction) .addRule(checkPurchaseConsistency) .addRule(checkGoogleDataIntegrity) class GooglePayPrePurchaseRuleResolver @Inject constructor( private val checkGoogleDataIntegrity: CheckGoogleDataIntegrity, private val checkPurchaseConsistency: CheckPurchaseConsistency, private val attachPreviousTransaction: AttachPreviousTransaction ) : PreValidationRulesResolver { override fun resolve(purchaseData: PurchaseData): PreValidationRulesChain { return PreValidationRulesChain() } }
  45. Pre-Validation Rule - concrete example class CheckPurchaseConsistencyRule @Inject constructor() :

    PreValidationRule { override fun perform(purchaseData: PurchaseData): ResultAction { val subscriber = purchaseData.subscriptionType val productType = purchaseData.productType return when (subscriber buying productType) { PlusUser buying Plus, GoldUser buying Gold, GoldUser buying Plus -> Interrupt else -> Proceed() } } } internal infix fun UserSubscriptionType.buying(productType: ProductType) = this to productType
  46. Pre-Validation Rule - concrete example class CheckPurchaseConsistencyRule @Inject constructor() :

    PreValidationRule { override fun perform(purchaseData: PurchaseData): ResultAction { val subscriber = purchaseData.subscriptionType val productType = purchaseData.productType return when (subscriber buying productType) { PlusUser buying Plus, GoldUser buying Gold, GoldUser buying Plus -> Interrupt else -> Proceed() } } } internal infix fun UserSubscriptionType.buying(productType: ProductType) = this to productType @RunWith(Parameterized::class) class CheckPurchaseConsistencyRuleTest(. . .) { . . . companion object { @Parameterized.Parameters(name = "{index}: {0} buying {1} should {2}") @JvmStatic fun data() = listOf( testCase(NonSubscriber, buyingProduct = Plus, expectedAction = Proceed()), testCase(NonSubscriber, buyingProduct = Gold, expectedAction = Proceed()), testCase(NonSubscriber, buyingProduct = Boost, expectedAction = Proceed()), testCase(NonSubscriber, buyingProduct = SuperLike, expectedAction = Proceed()) testCase(NonSubscriber, buyingProduct = TopPicks, expectedAction = Proceed()), testCase(PlusUser, buyingProduct = Plus, expectedAction = Interrupt), testCase(PlusUser, buyingProduct = Gold, expectedAction = Proceed()), testCase(PlusUser, buyingProduct = Boost, expectedAction = Proceed()), testCase(PlusUser, buyingProduct = SuperLike, expectedAction = Proceed()), testCase(PlusUser, buyingProduct = TopPicks, expectedAction = Proceed()), testCase(GoldUser, buyingProduct = Plus, expectedAction = Interrupt), testCase(GoldUser, buyingProduct = Gold, expectedAction = Interrupt), testCase(GoldUser, buyingProduct = Boost, expectedAction = Proceed()), testCase(GoldUser, buyingProduct = SuperLike, expectedAction = Proceed()), testCase(GoldUser, buyingProduct = TopPicks, expectedAction = Proceed()) ) }
  47. !

  48. !

  49. !

  50. Rules Resolver Fire pre-like analytics event Check if user can

    keep liking Confirm like Enqueue api request Fire post-swipe analytics . . . !
  51. Rules Resolver Enqueue api request Fire post-swipe analytics . .

    . ! Fire pre-like analytics event Check if user can keep liking Confirm like
  52. Rules Resolver Fire pre-like analytics event Check if user can

    keep liking Confirm like ! Enqueue api request Fire post-swipe analytics . . .
  53. !

  54. F C ! class CheckIfUserCanKeepLiking @Inject constructor( ... ) :

    SwipeValidationRule { ... override fun perform(swipe: Swipe): PreValidator { return when { userIsNonSubscriber && (swipe.isLike) && userExceededDailyLikeLimit -> { Interrupt } else -> Proceed } } }
  55. Rules Resolver Check if user can keep liking Composable rules

    - a design pattern Fire pre-like analytics event FSM + Composite Rules also drive Tinder's cards engine It actually inspired the new purchase architecture Around 1 Billion Likes / Passes a day just on Android
  56. New Purchase Outcomes Increased purchase KPI just by re-architecting the

    code. Developed a code base we love Successfully applied on multiple projects