Skip to main content

devlog 2 - eventbus and saas model

·5 mins

It’s been a while since I wrote a devlog. I aim to do these more regularly, but I got busy, sidetracked, and simply forgot. I kept reminding myself to get back to it but never did.

In today’s devlog, I want to discuss the custom event bus I’ve written in Go to handle internal event delivery within my codebase. I have numerous events that need to trigger various actions, so this was a crucial component. Additionally, I want to touch on pivoting and changing my mind regarding the product’s direction, whether it pertains to internal system components, the development approach, or other aspects.

Initially, I aimed to build this product as a self-hosted solution with a perpetual licence, and that remains a goal. However, I’ve also decided to support, or potentially support, a SaaS model in the future. For this reason, I’ve shifted from using SQLite to Postgres.

SQLite is a fantastic product and database, and I will undoubtedly use it for future projects. However, the current product is becoming more complex, and supporting a SaaS model introduces the challenge of managing tenanted SQLite databases. While this is likely feasible, especially with tools like libSQL, I prefer to avoid this complexity for now. For a simpler product, I might explore libSQL as a solution.

Event Bus #

Integrating an external system like Stripe into an application can be complex. However, if you only need to respond to a handful of events and perform specific tasks based on those events, it can be straightforward. For example, you receive a webhook for a charge, update a user’s subscription within your app, and you are done.

In my situation, I wanted to consume as many Stripe events as possible and build custom workflows based on multiple events and scenarios. This required a more robust solution. My goal was to provide users with the ability to trigger custom workflows, which necessitated access to as many events as Stripe emits. As of writing this, Stripe can emit 229 different event types, and I aim to support as many custom workflows as possible. Writing handling logic for all of these events would be complex, so I am utilising code generation and an internal event bus library that I’ve built to simplify this process.

The event bus utilises the publish/subscribe design pattern to ensure loose coupling and separation of concerns between components. It allows any service within my codebase to easily publish or subscribe to events using a simple function call with the appropriate Golang struct. This enables any service to subscribe to a particular event effortlessly.

For example the subscription for an event containing the stripe.Change entity it would look something like this.

eventbus.Subscribe[*stripeevents.Event[stripe.Charge]]()(eventbus.HandlerFunc[*stripeevents.Event[stripe.Charge]](s.HandleChargeEvent))

This event can be triggered by 6 of the 229 stripe.EventType we use a switch statement to catch these events publishing the event in the correct stripe struct. In this example we’re publishing a stripe.Charge entity.

case stripe.EventTypeChargeExpired, stripe.EventTypeChargeFailed, stripe.EventTypeChargePending, stripe.EventTypeChargeRefundUpdated, stripe.EventTypeChargeRefunded, stripe.EventTypeChargeSucceeded, stripe.EventTypeChargeUpdated:
    if err := publish(ctx, data, stripe.Charge{}, inEventType); err != nil {
        slog.Error("publishing event", "event_type", inEventType, "error", err)
        if errors.Is(err, ErrEventDataParsing) {
            return http.StatusBadRequest
        }
        return http.StatusInternalServerError
    }

The publisher is far simpler, requiring just a simple call to the generic Publish function:

eventbus.Publish[Event[T]]()(ctx, e)

Currently, the library relies on the singleton pattern, which means there is a single instance of the event bus shared across the entire application. This approach simplifies access and management of events, but it can also introduce limitations in flexibility and scalability. In the future, I may refactor this to support multiple instances to better handle different use cases and improve modularity. However, in its current form, it suits my needs.

Eventually, I plan to open-source the event bus library, but I feel like right now it lacks some features that I want, such as middleware support and improved concurrent handling of event handlers.

SaaS Model #

I’ve decided to potentially support a SaaS model of the product earlier in the development process. While I could have probably built this using the original database choice, SQLite, it presented complexities that I preferred to avoid at this stage. Although libSQL might have simplified some aspects, it introduced its own challenges. Despite being a fork of SQLite, libSQL lacks the extensive support and documentation that SQLite offers, making it harder to ensure everything is implemented correctly. The differences between libSQL and SQLite added another layer of difficulty, complicating the alignment between the two.

In the future, I might explore and utilise the multi-tenant features of libSQL for another project. However, given the current complexities I’m already managing, I decided it was best to reduce potential issues by not incorporating libSQL at this time.

Next devlog #

In my next devlog, I hope to demo some of the onboarding process for my application. When a user signs up, we need to request the Stripe API Key and Stripe Webhook Secret from the user. These values must be stored securely in the database to prevent accidental leaks and ensure that bad actors cannot read them, even if they are accidentally returned to the user. Access to the secrets will be managed through specific functions that handle decryption securely.