github logo
Get Source Code

Accepting payments in GoLang using Stripe

Here is the GitHub Repo I won't be going over the process of setting up your stripe account or what the payment process is with Stripe. This guide serves to show a clean & reusable implementation which allows you to process payments with any vendor (Stripe is used in this guide). In the same way, even though this guide is implemented in Go, it is easily understandable and can be used with any programming language you use for your application. The frontend is written in React and interacts with the GoLang application as a REST api.

The Problem

The problem is that we offer multiple services in our application: shopping (cart), donations, one-off purchases. Each of these services span across one or more packages in our application. We need to collect payment for all these services. How would we go about that? One option is to implement payment in each of the packages. However, if you've been coding for a while, you will realise that this can easily become living hell for your application. I will show you the process of implementing a single package for payments using the dependency inversion principle.

Our API (written in GoLang)

The payments package

This package will be responsible for handling everything that has to do with a payment. Initialisation, webhooks, etc. You might wonder how our code will know which product is being purchased by the customer and how purchases will be fulfilled. Well, that is where the ClientManager comes in. The ClientManager is a custom struct which allows other packages or pieces of code to register themselves as a client of the 'payments' package which means they can have their products purchased by supplying a name for their service, a slug, and implementing an interface: TransactionManager. The implemented transaction manager is responsible for retrieving the item our customer is attempting to purchase and also fulfilling the purchase once payment has been authorised. We won't be covering webhooks with stripe today as it is beyond the scope of this tutorial.

Let's start off with the payments/client_manager.dart file

payments/client_manager.dart

package payments
// payments/client_manager.go

type TransactionManager interface {
    GetItem(string) (*Item, error) // When we receive the item id in the request, we will send that id to the client to obtain information about the item (such as the price and name)
    CompletePurchase(*PurchaseDetails) (bool, error) // Once the purchase has been confirmed we inform the client with details of the purchase
}

// When we call GetItem on the transaction manager, the client fills the struct with the necessary data and returns it
type Item struct {
    Name        string
    Description string
    Price       float32
}

// Just a struct to carry details about the purchase back to our client once the order is fulfilled
type PurchaseDetails struct {
    ItemID string
}

// This is how the client is identified and how we know how to process a payment
type Client struct {
    Name      string
    Slug      string
    TXManager TransactionManager
}


type ClientManager interface {
    RegisterClient(*Client) // When a piece of code needs to accept payments for a product, they register themselves as a client
    findClientBySlug(string) *Client // We will use this to find the relevant client to the product
}

// Concrete implementation of the ClientManager interface
type clientManager struct {
    clients map[string]*Client
}

func (m *clientManager) RegisterClient(c *Client) {
    // When a client tries to get registered, we make sure they are not already registered.
    // If they are not, we can add them to the clients map. 
    // A map is used for constant-time retrieval
    if _, ok := m.clients[c.Slug]; ok {
        log.Fatalf("%v is already a registered service", c.Slug)
    }

    m.clients[c.Slug] = c
}

func (m *clientManager) findClientBySlug(clientSlug string) *Client {
    // The slug in this example is obtained from the url
    // You can also send it in with post data

    client, ok := m.clients[clientSlug]

    // The requested client may not be registered.
    // If that is the case, we just return nil. 
    // The API handler will handle it appropriately
    if ok {
        return client
    }

    return nil
}


// Just a factory. Hides the concrete implementation of the ClientManager
func NewClientManager() ClientManager {
    return &clientManager{
        clients: make(map[string]*Client),
    }
}

Everything above has already been explained. Usage by client code will be shown at the end of the guide.

payments/service.go

Following common patterns in golang, we will separate the logic from the api handler by creating a service struct (that conforms to an interface) and passing that into the controller.

The process of collecting payments with stripe involves interacting with the Stripe api to create a payment intent. This returns a ClientSecret which is sent back to the frontend and passed into the Stripe elements. Oh, and I may have not mentioned it already but our frontend is written in React.

package payments
// payments/service.go

import (
    "errors"

    "github.com/stripe/stripe-go/v72"
    "github.com/stripe/stripe-go/v72/paymentintent"
)

type Service interface {
    initialiseStripePayment(string, string) (string, error)
}

type service struct {
    clientManager ClientManager
}

func newService(cm ClientManager) Service {
    return &service{
        clientManager: cm,
    }
}


func (srvc *service) initialiseStripePayment(clientSlug, itemId string) (string, error) {
    // Remember the client manager? We get the clientSlug from the request when our customer tries to make a purchase
    // We then get the client from the manager
    client := srvc.clientManager.findClientBySlug(clientSlug)

    // If there is no client for that slug, it means it's a bad request. the wrong service was requested
    if client == nil {
        return "", errors.New("this service does not exist")
    }

    // We then retrieve the transaction manager from the client, responsible for handling transactions.
    // We then ask it to find us the item requested by the customer. 
    item, err := client.TXManager.GetItem(itemId)

    // If there is no item then it's also a bad request
    if err != nil {
        return "", errors.New("this product does not exist")
    }


    // Now, we initialise the payment intent with the relevant data
    params := &stripe.PaymentIntentParams{
        Amount:   stripe.Int64(int64(item.Price)),
        Currency: stripe.String(string(stripe.CurrencyUSD)),
        PaymentMethodTypes: stripe.StringSlice([]string{
            "card",
        }),
    }
    // this is very important because we need the item id for webhooks, etc as all metadata will be sent back by stripe
    params.AddMetadata("item_id", itemId)

    // this makes a request to stripes api to create the payment intent
    intent, err := paymentintent.New(params)

    // if for some reason this fails, it's from stripe. nothing we can do 
    if err != nil {
        return "", errors.New("could not initialise payment. An unexpected error occured")
    }

    // IMPORTANT: Ideally you should save information about the payment in a database (ie. the clientSlug, itemId, etc.) so it can be retrieved later, however it is beyond the scope of this guide.

    // return the intent's client secret. this is used by the frontend
    return intent.ClientSecret, nil
}

I must mention that we are not doing any database related stuff here because I am not willing to complicate things. However, typically you would store information about the client, the item, the amount, in the database so you can retrieve it later.

payments/handler.go

In some frameworks, this is called the controller. When using go, I prefer to call it the handler. This is very straightforward and simple to implement.

package payments
// payments/handler.go

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

// Our Handler Models
type Response struct {
    Message string      `json:"message"`
    Data    interface{} `json:"data"`
    Error   interface{} `json:"error"`
}

type InitialisePaymentRequest struct {
    ItemId string `json:"itemId" binding:"required"`
}

type InitialisePaymentResponse struct {
    ClientSecret string `json:"clientSecret"`
}

// Interface for the handler to hide the concrete implementation
type Handler interface {
    initialisePayment(*gin.Context)
}

type handler struct {
    service Service
}

// factory
func newHandler(s Service) Handler {
    return &handler{
        service: s,
    }
}

func (h *handler) initialisePayment(c *gin.Context) {
    var req InitialisePaymentRequest
    // get the client slug from the url params (not query params!)
    clientSlug := c.Params.ByName("service")

    err := c.BindJSON(&req)

    if err != nil {
        c.JSON(http.StatusBadRequest, Response{
            Message: "Could not initialise payment",
            Error:   err.Error(),
        })
        return
    }

    // Call the service to initialise the payment and return the client secret so we can return it to the frontend
    stripeClientSecret, err := h.service.initialiseStripePayment(clientSlug, req.ItemId)

    if err != nil {
        c.JSON(http.StatusBadRequest, Response{
            Message: "Failed to get checkout url",
            Error:   err.Error(),
        })
        return
    }

    c.JSON(http.StatusOK, Response{
        Message: "Payment initialised successfully",
        Data: &InitialisePaymentResponse{
            ClientSecret: stripeClientSecret,
        },
    })
}

payments/setup.go

Now, we create our setup code which is responsible for initialising our package ie. setting up the routes, injecting dependencies, etc. I always let this serve as the entrypoint into my packages. This means that in our main.go file, we can just call payments.Setup with the relevant parameters like database, etc. You might notice that we are injecting our ClientManager into the payments package. This is because other packages also need to access the ClientManager so it will be initialised in the main.go file and passed down. Dependency Injection is awesome right?!

package payments
// payments/setup.go

import (
    "github.com/gin-gonic/gin"
    "github.com/stripe/stripe-go/v72"
)

func setupStripe() {
    stripe.Key = "{{YOUR STRIPE SECRET KEY}}"
}

func setupRouter(router *gin.Engine, handler Handler) {
    p := router.Group("/payments")
    {
        p.POST(":service/initialise/", handler.initialisePayment)
    }
}

func Setup(router *gin.Engine, paymentsClientManager ClientManager) {
    service := newService(paymentsClientManager)
    handler := newHandler(service)

    setupStripe()
    setupRouter(router, handler)
}

This is really all we need to do in our payments package for this guide. It's really that simple. The benefits of having a single package to handle payments are that we reduce code duplication for the different packages that needs access to payments.

Using the payments package

To demonstrate usage of our payments package, I will create another package: cart. Now, this could be whatever you want. The first step is to implement the TransactionManager interface. We must implement this interface so the payments package knows how to handle our transactions.

cart/transaction_manager.dart

package cart
// cart/transaction_manager.dart

import "github.com/jayndu/stripe-payments/payments"

type CartTransactionManager struct {
}

func (m *CartTransactionManager) GetItem(id string) (*payments.Item, error) {
    // Typically get this data from a database using the provided id
    // Example: Select cart from cart_table where id = ${id}.
    // We would then calculate the total cost of the cart...
    // I'm hardcoding it cos it makes life easier for all of us

    return &payments.Item{
        Name:        "Item 1234",
        Description: "Test Description",
        Price:       12394,
    }, nil
}

func (m *CartTransactionManager) CompletePurchase(details *payments.PurchaseDetails) (bool, error) {
    // Now, we would perhaps dispatch the items in the cart or do whatever it is we need to do once
    // we have claimed the funds
    return true, nil
}

As usual, I will create my setup.go file to setup what we need for the cart package.

cart/setup.go

package cart

import (
    "fmt"

    "github.com/jayndu/stripe-payments/payments"
)

func setupPayments(clientManager payments.ClientManager) {
    clientManager.RegisterClient(&payments.Client{
        Name:      "Cart Client",
        Slug:      "cart",
        TXManager: &CartTransactionManager{},
    })
}

// We get the client manager injected in. Typically, we should also have the db, and router injected too and anything else we need
func Setup(paymentsClientManager payments.ClientManager) {
    fmt.Println("setting up cart...")

    setupPayments(paymentsClientManager)
}

Now, it's time for the main entrypoint of our application: main.go. There is really nothing spectacular going on in main.go other than initialising all our packages.

package main
// main.go

import (
    "github.com/gin-gonic/gin"
    "github.com/jayndu/stripe-payments/cart"
    "github.com/jayndu/stripe-payments/payments"
)

func main() {
    r := gin.Default()

    // Get ourselves a fresh client manager
    paymentsClientManager := payments.NewClientManager()

    // inject it into the cart package
    cart.Setup(paymentsClientManager)

    // inject it into the payments package
    payments.Setup(r, paymentsClientManager)

    r.Run() // listen and serve on 0.0.0.0:8080
}

The frontend

I won't go into the details of the frontend. It's just one page that interacts with our API and uses thee Stripe Elements. However, the whole project is accessible on my GitHub as a public repo with instructions on setup. Running it will get you this:

Image

Conclusion

I spent quite a while trying to figure out how I could create a reusable payments package. In fact, this is my first time implementing this in go. I usually implement it in python and it has worked fine. I really hope this was a good read for all of you.

You can support me by following my Twitter or my Instagram