Back to Course

Spot the Tax · Card 10 of 20

Business logic shouldn't know which payment provider you use

Why BillingService calling Stripe::Charge.create directly makes the service nearly impossible to test or evolve.

The code

What will this cost you in six months?

class BillingService
  def charge(user, amount_cents)
    result = Stripe::Charge.create(
      amount: amount_cents,
      currency: "usd",
      customer: user.stripe_customer_id
    )
    Payment.create!(user: user, stripe_id: result.id, amount_cents: amount_cents)
  end
end

The problem

BillingService is supposed to hold the high-level business logic for charging users and recording payments. By calling Stripe::Charge.create directly, it gets coupled to one specific payment vendor's SDK. Every test that touches the service has to stub Stripe at the SDK level, every dev environment needs Stripe API keys, and the day someone wants to add PayPal for European customers, you end up rewriting BillingService instead of just adding a new gateway.

Take a moment. Before revealing, think about how you'd make BillingService testable without stubbing the Stripe SDK, and how you'd add a second provider without rewriting the service.