<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Chad Burggraf</title>
    <description>The latest articles on DEV Community by Chad Burggraf (@chadburggraf).</description>
    <link>https://dev.to/chadburggraf</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F670343%2F2ed470a6-cdf2-4ee4-9fbf-cce6f3319457.jpg</url>
      <title>DEV Community: Chad Burggraf</title>
      <link>https://dev.to/chadburggraf</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chadburggraf"/>
    <language>en</language>
    <item>
      <title>How I Build and Deliver B2B SaaS Software as a 1.5* Person Indie Developer</title>
      <dc:creator>Chad Burggraf</dc:creator>
      <pubDate>Tue, 19 Apr 2022 15:04:47 +0000</pubDate>
      <link>https://dev.to/chadburggraf/how-i-build-and-deliver-b2b-saas-software-as-a-15-person-indie-developer-2co</link>
      <guid>https://dev.to/chadburggraf/how-i-build-and-deliver-b2b-saas-software-as-a-15-person-indie-developer-2co</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;This is my stack and my process. It’s not the best and there are many others, but this one is mine 😊&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In January of 2021, I quit my job as the CTO of a healthcare services company to be my own boss. In April 2021 I decided to pivot from my original business idea to a B2B SaaS software product called &lt;a href="https://www.assetbots.com/?utm_medium=social&amp;amp;utm_source=dev.to&amp;amp;utm_campaign=blog&amp;amp;utm_content=how-i-build-and-deliver-b2b-saas-software"&gt;Assetbots&lt;/a&gt;. While the story leading up to the pivot is interesting, I will have to save it for another time. For now, I’d like to talk about my software development process, technology stack, and deployment process.&lt;/p&gt;

&lt;p&gt;In this post, I’ll cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Technology and Process Goals for Indie Founders&lt;/li&gt;
&lt;li&gt;My Tech Stack&lt;/li&gt;
&lt;li&gt;My Hosting Stack&lt;/li&gt;
&lt;li&gt;How I Deploy&lt;/li&gt;
&lt;li&gt;How Much it Costs&lt;/li&gt;
&lt;li&gt;Takeaways and Thoughts for the Future&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Technology and Process Goals for Indie Founders
&lt;/h2&gt;

&lt;p&gt;The primary goal for any indie founder should be &lt;strong&gt;speed.&lt;/strong&gt; Working from that perspective, every choice you make while building your business that slows you down should be evaluated based on its &lt;strong&gt;Return on Investment of Time&lt;/strong&gt; (ROIT). This may seem simple and obvious, but it is important enough that it cannot be overstated.&lt;/p&gt;

&lt;p&gt;I have made many choices that have slowed me down. In fact, building Assetbots has not been particularly fast. However, I constantly review my ROIT to make sure I’m making what I feel are the right compromises for my business. &lt;strong&gt;Even though speed is my primary goal right now, it is not my only one, and whether I am meeting my speed goal or not must be evaluated in context.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In addition to speed, other technology and process goals that I believe are critical for indie founders are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Familiarity&lt;/strong&gt;
You must think about so much more than the technology of the product when building a business, so it is important to limit how much new tech you need to learn.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplicity&lt;/strong&gt;
You will be context switching between development, marketing, sales and administration, so your tech and your processes should be simple and understandable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repeatability&lt;/strong&gt;
It should be easy to do what works over and over again. Both from a process perspective (creating a proposal, keeping track of feedback, evaluating metrics) and from a development perspective (adding a feature, fixing a bug).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price&lt;/strong&gt;
Whether you’re bootstrapping (like me) or not, price matters. Don’t spend $1,000 when you could spend $100, but don’t obsess over getting that down to $10 either.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Of course, all of these goals are nuanced, and every business is unique. However, keeping speed, familiarity, simplicity, repeatability and price in mind as I work my way toward product-market-fit has been invaluable for me and the health of Assetbots so far.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Tech Stack
&lt;/h2&gt;

&lt;p&gt;Assebots is a B2B SaaS delivered exclusively as a web application over the public internet. There are effectively an infinite number of technologies you could choose to build such a product, but here are the ones I use. From the bottom, up:&lt;/p&gt;

&lt;h3&gt;
  
  
  Database
&lt;/h3&gt;

&lt;p&gt;All non-binary data is stored in &lt;a href="https://www.microsoft.com/en-us/sql-server/sql-server-downloads"&gt;Microsoft SQL Server&lt;/a&gt;. Like many of my tech stack choices, I chose SQL Server because of its combination of &lt;strong&gt;familiarity&lt;/strong&gt; and &lt;strong&gt;simplicity&lt;/strong&gt; (in the context of the rest of my stack and my history). While it’s not the best or cheapest tool for the job in the absolute sense, it is both the best and the cheapest for me and my business right now.&lt;/p&gt;

&lt;h3&gt;
  
  
  Server
&lt;/h3&gt;

&lt;p&gt;On the server, Assetbots is written in C# using &lt;a href="https://dotnet.microsoft.com/en-us/download/dotnet/6.0"&gt;.NET 6&lt;/a&gt;. The server is a monolith application that&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Talks HTTP and delivers HTML&lt;/li&gt;
&lt;li&gt;Provides API endpoints&lt;/li&gt;
&lt;li&gt;Handles webhooks&lt;/li&gt;
&lt;li&gt;Talks to clients via websockets&lt;/li&gt;
&lt;li&gt;Spins up &lt;a href="https://nodejs.org/en/"&gt;Node.js&lt;/a&gt; processes&lt;/li&gt;
&lt;li&gt;Collects and reports analytics&lt;/li&gt;
&lt;li&gt;Runs background jobs&lt;/li&gt;
&lt;li&gt;Talks to third-party services&lt;/li&gt;
&lt;li&gt;And more&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It’s the opposite of a &lt;a href="https://en.wikipedia.org/wiki/Microservices"&gt;microservices&lt;/a&gt; architecture. Moreover, it’s all developed in a single Visual Studio solution with over 100 individual projects.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--ciTrEUHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2lbhuoqvgrof8gxoczqq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--ciTrEUHG--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2lbhuoqvgrof8gxoczqq.png" alt="Visual Studio solution" width="880" height="604"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This one solution produces multiple web applications, console applications and client (JavaScript) applications when built – including the marketing site and main web application. A cold build takes almost 5 minutes on my laptop (although most of that time is spent in &lt;a href="https://webpack.js.org/"&gt;Webpack&lt;/a&gt;). Despite this, it’s still a positive ROIT for one reason: &lt;strong&gt;most of this code existed before I pivoted to Assetbots.&lt;/strong&gt; I got a huge head start by not having to develop (or learn!) a database layer, auth layer, permissions system, common app services, build and deployment systems, and more. And despite how slow a cold build is, I rarely have to perform one. &lt;/p&gt;

&lt;h3&gt;
  
  
  Client
&lt;/h3&gt;

&lt;p&gt;On the client, Assetbots is written in TypeScript using &lt;a href="https://reactjs.org/"&gt;React&lt;/a&gt;. Like the server, the client is a &lt;a href="https://en.wikipedia.org/wiki/Monorepo"&gt;monorepo&lt;/a&gt; using &lt;a href="https://yarnpkg.com/features/workspaces"&gt;Yarn workspaces&lt;/a&gt; and contains the code for all of the web applications produced by the Visual Studio solution, as well as some additional services like &lt;a href="https://mjml.io/"&gt;MJML&lt;/a&gt; email templating.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--mQ0Zxg6t--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y2gpvfw7o38pz444km20.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--mQ0Zxg6t--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/y2gpvfw7o38pz444km20.png" alt="Visual Studio Code project" width="880" height="574"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While I use a ton of third-party libraries, a lot of the client is hand-rolled. I think &lt;a href="https://nextjs.org/"&gt;Next.js&lt;/a&gt; and &lt;a href="https://create-react-app.dev/"&gt;create-react-app&lt;/a&gt; are great, but this code has been adapted across multiple projects of mine and is therefore faster for me to ship with.&lt;/p&gt;

&lt;p&gt;A few additional details on my client stack:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I use a hand-written Webpack configuration that is modularized and shared among all my projects. It is slow, but it provides all the batteries.&lt;/li&gt;
&lt;li&gt;I use &lt;a href="https://tailwindcss.com/"&gt;tailwindcss&lt;/a&gt; with a light sprinkling of &lt;a href="https://material.io/design"&gt;Material Design&lt;/a&gt; as a starting point for design and layout. I do not have a designer, so it is essential that I can make attractive, usable interfaces easily.&lt;/li&gt;
&lt;li&gt;There is no Redux, MobX or any other state management to be found. Most state is handled at the feature level using &lt;a href="https://reactjs.org/docs/context.html"&gt;React context&lt;/a&gt;. &lt;/li&gt;
&lt;li&gt;State that is synced with the server is handled using a combination of &lt;a href="https://react-query.tanstack.com/"&gt;React Query&lt;/a&gt; and &lt;a href="https://replicache.dev/"&gt;Replicache&lt;/a&gt;. I’ll be doing a writeup about my Replicache architecture in a future post.&lt;/li&gt;
&lt;li&gt;The client is pre-rendered on the server and then hydrated. The code for this is custom but not overly complicated, and allows me to achieve Next.js-level initial render performance in production:&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oFQmmAEF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/horcmrr8bpzf69jerjm4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oFQmmAEF--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/horcmrr8bpzf69jerjm4.png" alt="Lighthouse report" width="880" height="825"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Marketing
&lt;/h3&gt;

&lt;p&gt;The marketing site is developed exactly like the main web application using the same basic architecture. The big addition is an integration with &lt;a href="https://www.datocms.com/"&gt;DatoCMS&lt;/a&gt; for content management.&lt;/p&gt;

&lt;p&gt;As I’m sure you’ve noticed, there is a theme here. The marketing site can access and make use of all the code developed for the web application, on both the client and the server. &lt;strong&gt;In a vacuum, it would be faster to develop the marketing site using a tool like Next.js, but in context I was able to launch faster this way.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  My Hosting Stack
&lt;/h2&gt;

&lt;p&gt;Assetbots is hosted in &lt;a href="https://azure.microsoft.com/en-us/"&gt;Azure&lt;/a&gt; exclusively through &lt;a href="https://en.wikipedia.org/wiki/Platform_as_a_service"&gt;PaaS services&lt;/a&gt;. I don’t use any virtual machines or containers. I maintain four subscriptions, one for each environment: &lt;strong&gt;development, test, quality assurance and production.&lt;/strong&gt; &lt;/p&gt;

&lt;p&gt;The architecture is very simple and looks like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cmn&lt;/code&gt;
Common services for an entire environment (development, test, QA or production). As of writing, only a &lt;a href="https://azure.microsoft.com/en-us/services/key-vault/#product-overview"&gt;Key Vault&lt;/a&gt; and a &lt;a href="https://docs.microsoft.com/en-us/azure/storage/common/storage-account-overview"&gt;Storage Account&lt;/a&gt; are part of this layer.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;www&lt;/code&gt;
Services for hosting the marketing site. This includes a Key Vault, Storage Account, &lt;a href="https://docs.microsoft.com/en-us/azure/service-bus-messaging/service-bus-messaging-overview"&gt;Service Bus Namespace&lt;/a&gt;, &lt;a href="https://azure.microsoft.com/en-us/products/azure-sql/database/"&gt;Azure SQL&lt;/a&gt; database and an &lt;a href="https://azure.microsoft.com/en-us/services/app-service/"&gt;App Service&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app&lt;/code&gt;
Services for hosting the web application. This includes a Key Vault, Storage Account, Service Bus Namespace, Azure SQL database, &lt;a href="https://azure.microsoft.com/en-us/services/signalr-service/"&gt;SignalR&lt;/a&gt; service and an App Service.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And that’s it. My goal with this is to strike the right balance of cost, reliability and maintenance overhead. This deployment is expensive (more on that below), but not so expensive that it changes how much runway I have. In return for the price, I get four completely isolated environments that are defined entirely in code and have proven extremely reliable so far.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Deploy
&lt;/h2&gt;

&lt;p&gt;There are two types of deployments for Assetbots: infrastructure and code. While I use similar tools for both, they are not identical.&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploying Infrastructure
&lt;/h3&gt;

&lt;p&gt;Infrastructure (in other words, my hosting architecture) is defined entirely in code using a combination of &lt;a href="https://docs.microsoft.com/en-us/azure/azure-resource-manager/management/overview"&gt;Azure Resource Manager&lt;/a&gt; templates and PowerShell scripts. &lt;a href="https://www.terraform.io/"&gt;Terraform&lt;/a&gt; is the standard in the industry, but ARM templates are more than sufficient for my simple use case. Using ARM templates, I’m able to define &lt;strong&gt;a single file that deploys my entire architecture idempotently, in parallel.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When I need to deploy an infrastructure change (for example, I recently upgraded my Node.js version from 14.16.0 to 16.9.1), I update the relevant &lt;code&gt;Template.json&lt;/code&gt; file, commit the change, and push to &lt;code&gt;develop&lt;/code&gt;. Within a second or so, I can navigate to my &lt;a href="https://github.com/features/actions"&gt;GitHub Actions&lt;/a&gt; panel, choose the workflow for the environment I want to deploy to, and click &lt;strong&gt;Run Workflow.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SrEAqRDq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4ep0yx6wej3030ta45d9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SrEAqRDq--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4ep0yx6wej3030ta45d9.png" alt="GitHub Actions workflows" width="412" height="541"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;All the keys required to deploy the infrastructure are stored in the repository’s &lt;strong&gt;Actions Secrets.&lt;/strong&gt; However, if &lt;a href="https://www.githubstatus.com/history"&gt;GitHub is down&lt;/a&gt; and I need to deploy urgently, I can execute the same script via PowerShell from my laptop. It’s a bit more cumbersome because I must add all of the secrets via command-line arguments, but it gets the job done.&lt;/p&gt;

&lt;p&gt;In addition to point-and-click infrastructure deployment, I also automate deployment and teardown of the QA environment daily. This is both to save cost (so it’s only running during the day when I’m using it) and to ensure that I can “easily” spin up a new environment should Azure have a regional outage. If I were serving consumers rather than businesses, &lt;strong&gt;I would probably skip the QA environment altogether until my business was bigger.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Deploying Code
&lt;/h3&gt;

&lt;p&gt;Code is also deployed via GitHub Actions. The process is simple:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A push to the &lt;code&gt;develop&lt;/code&gt; branch triggers a release build and the creation of a &lt;a href="https://docs.github.com/en/repositories/releasing-projects-on-github/managing-releases-in-a-repository"&gt;tagged release&lt;/a&gt;, with deployment packages as assets. This release is marked as a &lt;strong&gt;pre-release.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;One of the triggers of the &lt;code&gt;Code – Deploy QA&lt;/code&gt; workflow is the creation of a new release, so the new release is automatically deployed to QA. The workflow itself invokes a series of PowerShell scripts that download the release, unpack it and deploy it using the &lt;a href="https://docs.microsoft.com/en-us/powershell/azure/install-az-ps"&gt;Azure Az PowerShell module&lt;/a&gt;. This is successful only after a status endpoint that performs several health checks reports that everything is up and running.&lt;/li&gt;
&lt;li&gt;Once I’ve smoke-tested the release manually by navigating around the QA environment in my browser, I merge &lt;code&gt;develop&lt;/code&gt; into &lt;code&gt;main&lt;/code&gt;. This triggers the &lt;code&gt;Code – Deploy Prod&lt;/code&gt; workflow. This workflow promotes the &lt;strong&gt;pre-release&lt;/strong&gt; and deploys it to production. One additional step here is to first deploy to a &lt;a href="https://docs.microsoft.com/en-us/azure/app-service/deploy-staging-slots"&gt;staging slot&lt;/a&gt;, verify the slot’s status endpoint, and then promote the slot to production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I’m happy with this setup overall. It’s extremely easy to rollback a bad release (database migrations notwithstanding), because each of the workflows accept a manual trigger with the release tag name as an optional parameter. I can also use the Azure portal to swap slots with the previous deployment almost instantaneously. There is a lot of flexibility here that doesn’t require waiting for a revert commit to be built, tested and finally deployed.&lt;/p&gt;

&lt;p&gt;The main downside is how long it takes to get the initial pre-release created in the first place: about &lt;strong&gt;20 minutes.&lt;/strong&gt; Promoting a release to production takes about 2 ½ minutes, in comparison.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Much it Costs
&lt;/h2&gt;

&lt;p&gt;I alluded to it above, but this environment is expensive, at least for the scale that I’m at and the service being delivered. I’m aware that I could run a virtual private server somewhere with Postgres and Nginx for basically $0. Even so, here are my most recent invoice numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Development:&lt;/strong&gt; $45.06
This includes $45 for my Visual Studio Professional subscription, so it’s really $0.06 in actual hosting.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test:&lt;/strong&gt; $0.26&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QA:&lt;/strong&gt; $62.90
This breaks down to about $5 for SQL Server and $57 for App Service instances; everything else is a rounding error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production:&lt;/strong&gt; $293.00
My next invoice will be at least $60 cheaper because I’ve canceled a couple of addon services that are not necessary. Again, the bulk of the cost here is in App Service instances: about $150. Another $50 for SignalR and $35 for SQL Server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At my scale, $400 per month matters. It is my biggest single operational expense. But it doesn’t move the needle on the length of my runway, &lt;strong&gt;which is all that really matters.&lt;/strong&gt; Given that, I’m happy with the tradeoffs as they currently stand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways and Thoughts for the Future
&lt;/h2&gt;

&lt;p&gt;Assetbots is made up of some solid engineering. I’m proud of the architecture and the product that architecture enables. Even so, I might make different choices if I were presented with a clean slate to build it from. With 20/20 hindsight, I would:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Make my builds faster by forcing myself onto &lt;a href="https://esbuild.github.io/"&gt;esbuild&lt;/a&gt; from the beginning, even if that makes delivering some capabilities harder (like web workers and service workers, Hot Module Replacement, and so on).&lt;/li&gt;
&lt;li&gt;Make development faster by sharing more code between client and server – for example, running my API endpoints on a platform like &lt;a href="https://deno.land/"&gt;deno&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Make my deployment cheaper by using multiple hosting options for multiple needs – for example &lt;a href="https://vercel.com/"&gt;Vercel&lt;/a&gt; for the marketing site, a container for the MVC bits, and deno for the API endpoints.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then again, the above would be faster and cheaper, but also more complicated. It might be worth it, but who knows?&lt;/p&gt;

&lt;p&gt;As I march forward onboarding customers and searching for product-market-fit, my main concern continues to be speed of feature delivery. For feature delivery, my main bottleneck continues to be how fast my brain can ideate and execute. Until that changes or I run out of runway, I plan to keep things largely as they are, making only incremental improvements.&lt;/p&gt;

&lt;p&gt;Thanks for reading, and please stay in touch if you’d like to follow along as I bootstrap my business by simply &lt;strong&gt;building a better mousetrap.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;*&lt;/strong&gt; &lt;em&gt;The other ½ person is my wife, who helps with pretty much everything while working full-time and doing more than her fair share of raising our two daughters.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>azure</category>
      <category>startup</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>How to Schedule Delayed Jobs on a Specific Queue with Hangfire</title>
      <dc:creator>Chad Burggraf</dc:creator>
      <pubDate>Mon, 19 Jul 2021 21:57:00 +0000</pubDate>
      <link>https://dev.to/chadburggraf/how-to-schedule-delayed-jobs-on-a-specific-queue-with-hangfire-4cmc</link>
      <guid>https://dev.to/chadburggraf/how-to-schedule-delayed-jobs-on-a-specific-queue-with-hangfire-4cmc</guid>
      <description>&lt;p&gt;We use &lt;a href="https://www.hangfire.io/"&gt;Hangfire&lt;/a&gt; at &lt;a href="https://www.assetbots.com/?utm_medium=social&amp;amp;utm_source=dev.to&amp;amp;utm_campaign=blog&amp;amp;utm_content=schedule-delayed-jobs-specific-queue-hangfire"&gt;Assetbots&lt;/a&gt; to manage and coordinate all our background processing and event handling. While Hangfire comes with a lot of great features out of the box, it lacks the ability to &lt;a href="https://discuss.hangfire.io/t/how-schedule-a-delayed-job-to-a-specific-queue/911"&gt;schedule delayed jobs on a specific queue&lt;/a&gt;. Luckily for us, Hangfire’s architecture is extremely simple and extensible, so with just a little bit of custom code we can implement this feature ourselves.&lt;/p&gt;

&lt;h2&gt;
  
  
  Jobs as State Machines
&lt;/h2&gt;

&lt;p&gt;At its core, Hangfire treats jobs as individual state machines. There are a number of &lt;a href="https://github.com/HangfireIO/Hangfire/blob/fb1f5d72defce569ccf5e25b5aab55425b943d90/src/Hangfire.Core/GlobalStateHandlers.cs"&gt;built-in states&lt;/a&gt; that the system ships with, along with their corresponding handlers. Each state’s handler is responsible for transitioning jobs to and from that state in storage.&lt;/p&gt;

&lt;p&gt;For example, the &lt;code&gt;EnqueuedState&lt;/code&gt; &lt;a href="https://github.com/HangfireIO/Hangfire/blob/1df2c716e8c46d0fd0edc5a2d2355c0e47514b8b/src/Hangfire.Core/States/EnqueuedState.cs#L234"&gt;handler&lt;/a&gt; adds jobs to their corresponding queue in storage. Compare that to the &lt;code&gt;ScheduledState&lt;/code&gt; &lt;a href="https://github.com/HangfireIO/Hangfire/blob/1df2c716e8c46d0fd0edc5a2d2355c0e47514b8b/src/Hangfire.Core/States/ScheduledState.cs#L162"&gt;handler&lt;/a&gt;, which sets a timestamp on a custom &lt;code&gt;scheduled&lt;/code&gt; metadata key in storage that indicates when the job should be enqueued.&lt;/p&gt;

&lt;h2&gt;
  
  
  Job Filters as State Machine Middleware
&lt;/h2&gt;

&lt;p&gt;Another core feature of Hangfire’s architecture is the &lt;strong&gt;chain-of-responsibility&lt;/strong&gt; pipeline. This processing pipeline has a number of stages that can be intercepted using &lt;a href="https://docs.hangfire.io/en/latest/extensibility/using-job-filters.html"&gt;job filters&lt;/a&gt;. Each filter can operate on and change the job’s behavior at that point in the pipeline. There can be multiple filters applied, each operating independently, and each applied at different levels of granularity (e.g., at the job or method level, at the class level, or system-wide).&lt;/p&gt;

&lt;p&gt;Using filters, you can extend Hangfire to implement things like logging each state transition or reporting unhandled errors to &lt;a href="https://www.bugsnag.com/"&gt;Bugsnag&lt;/a&gt; or &lt;a href="https://sentry.io/welcome/"&gt;Sentry&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using States + Filters to Schedule Jobs on a Specific Queue
&lt;/h2&gt;

&lt;p&gt;As you might imagine, there are a lot of possibilities afforded by these two simple primitives. In any case, let's use a &lt;strong&gt;custom job state&lt;/strong&gt; and &lt;strong&gt;custom queue filter&lt;/strong&gt; to enable us to schedule delayed jobs on the queue of our choice.&lt;/p&gt;

&lt;p&gt;We need to accomplish two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add a custom piece of metadata to the scheduled job indicating what queue we want to be enqueued onto when we transition to the &lt;code&gt;EnqueuedState&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Read our custom queue metadata when transitioning from the &lt;code&gt;ScheduledState&lt;/code&gt; to the &lt;code&gt;EnqueuedState&lt;/code&gt; and using it to enqueue the job to the correct queue.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;ScheduledQueueState&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Hangfire ships with an un-sealed state representing delayed, scheduled jobs. And since states in Hangfire are persisted as JSON using &lt;a href="https://www.newtonsoft.com/json"&gt;JSON.NET&lt;/a&gt;, we can simply extend this class and add a custom property to it. This property will be serialized and de-serialized using the default JSON serialization infrastructure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using System;
using Hangfire.States;
using Newtonsoft.Json;

public sealed class ScheduledQueueState : ScheduledState
{
        public ScheduledQueueState(TimeSpan enqueueIn)
                : this(DateTime.UtcNow.Add(enqueueIn), null)
        {
        }

        public ScheduledQueueState(DateTime enqueueAt)
                : this(enqueueAt, null)
        {
        }

        [JsonConstructor]
        public ScheduledQueueState(DateTime enqueueAt, string queue)
                : base(enqueueAt)
        {
                this.Queue = queue?.Trim();
        }

        public string Queue { get; }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This class is extremely straightforward: extend &lt;code&gt;ScheduledState&lt;/code&gt; and add a &lt;code&gt;Queue&lt;/code&gt; property while maintaining JSON serialization compatibility. Next, we need to take advantage of this new property when moving into the &lt;code&gt;EnqueuedState&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;QueueFilter&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Our &lt;code&gt;QueueFilter&lt;/code&gt; class will do two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Watch for jobs being created in either the &lt;code&gt;EnqueuedState&lt;/code&gt; or the &lt;code&gt;ScheduledQueueState&lt;/code&gt; and grab their &lt;code&gt;Queue&lt;/code&gt; property to store as a custom job parameter, and&lt;/li&gt;
&lt;li&gt;Use our custom &lt;code&gt;Queue&lt;/code&gt; job parameter when transitioning to (or &lt;em&gt;electing&lt;/em&gt;) the &lt;code&gt;EnqueuedState&lt;/code&gt;.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using System;
using Hangfire.Client;
using Hangfire.States;

public sealed class QueueFilter : IClientFilter, IElectStateFilter
{
        public const string QueueParameterName = "Queue";

        public void OnCreated(CreatedContext filterContext)
        {
        }

        public void OnCreating(CreatingContext filterContext)
        {
                string queue = null;

                switch (filterContext.InitialState)
                {
                        case EnqueuedState es:
                                queue = es.Queue;
                                break;
                        case ScheduledQueueState sqs:
                                queue = sqs.Queue;
                                break;
                        default:
                                break;
                }

                if (!string.IsNullOrWhiteSpace(queue))
                {
                        filterContext.SetJobParameter(QueueFilter.QueueParameterName, queue);
                }
        }

        public void OnStateElection(ElectStateContext context)
        {
                if (context.CandidateState.Name == EnqueuedState.StateName)
                {
                        string queue = context.GetJobParameter&amp;lt;string&amp;gt;(QueueFilter.QueueParameterName)?.Trim();

                        if (string.IsNullOrWhiteSpace(queue))
                        {
                            queue = EnqueuedState.DefaultQueue;
                        }

                        context.CandidateState = new EnqueuedState(queue);
                }
        }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, this class is pretty simple. We’re using C#’s &lt;a href="https://docs.microsoft.com/en-us/dotnet/csharp/fundamentals/functional/pattern-matching"&gt;pattern matching&lt;/a&gt; to check if the job is being created with either of the two states we care about. If it is, we are setting a custom parameter that will travel with the job through the pipeline.&lt;/p&gt;

&lt;p&gt;Then, when the system is transitioning the job to a new state, we check to see if the state being transitioned to is the &lt;code&gt;EnqueuedState&lt;/code&gt;. If it is, we look for our previously set custom parameter and use it to create a new version of the &lt;code&gt;EnqueuedState&lt;/code&gt; to transition into instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting the Pieces Together
&lt;/h2&gt;

&lt;p&gt;Now that we have all the code we need in place, how do we actually wire up the filter and take advantage of our new &lt;code&gt;ScheduledQueueState&lt;/code&gt;? First, we need to register our &lt;code&gt;QueueFilter&lt;/code&gt; into Hangfire’s processing pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;services.AddHangfire(configuration: (services, config) =&amp;gt;
{
        config.UseFilter(new QueueFilter());
});
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, we can create extension methods corresponding to the &lt;code&gt;Schedule&lt;/code&gt; &lt;a href="https://github.com/HangfireIO/Hangfire/blob/55fb6e60422976d052cd28bcc29e7c46eebc07d6/src/Hangfire.Core/BackgroundJobClientExtensions.cs#L120"&gt;overloads&lt;/a&gt; that we need. For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public static string Schedule(
        [NotNull] this IBackgroundJobClient client, 
        [NotNull, InstantHandle] Expression&amp;lt;Action&amp;gt; methodCall, 
        TimeSpan delay,
        string queue)
{
        if (client == null)
        {
            throw new ArgumentNullException(nameof(client));
        }

        return client.Create(methodCall, new ScheduledQueueState(delay, queue));
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above extension will let us schedule delayed jobs that don’t return a &lt;code&gt;Task&lt;/code&gt; and don’t take a parameter onto the &lt;code&gt;scheduled&lt;/code&gt; queue as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Acquire a reference to IBackgroundJobClient via dependency injection.
private IBackgroundJobClient client;    

// When you want to schedule your job.
this.client.Schedule(
        () =&amp;gt; Console.Log("Hello, world!"),
        TimeSpan.FromDays(1),
        "scheduled");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It is straightforward from here how to add any other overloads you may need.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;We have been happy with Hangfire’s combination of easy setup and rich extensibility while building Assetbots. It has become an important part of our internal infrastructure as we scale our background processing needs.&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
