Thomas Bandt

Technology Choices For My SaaS In Retrospect

Two years ago, I started out building a new software product. From the start, it would require mobile, web, and backend components to make it work, so I had to make quite a few technology choices. Here is how those turned out.

Published on Thursday, 22 September 2022

While some of those decisions were easy, I spent quite some time contemplating others. So I thought it would be a good idea to look back and see whether or not my assumptions back then would hold true over time.

The Context

The product itself is almost boring from a technological perspective. It's not simply forms over data, but at the end of the day, there is not much about it that would require sophisticated engineering – at least not in the early days when product/market fit has not been validated. Think of a platform that allows businesses to interact with their customers regularly and in a more structured manner than, let's say, through e-mail.

The platform's core is represented by a server-side application that communicates with client apps through an API. Those client apps consisted of a mobile app and a web app right from the start.

The Strategy

My goal was to bootstrap the product in its early phases with as little effort and resources as possible. However, my design capabilities are limited, and I did not want to build the mobile app myself after being primarily in the mobile space for almost a decade.

So I hired some folks to take over those tasks and went knee-deep into designing and coding the backend and the web app. I was looking for a pragmatic approach that was sustainable enough for the early stage of the project and sufficiently flexible to adjust while (hopefully) growing larger over time.

The Technological Choices

The Mobile App

The choice seemed obvious as a long-term dotnet developer who spent the last years building B2B mobile apps with Xamarin. However, I did not want to spend a large part of my limited budget on building two polished user interfaces, one for iOS and one for Android. Xamarin.Forms was about to be discontinued soon, but dotnet MAUI was far from being production-ready at the time. So I went with Flutter instead, which promised to allow us to build a more cohesive code base with less platform-dependent code needed than Xamarin.

The outcome: Within a few weeks, we had the first version of the app up and running for both iOS and Android, which was great in terms of "time to market." The UI of our app had and still has its glitches, but those were and are neglectable for a B2B use case like ours. Behind its bold and shiny promises, Flutter suffers from many bugs and problems, but nothing has turned out to be a show-stopper. I don't like Dart as a language, though. This wouldn't be a problem if, because of limited resources, I wouldn't be required to learn it at some point. So maybe it would have been better to stick with Xamarin or choose React Native at that point.

API

Where did I want to go with the API – scale it to the moon? No. It only needed to do its job for my first (hopefully someday existing) set of customers. So I immediately forced myself to forget about learning Kubernetes and everything related to serverless architectures and the like. Instead, I decided to build a monolith. A single but modularized dotnet application, written in F# and following the onion architecture. I also ignored the hype about GraphQL and went with a good old REST approach, which seemed reasonable enough as I had complete control over all client apps.

The outcome: The API has been incredibly robust and very fast (given the little money I spent on hosting – more on that later), which caused me very little trouble while deployed in production. I can't prove it, but I am sure that a lot of that success can be attributed to the "applied functional programming" approach I used here. Also, F# code is beautiful and incredibly easy to read and comprehend. So far, the application relies on only a handful of community-built open-source libraries. So the fact that the F# ecosystem is still tiny and relatively slow-moving is not an issue here (kudos to Zaid for Npgsql.FSharp!).

Persistence

I grew up–professionally–with Microsoft SQL Server. But to be honest, I had no interest whatsoever in spending money on license fees for a database system. So I went with PostgreSQL, which I already used with my last product. There is a single Postgres database that serves all the backend needs (read: the API application), including some queuing mechanisms. However, as the product also requires blob storage, I had to address this, too. So I went with… the server's file system.

The outcome: It just works. Working with Postgres is a joy. Its jsonb data type allows me to mix up relational and NoSQL techniques in a very reliable and comfortable way. However, I will probably never memorize its JSON query syntax. Storing the blobs on disk was also a great decision, as implementing something like AWS S3 at the time would not have benefited the system's few users.

Web App

I spent a lot of time contemplating the technology decisions that had to be made for the web app, as my gut told me to choose Fable and F#. Also, there were a lot of plausible choices in terms of libraries and frameworks, even if I went for JavaScript or TypeScript, namely Angular, Svelte, Vue, and React. Eventually, I decided to use React and TypeScript and to build a SPA. Another important decision I made pretty early on was to adopt Tailwind CSS.

The outcome: React just works, TypeScript just works. As with Dart, I do not necessarily enjoy writing TypeScript, and I do not enjoy reading it much. Although it has some excellent features, e.g., concerning discriminated unions, which I would love to see in F# one day. However, the overall developer experience is good: build times are insanely low (compared to mobile development or the slow F# compiler [sigh!]), there is a package for nearly every complex UI problem, and the whole application model of React makes sense to me, looking at it from a functional programming angle. About Tailwind CSS: Having written my first bits of CSS in the late 1990s, it has freed me of so much cognitive load that I consider it invaluable today.

Infrastructure

With the decision to build a monolith, the hosting question suddenly became a lot easier to answer. I rented a single Linux machine from Hetzner, which runs three Docker containers: One for the Postgres database, one for the dotnet API, and finally one with nginx hosting the SPA. All of that is built and deployed automatically through GitHub Actions.

The outcome: While simultaneously deploying client and backend applications obviously leads to downtimes, those are short and neglectable for now. The whole process is slim and reliable, and so is the cost structure. Hetzner is actually a bargain.

Wrapping It Up

I am pretty happy with most of the decisions I made. I think I spent my innovation tokens well this time, primarily investing them into working with F# and functional programming on a broader scale than I was able to do in previous projects.

Speaking of F#, it is one of three programming languages in the project: F#, TypeScript, and Dart. As with other things in life, three is one too many. Even though dotnet MAUI as a platform might not be as mature and capable as Flutter even today, I would probably go for it just for not having to learn and maintain a code base written in a language I do not regularly use (or like) myself. Maybe it would have been even better to go with a hybrid native/web approach for the mobile app. This means building a mobile app that only hosts the web app and extends it with a few native features, like preprocessed file uploads.

However, the web and mobile apps do a great job, and users love them. So now, the remaining vital questions do not include software engineering or programming so much anymore but revolve around sales and marketing. And that, my friend, is something for another post.

What do you think? Drop me a line and let me know!