<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Benedikt Deicke - Software Engineer</title>
  <id>https://benediktdeicke.com/</id>
  <link href="https://benediktdeicke.com/"/>
  <link href="http://feeds.feedburner.com/benediktdeicke" rel="self"/>
  <updated>2019-09-24T00:00:00+00:00</updated>
  <author>
    <name>Benedikt Deicke</name>
  </author>
  <entry>
    <title>Ruby Templating: Baking an Interpreter</title>
    <link rel="alternate" href="https://blog.appsignal.com/2019/09/24/ruby-magic-baking-an-interpreter.html"/>
    <id>https://blog.appsignal.com/2019/09/24/ruby-magic-baking-an-interpreter.html</id>
    <published>2019-09-24T00:00:00+00:00</published>
    <updated>2019-09-24T00:00:00+00:00</updated>
    <author>
      <name>Benedikt Deicke</name>
    </author>
    <summary type="html">&lt;p&gt;We hope you’ve got your stroopwafels warmed on top of your coffee because today we’re gluing things up with sticky stroop (the syrup that makes the two halves of a stroopwafel stick together). In the first two parts of our series, we baked a &lt;a href="https://blog.appsignal.com/2019/07/02/ruby-magic-brewing-our-own-template-lexer-in-ruby.html"&gt;Lexer&lt;/a&gt; and a &lt;a href="https://blog.appsignal.com/2019/07/30/ruby-magic-ruby-templating-the-parser.html"&gt;Parser&lt;/a&gt; and now, we’re adding the Interpreter and gluing things together by pouring stroop over them.&lt;/p&gt;
</summary>
    <content type="html">&lt;p&gt;We hope you’ve got your stroopwafels warmed on top of your coffee because today we’re gluing things up with sticky stroop (the syrup that makes the two halves of a stroopwafel stick together). In the first two parts of our series, we baked a &lt;a href="https://blog.appsignal.com/2019/07/02/ruby-magic-brewing-our-own-template-lexer-in-ruby.html"&gt;Lexer&lt;/a&gt; and a &lt;a href="https://blog.appsignal.com/2019/07/30/ruby-magic-ruby-templating-the-parser.html"&gt;Parser&lt;/a&gt; and now, we’re adding the Interpreter and gluing things together by pouring stroop over them.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Digging Deeper into Ruby Templating: The Parser</title>
    <link rel="alternate" href="https://blog.appsignal.com/2019/07/30/ruby-magic-ruby-templating-the-parser.html"/>
    <id>https://blog.appsignal.com/2019/07/30/ruby-magic-ruby-templating-the-parser.html</id>
    <published>2019-07-30T00:00:00+00:00</published>
    <updated>2019-07-30T00:00:00+00:00</updated>
    <author>
      <name>Benedikt Deicke</name>
    </author>
    <summary type="html">&lt;p&gt;Today, we continue our journey into Ruby Templating. With the lexer in place, let’s move on to the next step: The parser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.appsignal.com/2019/07/02/ruby-magic-brewing-our-own-template-lexer-in-ruby.html"&gt;Last time&lt;/a&gt;, we looked at string interpolation and subsequently, dived into creating our own templating language. We started by implementing a lexer that reads a template and converts it into a stream of tokens. Today, we’ll implement the accompanying parser. We will also dip...&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;Today, we continue our journey into Ruby Templating. With the lexer in place, let’s move on to the next step: The parser.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blog.appsignal.com/2019/07/02/ruby-magic-brewing-our-own-template-lexer-in-ruby.html"&gt;Last time&lt;/a&gt;, we looked at string interpolation and subsequently, dived into creating our own templating language. We started by implementing a lexer that reads a template and converts it into a stream of tokens. Today, we’ll implement the accompanying parser. We will also dip our toes into a bit of language theory.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Brewing our own Template Lexer in Ruby</title>
    <link rel="alternate" href="https://blog.appsignal.com/2019/07/02/ruby-magic-brewing-our-own-template-lexer-in-ruby.html"/>
    <id>https://blog.appsignal.com/2019/07/02/ruby-magic-brewing-our-own-template-lexer-in-ruby.html</id>
    <published>2019-07-02T00:00:00+00:00</published>
    <updated>2019-07-02T00:00:00+00:00</updated>
    <author>
      <name>Benedikt Deicke</name>
    </author>
    <summary type="html">&lt;p&gt;Put on your scuba diving suite and pack your stencils, we’re diving into Templates today!&lt;/p&gt;

&lt;p&gt;Most software that renders web pages or generates emails uses templating to embed variable data into text documents. The main structure of the document is often set up in a static template with placeholders for the data. The variable data, like user names or web page contents, replace the placeholders while...&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;Put on your scuba diving suite and pack your stencils, we’re diving into Templates today!&lt;/p&gt;

&lt;p&gt;Most software that renders web pages or generates emails uses templating to embed variable data into text documents. The main structure of the document is often set up in a static template with placeholders for the data. The variable data, like user names or web page contents, replace the placeholders while rendering the page.&lt;/p&gt;

&lt;p&gt;For our dive into templating, we’ll implement a subset of Mustache, a templating language that’s available in many programming languages. In this episode, we’ll investigate different ways of templating. We’ll start out looking at string concatenation, and end up writing our own lexer to allow for more complex templates.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>A Background Processing System in Ruby</title>
    <link rel="alternate" href="https://blog.appsignal.com/2019/04/02/background-processing-system-in-ruby.html"/>
    <id>https://blog.appsignal.com/2019/04/02/background-processing-system-in-ruby.html</id>
    <published>2019-04-02T00:00:00+00:00</published>
    <updated>2019-04-02T00:00:00+00:00</updated>
    <author>
      <name>Benedikt Deicke</name>
    </author>
    <summary type="html">&lt;p&gt;In today’s post, we are going to implement a naive background processing system for fun! We might learn some things along the way as a peek into the the internals of popular background processing systems like &lt;a href="http://sidekiq.org/"&gt;Sidekiq&lt;/a&gt;. The product of this fun is by no means intended for production use.&lt;/p&gt;

&lt;p&gt;Let’s imagine we have a task in our application that loads one or more websites and extracts their titles. As we...&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;In today&amp;rsquo;s post, we are going to implement a naive background processing system for fun! We might learn some things along the way as a peek into the the internals of popular background processing systems like &lt;a href="http://sidekiq.org/"&gt;Sidekiq&lt;/a&gt;. The product of this fun is by no means intended for production use.&lt;/p&gt;

&lt;p&gt;Let’s imagine we have a task in our application that loads one or more websites and extracts their titles. As we don’t have any influence on the performance of these websites, we’d like to perform the task outside our main thread (or the current request—if we’re building a web application), but in the background.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>The Magic of Class-level Instance Variables</title>
    <link rel="alternate" href="https://blog.appsignal.com/2018/10/02/ruby-magic-class-level-instance-variables.html"/>
    <id>https://blog.appsignal.com/2018/10/02/ruby-magic-class-level-instance-variables.html</id>
    <published>2018-10-02T00:00:00+00:00</published>
    <updated>2018-10-02T00:00:00+00:00</updated>
    <author>
      <name>Benedikt Deicke</name>
    </author>
    <summary type="html">&lt;p&gt;In a previous &lt;a href="https://blog.appsignal.com/2018/08/07/ruby-magic-changing-the-way-ruby-creates-objects.html"&gt;Ruby Magic&lt;/a&gt;, we figured out how to reliably inject modules into classes by overwriting its &lt;code&gt;.new&lt;/code&gt; method, allowing us to wrap methods with additional behavior.&lt;/p&gt;

&lt;p&gt;This time, we’re taking it one step further by extracting that behaviour into a module of its own so we can reuse it. We’ll build a &lt;code&gt;Wrappable&lt;/code&gt; module that handles the class extension for us, and we’ll learn all about class-level...&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;In a previous &lt;a href="https://blog.appsignal.com/2018/08/07/ruby-magic-changing-the-way-ruby-creates-objects.html"&gt;Ruby Magic&lt;/a&gt;, we figured out how to reliably inject modules into classes by overwriting its &lt;code&gt;.new&lt;/code&gt; method, allowing us to wrap methods with additional behavior.&lt;/p&gt;

&lt;p&gt;This time, we&amp;rsquo;re taking it one step further by extracting that behaviour into a module of its own so we can reuse it. We&amp;rsquo;ll build a &lt;code&gt;Wrappable&lt;/code&gt; module that handles the class extension for us, and we&amp;rsquo;ll learn all about class-level instance variables along the way. Let&amp;rsquo;s dive right in!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Changing the Way Ruby Creates Objects</title>
    <link rel="alternate" href="https://blog.appsignal.com/2018/08/07/ruby-magic-changing-the-way-ruby-creates-objects.html"/>
    <id>https://blog.appsignal.com/2018/08/07/ruby-magic-changing-the-way-ruby-creates-objects.html</id>
    <published>2018-08-07T00:00:00+00:00</published>
    <updated>2018-08-07T00:00:00+00:00</updated>
    <author>
      <name>Benedikt Deicke</name>
    </author>
    <summary type="html">&lt;p&gt;One of the things that makes Ruby great is that we can customize almost anything to our needs. This is both useful and dangerous. It&amp;rsquo;s easy to shoot ourselves in the foot, but when used carefully, this can result in pretty powerful solutions.&lt;/p&gt;
</summary>
    <content type="html">&lt;p&gt;One of the things that makes Ruby great is that we can customize almost anything to our needs. This is both useful and dangerous. It&amp;rsquo;s easy to shoot ourselves in the foot, but when used carefully, this can result in pretty powerful solutions.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>2017 in Review</title>
    <link rel="alternate" href="https://benediktdeicke.com/2018/01/year-in-review/"/>
    <id>https://benediktdeicke.com/2018/01/year-in-review/</id>
    <published>2018-01-06T00:00:00+00:00</published>
    <updated>2018-01-06T00:00:00+00:00</updated>
    <author>
      <name>Benedikt Deicke</name>
    </author>
    <summary type="html">&lt;p&gt;&lt;img src="/assets/2018/01/review-34f0f126.jpg" alt=""&gt;&lt;/p&gt;

&lt;p&gt;The start of a new year is a great time to reflect and think about the previous one. Two years ago, I started publishing my thoughts about my personal year. With 2018 just starting off, it’s time for me to look back at 2017.&lt;/p&gt;

&lt;p&gt;Feel free to go back to my reviews of &lt;a href="/2016/01/year-in-review"&gt;2015&lt;/a&gt; and &lt;a href="/2017/01/year-in-review"&gt;2016&lt;/a&gt; for additional context.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;&lt;img src="/assets/2018/01/review-34f0f126.jpg" alt="" /&gt;&lt;/p&gt;

&lt;p&gt;The start of a new year is a great time to reflect and think about the previous one. Two years ago, I started publishing my thoughts about my personal year. With 2018 just starting off, it’s time for me to look back at 2017.&lt;/p&gt;

&lt;p&gt;Feel free to go back to my reviews of &lt;a href="/2016/01/year-in-review"&gt;2015&lt;/a&gt; and &lt;a href="/2017/01/year-in-review"&gt;2016&lt;/a&gt; for additional context.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;The year in general&lt;/h2&gt;

&lt;p&gt;2017 was weird. I don’t really know how to feel about it. It had some highs and a good bunch of low moments, both in work and private life.&lt;/p&gt;

&lt;p&gt;Twelve months ago, I set out with four goals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Relaunch my website with productized consulting offerings&lt;/li&gt;
&lt;li&gt;Launch a new product or relaunch Stage with a twist&lt;/li&gt;
&lt;li&gt;Maintain overall revenue (from both consulting and products)&lt;/li&gt;
&lt;li&gt;Move into a new apartment.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the end, I managed to achieve only two of them: I relaunched my website and moved into a new apartment. While I announced new products, I didn’t launch anything and I made it to about 2/3 of my revenue goal.&lt;/p&gt;

&lt;h2&gt;The consulting business&lt;/h2&gt;

&lt;p&gt;I struggled a bit with consulting this year, even though it was off to a good start. My first consulting project of the year, &lt;a href="http://tinyreminder.com"&gt;Tiny Reminder&lt;/a&gt;, was one of the most fun projects in a long time.&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="http://uibreakfast.com"&gt;Jane Portman&lt;/a&gt;’s help, I was able to relaunch my website and started offering consultation calls and &lt;a href="/services"&gt;productized services&lt;/a&gt;. At about the same time I started an ongoing maintenance contract with a local startup. This provided some steady income throughout the year.&lt;/p&gt;

&lt;p&gt;Apart from that, I haven’t figured out a way to consistently and reliably get new work. This resulted in a drought this summer with almost no new work for weeks. This lead to taking on projects that didn’t really excite me, and while it was okay working on them, I didn’t enjoy it.&lt;/p&gt;

&lt;p&gt;It also made me give working through recruiting agencies a try. This quickly turned into a huge mess, reaffirming my previous rule of not working via recruiters.&lt;/p&gt;

&lt;p&gt;When I finally managed to get more work, it was late in the year. I overcommitted on too much stuff at once. As a result, the last quarter was incredibly stressful.&lt;/p&gt;

&lt;h2&gt;The product business&lt;/h2&gt;

&lt;p&gt;I started into 2017 knowing that there’s no sense in putting more work into Stage unless I find some way to make it drastically more interesting to the target audience. As I didn’t have a clear idea on how to do this, I was open to focusing on something new. I set a goal of launching a new product (or a major change to Stage) within the year.&lt;/p&gt;

&lt;p&gt;For the most part of the year, I spent a lot of time thinking about what to do next. As it took me several years to build and launch Stage, I was hesitant about starting something, not knowing if it will lead to something. I abandoned a lot of things, because I wasn’t sure if it’s worth the effort.&lt;/p&gt;

&lt;p&gt;The irony of this is, of course, that it resulted in me not working on anything. Instead, I wasted my time worrying about starting the wrong thing. It was in August, when I finally realized this and started moving forward on some ideas I had.&lt;/p&gt;

&lt;h3&gt;The SaaS Guidebook&lt;/h3&gt;

&lt;p&gt;One of the ideas I dabbled with was starting a new podcast related to the technical side of building and running SaaS applications. While that idea isn’t off the table, it sparked the idea of &lt;a href="/saas-guidebook"&gt;writing a book on that topic&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In the last couple of years, I gained experience in building SaaS applications for my consulting clients and myself. The lessons learned helped me a lot with my own projects and allowed me to help friends with their SaaS products.&lt;/p&gt;

&lt;p&gt;As of today, I’m in the process of writing the book, which is a lot harder than I anticipated. While I have a rough idea about the topics, it’s tough to find the time to write. I’m confident that I’ll be able to finish it eventually, though.&lt;/p&gt;

&lt;h3&gt;Userlist&lt;/h3&gt;

&lt;p&gt;In September, Jane Portman approached me with an idea for a new product. We teamed up with &lt;a href="http://loveyourcustomer.co"&gt;Claire Suellentrop&lt;/a&gt; to build and launch Userlist.&lt;/p&gt;

&lt;p&gt;Our goal for &lt;a href="http://userlist.com"&gt;Userlist&lt;/a&gt; is to build a tool that allows SaaS founders to see what their users are doing inside their application, and communicate with them based on their behavior.&lt;/p&gt;

&lt;p&gt;What excites me most about this, is the team: With Jane&amp;rsquo;s experience in design, Claire&amp;rsquo;s marketing expertise, and my experience with the technical parts, I feel like we’re in a good position to build something great.&lt;/p&gt;

&lt;p&gt;As of today, we’re early in the process and figuring out the details. As you&amp;rsquo;re reading this I just started writing the first lines of code. If you want to follow along our journey, &lt;a href="http://userlist.com"&gt;sign up for the mailing list&lt;/a&gt;. I’ll also be posting updates about the development process on &lt;a href="http://twitter.com/benediktdeicke"&gt;Twitter&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;Stage CMS&lt;/h3&gt;

&lt;p&gt;Even though I didn’t focus on Stage this year, there has been progress. Two new websites got build on the platform, which made me cross €500 in monthly recurring revenue.&lt;/p&gt;

&lt;p&gt;I hired &lt;a href="http://docontent.de"&gt;Cristina Mlynek&lt;/a&gt; to do some SEO, improve the content of the website, as well as manage Stage’s social media presence. It’s nice to see some improvements, without having to do the work myself.&lt;/p&gt;

&lt;h2&gt;The Conference&lt;/h2&gt;

&lt;p&gt;One of the highlights this year was the first edition of &lt;a href="http://femtoconf.com/2017"&gt;FemtoConf&lt;/a&gt;, the conference I’m organizing together with &lt;a href="http://christophengelhardt.com"&gt;Christoph Engelhardt&lt;/a&gt;. It was intended to be a meet-up for listeners of our podcast and friends from the micropreneur community. Somehow it turned into a full-blown conference.&lt;/p&gt;

&lt;p&gt;The conference was a great success. One of our goals was to give it a similar vibe as MicroConf Europe (which was a huge inspiration). We wanted it to have a focus on meeting people and building friendships, and we were able to achieve it.&lt;/p&gt;

&lt;p&gt;As feedback from attendees was very positive, we’re &lt;a href="http://femtoconf.com/2018"&gt;organizing a second edition&lt;/a&gt; in March.&lt;/p&gt;

&lt;h2&gt;Private Life&lt;/h2&gt;

&lt;p&gt;As mentioned above, my partner and I moved into a new home. After a long hunt for a new apartment, we fled the tiny one bedroom apartment I moved into after finishing university. Now, we live a bit outside of Darmstadt, Germany in a quiet neighborhood and don’t have to worry about where to store our stuff anymore. The process of moving was stressful. The amount of things to think about and take care of was overwhelming at times.&lt;/p&gt;

&lt;p&gt;On a sad note, I lost my last two grandparents this year. Seeing an entire generation of my family disappear within a couple of months had a huge impact on me.&lt;/p&gt;

&lt;p&gt;In addition to the stressful work life in the last quarter of the year, my partner got sick. She even got hospitalized for a couple of days. While it’s nothing life threatening, it still dragged both of us down. She’s feeling a bit better now, but it’s still not clear what causes her health issues. Fingers crossed the doctors will figure it out soon.&lt;/p&gt;

&lt;h2&gt;Lessons learned in 2017&lt;/h2&gt;

&lt;p&gt;Looking back at the year, I learned a few important things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Momentum is important.&lt;/strong&gt; I got stuck overthinking every possible way to move forward. I was so afraid to waste my time on the wrong things that I instead wasted my time doing nothing. The moment I realized that dilemma, it was easier to start working on new things and putting them out there to gather feedback.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It’s okay to not work on some things.&lt;/strong&gt; In October, I counted ongoing commitments (client projects, personal work related projects) and got the crazy number of 14 as a result. This made it clear that I had to explicitly put some things on the back burner and &lt;em&gt;not&lt;/em&gt; try to get everything done at once.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Community and relationships are important.&lt;/strong&gt; My partner, my family, my friends, and my mastermind groups helped me a lot to get through the tougher parts of my year. Thanks to all of you for your encouragement and support.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;Goals for 2018&lt;/h2&gt;

&lt;p&gt;As with previous years, I set some goals I want to achieve. Unlike previous years, I’ll try a new approach. Instead of pursuing a set of goals for the entire year, I’ll break it them into quarterly goals. This will hopefully make it easer to come up with a plan to achieve them.&lt;/p&gt;

&lt;p&gt;Nonetheless, for the sake of simplicity, these are the goals I want to achieve in 2018:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Launching Userlist&lt;/li&gt;
&lt;li&gt;Launching The SaaS Guidebook&lt;/li&gt;
&lt;li&gt;Get my overall revenue back to the level of 2016&lt;/li&gt;
&lt;li&gt;Improve my personal brand&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check back in a year to learn if I managed to achieve some of them. As always, I hope this was interesting and that you got something out of it for yourself. Thanks a lot for reading and a &lt;strong&gt;Happy New Year 2018&lt;/strong&gt;!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Don't call us, we'll call you: Sending webhooks with Rails</title>
    <link rel="alternate" href="https://benediktdeicke.com/2017/09/sending-webhooks-with-rails/"/>
    <id>https://benediktdeicke.com/2017/09/sending-webhooks-with-rails/</id>
    <published>2017-09-20T00:00:00+00:00</published>
    <updated>2017-09-20T00:00:00+00:00</updated>
    <author>
      <name>Benedikt Deicke</name>
    </author>
    <summary type="html">&lt;p&gt;&lt;img src="/assets/2017/09/webhooks-51ca55cd.jpg" alt=""&gt;&lt;/p&gt;

&lt;p&gt;Integrating with other services is on the roadmap of almost any SaaS application. There’s only so much your application can do itself. Sometimes it’s best to leave tasks up to others who specialize on them.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;&lt;img src="/assets/2017/09/webhooks-51ca55cd.jpg" alt="" /&gt;&lt;/p&gt;

&lt;p&gt;Integrating with other services is on the roadmap of almost any SaaS application. There’s only so much your application can do itself. Sometimes it’s best to leave tasks up to others who specialize on them.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Building specialized integrations with other tools is one way to approach this. However, wouldn’t it be nice if your application had a generic way for others to be notified about things that happen on your side so they can work with that information?&lt;/p&gt;

&lt;p&gt;When it comes to passing data around in (near) realtime, &lt;a href="http://www.webhooks.org"&gt;webhooks&lt;/a&gt; are the way to go. There is no real standard for them, but most services are working with simple POST requests that send JSON as their request body. Webhooks are triggered for different events and transmit relevant data to other parties.&lt;/p&gt;

&lt;p&gt;In the following, we’ll be talking about how to allow other services to integrate with your application, by allowing them to register webhook endpoints and notifying them when something interesting happens.&lt;/p&gt;

&lt;h2&gt;Registering webhook endpoints&lt;/h2&gt;

&lt;p&gt;In order to send webhooks, you have to know where to send them to. You have to provide your users with a way to set up new webhook endpoints. This can be done via user interface. In it’s simplest form, it would just be a textfield where users can enter a URL. In more advanced scenarios, you might allow them to specify multiple different endpoints and the types of events that should send a webhook to these endpoints.&lt;/p&gt;

&lt;p&gt;To make things easier for your users, you can also decide to allow webhook endpoint registration via your API. This way, other services can just setup the endpoints they need, making things a lot easier for your users. Add authentication via OAuth on top, and it can be as easy as a single click of a button.&lt;/p&gt;

&lt;p&gt;We start off by implementing a &lt;code&gt;Webhook::Endpoint&lt;/code&gt; model. Each endpoint needs a target url, a set of events that it cares about, and an account this endpoint belongs to. To make things a little easier for ourselves, we use PostgreSQL’s array columns to store the events as a simple list.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CreateWebhookEndpoints&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ActiveRecord&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Migration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;5.1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change&lt;/span&gt;
    &lt;span class="n"&gt;create_table&lt;/span&gt; &lt;span class="ss"&gt;:webhook_endpoints&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:target_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;string&lt;/span&gt; &lt;span class="ss"&gt;:events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;array: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;references&lt;/span&gt; &lt;span class="ss"&gt;:account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;foreign_key: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;null: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timestamps&lt;/span&gt;
      &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;index&lt;/span&gt; &lt;span class="ss"&gt;:events&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This example uses namespaces to keep things a bit more organized. We put all webhooks related code into a &lt;code&gt;webhook&lt;/code&gt; directory. In the database we prefix all webhook related tables with &lt;code&gt;webhook_&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/webhook/endpoint.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Webhook&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Endpoint&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;table_name_prefix&lt;/span&gt;
      &lt;span class="s1"&gt;'webhook_'&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To make the &lt;code&gt;events&lt;/code&gt; array column work, we have to tell Rails about it by adding an &lt;code&gt;attribute&lt;/code&gt; macro to the model.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;attribute&lt;/span&gt; &lt;span class="ss"&gt;:events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;array: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;default: &lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;On the validation side, we only want valid http urls as target url. Luckily Ruby comes with a handy URI regular expression in it’s standard library, ready for us to use. We also add a presence validation for the events, so every endpoint is triggered by at least one event.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:target_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;presence: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;format: &lt;/span&gt;&lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;regexp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sx"&gt;%w(http https)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;validates&lt;/span&gt; &lt;span class="ss"&gt;:events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;presence: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In order to quickly retrieve endpoints for one (or more) events later on, we implement a custom scope. It uses PostgreSQL’s contains operator (&lt;code&gt;@&amp;gt;&lt;/code&gt;) to just return the endpoints interested in a particular event.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nc"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;for_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'events @&amp;gt; ARRAY[?]::varchar[]'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Notice that we’re using &lt;code&gt;Array(events)&lt;/code&gt; to convert the methods arguments into an array, or just leave it untouched if it already is one. This allows us to call the scope with one event name, or an array of multiple ones.&lt;/p&gt;

&lt;p&gt;Next, we add some normalization and sanitization logic to the events setter method. This can be done, by simply overwriting the method, doing the necessary changes, and then calling super.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;events&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;underscore&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;Webhook&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Event&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;EVENT_TYPES&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In this case we’re normalizing the events into underscore strings. Afterwards we intersect the resulting array with a predefined array of event names (more on that later), to make sure all the events name are actually valid.&lt;/p&gt;

&lt;p&gt;Finally, we add a method to deliver an event to this endpoint. It’s empty on purpose for now.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deliver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With the &lt;code&gt;Webhook::Endpoint&lt;/code&gt; model in place, we need a controller, the view layer and most likely an API representation for it. There’s nothing special about the implementation of all of this, so we won’t cover it here.&lt;/p&gt;

&lt;h2&gt;Representing events&lt;/h2&gt;

&lt;p&gt;As mentioned before there is no standard on how webhook payloads should look like. In this example we use them to represent events that are triggered within your application. Assuming the application somehow works with a &lt;code&gt;Project&lt;/code&gt; model, here’s an example of an event for a newly created project.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"event_name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"project_created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"project"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Example Project"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The webhook payload follows a simple structure. It always has an &lt;code&gt;event_name&lt;/code&gt; and a set of embedded models with relevant data. You’re of course free to settle on a different format. Instead of embedding the event name in the payload itself, you might choose to send an &lt;code&gt;X-Event-Name&lt;/code&gt; header with your request. Whatever you do, just stick to a pattern for all the webhooks you’re sending and document it. All the developers building integrations with your application will thank you for it.&lt;/p&gt;

&lt;p&gt;Let’s implement a &lt;code&gt;Webhook::Event&lt;/code&gt; model to represent a single event. This can be done as “plain old ruby object”. If you want to keep track of the state of every single event, you can store all events in the database as well. For the purpose of this example, we’ll keep it simple.&lt;/p&gt;

&lt;p&gt;The implementation of &lt;code&gt;Webhook::Event&lt;/code&gt; is straight-forward. It’s an object with two instance variables. One for the event’s name, and another one for its (optional) payload. To keep track of valid events, we add the &lt;code&gt;EVENT_TYPES&lt;/code&gt; constant used earlier. Whenever you add a new event, add it to this list.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/webhook/event.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Webhook&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;
    &lt;span class="no"&gt;EVENT_TYPES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sx"&gt;%w(
      project_created
      project_updated
      project_destroyed
    )&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;freeze&lt;/span&gt;

    &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:event_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:payload&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
      &lt;span class="vi"&gt;@event_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event_name&lt;/span&gt;
      &lt;span class="vi"&gt;@payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;As we want events to be delivered as JSON in the HTTP request’s body, we have to implement serialization for this event model. This can be as simple as taking a copy of the payload and adding the &lt;code&gt;event_name&lt;/code&gt; to it.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;as_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dup&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
  &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:event_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event_name&lt;/span&gt;
  &lt;span class="nb"&gt;hash&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;However, we want to keep our code clean and not come up with the entire payload for every single event. If you’re using &lt;a href="http://github.com/rails/active_model_serializers"&gt;&lt;code&gt;ActiveModel::Serializers&lt;/code&gt;&lt;/a&gt; to handle serialization of your API resources, you can leverage it to properly serialize the payload for you.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;as_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="nb"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;transform_values&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;serialize_resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;as_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:event_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event_name&lt;/span&gt;
  &lt;span class="nb"&gt;hash&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="kp"&gt;private&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;serialize_resource&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;ActiveModelSerializers&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SerializableResource&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This way, you can create events and simply pass your model instances as payload.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Webhook&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:project_created&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;project: &lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;Triggering events&lt;/h2&gt;

&lt;p&gt;With both &lt;code&gt;Webhook::Endpoint&lt;/code&gt; and &lt;code&gt;Webhook::Event&lt;/code&gt; in place, it’s time to actually trigger events. To keep things organized and DRY, we use a module to implement this. Whenever you want to trigger events, you just include that module in your model and call one of the helper methods.&lt;/p&gt;

&lt;p&gt;We start off by adding a &lt;code&gt;webhook_scope&lt;/code&gt; method. It’s just a stub, that has to be overwritten by each model to return the scope of the webhooks. If you’re getting a bit confused by that, don’t worry. It’ll get clear once you see the module used in an actual example in a few moments.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/webhook/delivery.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Webhook&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Delivery&lt;/span&gt;
    &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Concern&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;webhook_scope&lt;/span&gt;
      &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;NotImplementedError&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Next, we add a method to trigger events. It just takes two arguments. One for the event name, and one for its payload. It then constructs an &lt;code&gt;Webhook::Event&lt;/code&gt; object from those and asks every endpoint interested in the event to deliver it.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deliver_webhook_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Webhook&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
  &lt;span class="n"&gt;webhook_scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;webhook_endpoints&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;for_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_name&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deliver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To make things even simpler, we implement a default &lt;code&gt;webhook_payload&lt;/code&gt; method, as well as a &lt;code&gt;deliver_webhook&lt;/code&gt; method. The latter relies on a naming convention to generate an event for the current model based on an action name.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;webhook_payload&lt;/span&gt;
 &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deliver_webhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;event_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;class&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;underscore&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;_&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="n"&gt;deliver_webhook_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;webhook_payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Coming back to our projects example from earlier, we can now trigger events like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/project.rb&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Project&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Webhook&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Delivery&lt;/span&gt;

  &lt;span class="n"&gt;after_commit&lt;/span&gt; &lt;span class="ss"&gt;on: :create&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;deliver_webhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:created&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:account&lt;/span&gt;

&lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;webhook_scope&lt;/span&gt;
    &lt;span class="n"&gt;account&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;webhook_payload&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;project: &lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;As most events revolve around creating, updating, and removing models in your database, we can also build a simple module to handle those events automatically.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/models/webhook/observable.rb&lt;/span&gt;
&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Webhook&lt;/span&gt;
  &lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Observable&lt;/span&gt;
    &lt;span class="kp"&gt;extend&lt;/span&gt; &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Concern&lt;/span&gt;
    &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Webhook&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Delivery&lt;/span&gt;

    &lt;span class="n"&gt;included&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;after_commit&lt;/span&gt; &lt;span class="ss"&gt;on: :create&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;deliver_webhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:created&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="n"&gt;after_commit&lt;/span&gt; &lt;span class="ss"&gt;on: :update&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;deliver_webhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:updated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="n"&gt;after_commit&lt;/span&gt; &lt;span class="ss"&gt;on: :destroy&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;deliver_webhook&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:destroyed&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Please notice that the module is using &lt;code&gt;after_commit&lt;/code&gt; callbacks. It’s important to only send webhooks after the data is actually present in your database. This is to prevent weird “not found” errors in the occasions where webhooks are delivered faster than your database takes to commit the transactions or it’s part of a larger transaction that fails shortly afterwards.&lt;/p&gt;

&lt;p&gt;Using the &lt;code&gt;Webhook::Observable&lt;/code&gt; module, adding webhook events to a model becomes as simple as this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Project&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;ApplicationRecord&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Webhook&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Observable&lt;/span&gt;

  &lt;span class="n"&gt;belongs_to&lt;/span&gt; &lt;span class="ss"&gt;:account&lt;/span&gt;

&lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;webhook_scope&lt;/span&gt;
    &lt;span class="n"&gt;account&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;webhook_payload&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;project: &lt;/span&gt;&lt;span class="nb"&gt;self&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;Delivering webhooks&lt;/h2&gt;

&lt;p&gt;To deliver webhooks, I strongly recommend using a background job queue. As delivery depends on 3rd party services, you should expect them to be slow or even unreachable. As your application might send webhooks to multiple endpoints per event, you definitely don’t want your users having to wait until all of them are delivered. Using a queue also allows for retries when delivery fails for some reason.&lt;/p&gt;

&lt;p&gt;In this example, we’re using Sidekiq as background job queue. This is not a requirement, though. ActiveJob with any backend you like will to this job as well.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# app/workers/webhook/delivery_worker.rb&lt;/span&gt;
&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'net/http'&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;Webhook&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;DeliveryWorker&lt;/span&gt;
    &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;Sidekiq&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Worker&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;perform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;endpoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Webhook&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Endpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;target_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;code&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="mi"&gt;410&lt;/span&gt;
        &lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;destroy&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;599&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_s&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The implementation will raise an exception for all HTTP status codes that indicate some sort of error. Sidekiq will catch the exception and just retry the delivery a few moments later. Please know that HTTP status code &lt;code&gt;410 Gone&lt;/code&gt; is an exception. It’s a small idea picked up from &lt;a href="http://zapier.com"&gt;Zapier&lt;/a&gt;. Whenever the endpoint returns a 410, we consider it no longer valid and just remove it from the database.&lt;/p&gt;

&lt;p&gt;The actual HTTP request is nothing fancy. As a result, we just use &lt;code&gt;Net::HTTP&lt;/code&gt; from Ruby’s standard library.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;uri&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;URI&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;endpoint&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Post&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request_uri&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'Content-Type'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'application/json'&lt;/span&gt;
  &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;

  &lt;span class="n"&gt;http&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Net&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;HTTP&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;host&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;port&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use_ssl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;scheme&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'https'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

  &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Finally, we add the implementation of &lt;code&gt;Webhook::Endpoint#deliver&lt;/code&gt; we skipped earlier. It enqueues a new &lt;code&gt;Webhook::DeliveryWorker&lt;/code&gt; for the given event.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deliver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Webhook&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DeliveryWorker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It’s important to serialize the event model in the very moment the delivery is  triggered. Otherwise the payload might have changed until the delivery takes place, resulting in weird data and confused developers.&lt;/p&gt;

&lt;h2&gt;Taking things one step further&lt;/h2&gt;

&lt;p&gt;While the implementation shown here covers the most important parts to get webhooks implemented in your application, there are a few more things you should consider.&lt;/p&gt;

&lt;h3&gt;Allowing for authenticity checks&lt;/h3&gt;

&lt;p&gt;As webhooks are usually delivered to more or less public endpoints it’s best to add some way that allows the receiving application to ensure the data is actually coming from you. One common way to do this is to add a signature somewhere in the payload. This can either be done in the JSON body or in a custom HTTP header. The signature should be based on the payload itself and a secret only known to the sending and the receiving application.&lt;/p&gt;

&lt;h3&gt;Accidental Denial of Service attacks&lt;/h3&gt;

&lt;p&gt;Based on the details of your application, it might happen that you send &lt;em&gt;a lot&lt;/em&gt; of requests to the endpoints. Some of them might not be able to handle the load. With retries in place, the number of requests sent to the endpoint will only increase, making things worse. It’s a good idea to have some sort of rate limiting in place or even temporarily disable an endpoint that is unable to process requests right now.&lt;/p&gt;

&lt;h2&gt;Wrapping up&lt;/h2&gt;

&lt;p&gt;Webhooks are a great way to allow applications to integrate with yours. At their core, they’re simple and easy to understand HTTP requests. With just a little bit of code, they allow your users to use your application in ways that you didn’t think of before.&lt;/p&gt;

&lt;p&gt;You can get the source code of everything explained in this article at &lt;a href="https://gist.github.com/benedikt/fa3488d968385e553884f61676a69f13"&gt;GitHub’s Gist&lt;/a&gt;. You should be able to use it in your existing applications, with just minor modifications.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>Just skip it: 9 features your product does not need to launch</title>
    <link rel="alternate" href="https://benediktdeicke.com/2017/04/features-your-product-does-not-need-to-launch/"/>
    <id>https://benediktdeicke.com/2017/04/features-your-product-does-not-need-to-launch/</id>
    <published>2017-04-04T00:00:00+00:00</published>
    <updated>2017-04-04T00:00:00+00:00</updated>
    <author>
      <name>Benedikt Deicke</name>
    </author>
    <summary type="html">&lt;p&gt;&lt;img src="/assets/2017/04/features-4edd6c69.jpg" alt=""&gt;&lt;/p&gt;

&lt;p&gt;Building a SaaS (or any software product, really) takes a lot of time and money. It’s hard to stay focused on the things that actually produce value for your customers. In the following, I’ll show you a few things that you can easily skip until after launching. This way, you’ll launch earlier, and maybe even learn that you don’t need some of the features you thought everyone wants.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;&lt;img src="/assets/2017/04/features-4edd6c69.jpg" alt="" /&gt;&lt;/p&gt;

&lt;p&gt;Building a SaaS (or any software product, really) takes a lot of time and money. It&amp;rsquo;s hard to stay focused on the things that actually produce value for your customers. In the following, I&amp;rsquo;ll show you a few things that you can easily skip until after launching. This way, you&amp;rsquo;ll launch earlier, and maybe even learn that you don&amp;rsquo;t need some of the features you thought everyone wants.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;h2&gt;1. Self-service on-boarding&lt;/h2&gt;

&lt;p&gt;The dream of SaaS is that it&amp;rsquo;s full self-service at some point. People sign up, give you money, and you never have to talk to them. To achieve this dream, you spend a lot of time building and tweaking your self-service customer on-boarding flow. You create wizards, guided tours and streamline everything.&lt;/p&gt;

&lt;p&gt;However, when you have not launched yet, I urge you to skip it. Instead, manually set up your customer&amp;rsquo;s accounts for them. Afterwards, invite them to a screen sharing call and walk them through your application. &lt;/p&gt;

&lt;p&gt;Or, for a better effect, let them try to figure it out themselves, while you&amp;rsquo;re watching. When they get stuck or confused, help them by explaining things. &lt;strong&gt;This way, you learn about the actual problems of your application and any potential mismatch between your assumptions and your customer&amp;rsquo;s expectations.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Take notes about the things that don&amp;rsquo;t explain themselves and improve them. In the end, you&amp;rsquo;ll get a much better understanding about what you have to (or don&amp;rsquo;t have to) explain in a walkthrough.&lt;/p&gt;

&lt;h2&gt;2. Activity streams&lt;/h2&gt;

&lt;p&gt;Activity streams are a good way to show your customers what happened while they were away. While they&amp;rsquo;re a good way to show and document the value generated by your tool, &lt;strong&gt;they rarely are the feature that produces the value for your customer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;On top of that, there has to be a lot of activity for them to become meaningful. So, when people just signed up, they are not going to see anything useful anyways.&lt;/p&gt;

&lt;p&gt;Instead of implementing activity streams before even launching, remove that feature until your customers request some insights into their account activity.&lt;/p&gt;

&lt;h2&gt;3. Payment Integration&lt;/h2&gt;

&lt;p&gt;Don&amp;rsquo;t implement a payment integration before you launch. I know, this sounds bit unintuitive, because you&amp;rsquo;re building this product to make some money. &lt;/p&gt;

&lt;p&gt;However, not implementing a payment integration, does not mean you cannot get paid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why don&amp;rsquo;t you start by invoicing your customers manually, asking them to wire transfer the money?&lt;/strong&gt; You can also use services like PayPal or Gumroad to sell pre-paid subscriptions packages for a couple of months. Those tools are set up within minutes and don&amp;rsquo;t require you to integrate anything into your application.&lt;/p&gt;

&lt;p&gt;Even if you&amp;rsquo;re expecting a huge amount of new customers at launch, you don&amp;rsquo;t need automated billing right away. You have until the end of your trial period to get payments implemented.&lt;/p&gt;

&lt;h2&gt;4. Permissions / User Roles&lt;/h2&gt;

&lt;p&gt;A fine grained roles and permissions systems is sometimes praised as a great feature of your application. &lt;/p&gt;

&lt;p&gt;In my opinion, it makes things more complicated for both you and your users. Your application only needs one or two different roles. Chances are good that it doesn&amp;rsquo;t need them at all.&lt;/p&gt;

&lt;p&gt;For your launch, only allow one user per customer. People can easily share their login credentials should multiple people need to work with your application. Don&amp;rsquo;t worry too much about someone in your customer&amp;rsquo;s team screwing up the settings or accidentally deleting something. &lt;strong&gt;A complex permissions system is not the fix for trust and competence issues.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;5. Administration Interface&lt;/h2&gt;

&lt;p&gt;It&amp;rsquo;s tempting to implement a secret administration interface that gives you convenient access to your customer&amp;rsquo;s data. In my experience, you don&amp;rsquo;t really need it to get started, though.&lt;/p&gt;

&lt;p&gt;For one, you don&amp;rsquo;t have a lot of data and customers to look at in the first place. For another, you can just peek into the database, or ask your developer to run a quick query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Over time, you&amp;rsquo;ll recognize what bits of information you actually need, and which tasks you have to execute often.&lt;/strong&gt; As soon as it takes your developer a substantial amount of time to do these things for you, it makes sense to implement a dedicated administration interface for those things.&lt;/p&gt;

&lt;h2&gt;6. Advanced Technology / Architecture / Infrastructure&lt;/h2&gt;

&lt;p&gt;When starting out, you want to use the latest and greatest piece of technology around. It&amp;rsquo;s nice and shiny, and all the cool kids are using it right now.&lt;/p&gt;

&lt;p&gt;However, you don&amp;rsquo;t need it to solve your customer&amp;rsquo;s actual problems. A fancy, one-page, real-time, super fast JavaScript application is pretty cool, but it&amp;rsquo;ll take a lot longer to develop. A simple form with a bit of code in the back-end will do the job just as good.&lt;/p&gt;

&lt;p&gt;Just because some piece of technology is the best thing for a large, successful startup with lots of customers and engineers, doesn&amp;rsquo;t mean it&amp;rsquo;s the right thing for your application. Start with something simple, and build up from there. Build more advanced stuff when you actually need it. &lt;strong&gt;You cannot anticipate the problems you&amp;rsquo;ll run into eventually, so don&amp;rsquo;t try to anticipate them by using an advanced and complex setup.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When it comes to infrastructure (servers and stuff), keep things simple as well. Don&amp;rsquo;t expect huge growth in the early days that would warrant a super scalable and high performance infrastructure setup. It&amp;rsquo;s not worth the time and money unless you are absolutely certain you&amp;rsquo;ll need it.&lt;/p&gt;

&lt;h2&gt;7. Settings and customizations&lt;/h2&gt;

&lt;p&gt;For the initial version of your product, implement the bare minimum of features you need to solve the customer&amp;rsquo;s problem. Try to optimize for just one simple use case, and don&amp;rsquo;t any customizing.&lt;/p&gt;

&lt;p&gt;For one, this will make the application easier to use, for another it&amp;rsquo;ll save you a lot of time to build it. &lt;strong&gt;Pick some reasonable defaults and stick with them until enough people request more options and settings to tweak.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This includes things like: The number of different types of things (Field types, modes, &amp;hellip;), custom themes and branding, available languages, sorting, filters, pagination, different views on the same data, and a whole lot more.&lt;/p&gt;

&lt;p&gt;A good example for this is is &lt;a href="http://tinyreminder.com"&gt;Tiny Reminder&lt;/a&gt;. In the first sketches of the product, there were several types of form fields that people could use collect data from their clients. In the process of cutting down unnecessary features, &lt;a href="http://twitter.com/uibreakfast"&gt;Jane&lt;/a&gt; removed all but four simple ones (Short Text, Long Text, Tasks, and Files). Up to now, nobody missed the ones she decided to drop. &lt;/p&gt;

&lt;h2&gt;8. Metrics&lt;/h2&gt;

&lt;p&gt;Collecting data is important to understand how your customers use your application. However, the metrics are meaningless until you collected a lot of data points.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don&amp;rsquo;t attempt to measure and track every single action your customers might take in your application. Skip that part until after your launch.&lt;/strong&gt; You can get a pretty good first idea about your customer&amp;rsquo;s usage patterns just by looking at the data in your database.&lt;/p&gt;

&lt;p&gt;Once this data doesn&amp;rsquo;t answer all your questions, slowly start collecting data points for different things. As always, keep it simple and to a bare minimum. Having an overwhelming amount of data only makes it harder to draw any conclusions from it.&lt;/p&gt;

&lt;h2&gt;9. Customer support tool / knowledge base&lt;/h2&gt;

&lt;p&gt;Customer support is one of the core features your application should offer. It&amp;rsquo;s important to keep your customers happy. They will run into problems and there should be an easy way for them to get help.&lt;/p&gt;

&lt;p&gt;However, &lt;strong&gt;a simple support email address is more than enough to get started.&lt;/strong&gt; You don&amp;rsquo;t need a dedicated customer support tool. Once you get too many support requests to be able to handle them via email, you can integrate a proper tool.&lt;/p&gt;

&lt;p&gt;This also is true for the knowledge base. It&amp;rsquo;s a super useful tool to reduce the number of support requests you get. Don&amp;rsquo;t bother building it before your launch. Instead, learn from the common issues your customers have and write the knowledge base content based on that.&lt;/p&gt;

&lt;h2&gt;Bonus: Don&amp;rsquo;t build software at all&lt;/h2&gt;

&lt;p&gt;Did you ever consider that you might not even need to build a software product? Chances are, your customer&amp;rsquo;s problems can be fixed by offering a service. &lt;strong&gt;Start by doing things manually and learn about your customer&amp;rsquo;s actual problems and requirements.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This gets you into learning mode quickly, and you&amp;rsquo;ll save a lot of time and money that would otherwise go into building a software product that doesn&amp;rsquo;t fit your customers&amp;rsquo; needs.&lt;/p&gt;

&lt;p&gt;Brian Casel&amp;rsquo;s &lt;a href="http://audienceops.com"&gt;Audience Ops&lt;/a&gt; is a great example of this. Instead of starting with software, he started offering a service. Only now that he knows what tools would actually be useful, he&amp;rsquo;s building a &lt;a href="http://opscalendar.com"&gt;software product&lt;/a&gt;. &lt;/p&gt;

&lt;h2&gt;Your mileage may vary&amp;hellip;&lt;/h2&gt;

&lt;p&gt;As always, your mileage may vary. Depending on your actual product you cannot go without some of these features. Be careful about the features you add to your application early on. While most of the things listed above are useful, it&amp;rsquo;s probably too early to spend time on them.&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>2016 in Review</title>
    <link rel="alternate" href="https://benediktdeicke.com/2017/01/year-in-review/"/>
    <id>https://benediktdeicke.com/2017/01/year-in-review/</id>
    <published>2017-01-04T00:00:00+00:00</published>
    <updated>2017-01-04T00:00:00+00:00</updated>
    <author>
      <name>Benedikt Deicke</name>
    </author>
    <summary type="html">&lt;p&gt;A year ago, I published my first &lt;a href="/2016/01/year-in-review/"&gt;Year in Review&lt;/a&gt; post. I did not  publish anything since that review and therefore was a little hesitant about writing one for 2016. It was a tweet by Jamie Lawrence that convinced me to write this review:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Year in review” posts are great way of rewriting history, and a terrible way to compare yourself with others – &lt;a href="https://twitter.com/ideasasylum/status/814447889351143424"&gt;Jamie Lawrence&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;However, my intention isn’t to rewrite history. Instead, I want to preserve the things (both good and bad) that happened this year.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;A year ago, I published my first &lt;a href="/2016/01/year-in-review/"&gt;Year in Review&lt;/a&gt; post. I did not  publish anything since that review and therefore was a little hesitant about writing one for 2016. It was a tweet by Jamie Lawrence that convinced me to write this review:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Year in review” posts are great way of rewriting history, and a terrible way to compare yourself with others &amp;ndash; &lt;a href="https://twitter.com/ideasasylum/status/814447889351143424"&gt;Jamie Lawrence&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;However, my intention isn&amp;rsquo;t to rewrite history. Instead, I want to preserve the things (both good and bad) that happened this year.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;To give you some perspective, please allow me to introduce myself. I&amp;rsquo;m Benedikt Deicke, a software engineer and consultant who launched &lt;a href="http://stagecms.com"&gt;Stage&lt;/a&gt;, a content management system for bands, as a side project in 2015.&lt;/p&gt;

&lt;h2&gt;The year in general&lt;/h2&gt;

&lt;p&gt;Looking back at 2016, I&amp;rsquo;m a little disappointed, even though I shouldn&amp;rsquo;t be. My goals for the year included &lt;em&gt;increasing my revenue&lt;/em&gt;, &lt;em&gt;growing Stage to 1,000€ MRR&lt;/em&gt;, and &lt;em&gt;launching a productized consulting offering&lt;/em&gt;. I only count the revenue goal as a success, but more on that below.&lt;/p&gt;

&lt;h2&gt;The consulting business&lt;/h2&gt;

&lt;p&gt;In contrast to 2015, the consulting business was a smooth ride. I was able to hit my revenue goal for the year by consulting alone, and even a few months early. I had the pleasure of working on interesting projects, together with people I like and admire.&lt;/p&gt;

&lt;p&gt;The lessons learned in previous years definitely helped to make this possible.&lt;/p&gt;

&lt;p&gt;I also sold some coaching and mentoring retainer packages. While this was part of my goals for the year, I only call it half a success, because I never announced them publicly.&lt;/p&gt;

&lt;h2&gt;The SaaS business&lt;/h2&gt;

&lt;p&gt;One of my biggest goals this year, was to grow Stage to 1.000€ in monthly recurring revenue. I started the year with a baseline of 357€ MRR. Unfortunately, I quickly realized that adding 643€ was going to be tough. My target audience (professional musicians) are hard to reach online and don&amp;rsquo;t care about their websites that much. In the end, I managed to close two deals, adding 58€ in revenue. A third deal I closed in 2016 will start adding another 79€ in MRR this February. So, depending on how you count it, I&amp;rsquo;m at least 506€ short of my goal.&lt;/p&gt;

&lt;p&gt;Here are the things I tried to grow Stage this year:&lt;/p&gt;

&lt;h3&gt;Lead generation&lt;/h3&gt;

&lt;p&gt;To help achieve my MRR goal, I hired a lead generation service. The goal was to get prequalified leads for one-on-one product demos. This was one of the worst experiences this year. While happily taking my money, they failed to deliver &lt;em&gt;at all&lt;/em&gt;. Instead I got put off with empty promises for the first few weeks, until they finally decided to stop responding to messages at all. It took several weeks to get both the subscription cancelled and my money back. The service has been shut down completely since.&lt;/p&gt;

&lt;h3&gt;Doing cold outreach&lt;/h3&gt;

&lt;p&gt;In another effort, I hired a virtual assistant to help me with researching potential leads. It was a great experience and my virtual assistant was a great help. She went through several record label websites, compiled lists of bands, and researched their managers.&lt;/p&gt;

&lt;p&gt;I later used that list and emailed potential customers. While I got a pretty good response rate, none of them were interested in the product.&lt;/p&gt;

&lt;p&gt;In addition, I tried reaching out to record labels. Thanks to a friend, I even got an in-person meeting with a major record label. Unfortunately they also were not interested in my offerings.&lt;/p&gt;

&lt;p&gt;The three deals I closed this year, all came via referrals from friends and people I met in previous years. So, to sum it up, it was a great learning experience with no effect on growth at all.&lt;/p&gt;

&lt;h3&gt;Bye, bye profit…&lt;/h3&gt;

&lt;p&gt;To make things worse, the hosting provider I was using for Stage,  shut down in early 2016. They were a perfect fit for the requirements Stage had. Also they were quite cheap compared to the alternatives. In hindsight that should have been a warning sign.&lt;/p&gt;

&lt;p&gt;They were kind enough to announce the end of their service several months in advance. Still, I had to move to multiple new providers (they were the only ones that combined all of my needs in one service) and the costs skyrocketed. What once was a profitable (read: making a bit more than 0€) product was now losing several hundred euros per month. Most of it was fixed costs, though. Having my MRR goal in mind I had some hopes to make it up in added revenue, but as you know that never happened.&lt;/p&gt;

&lt;p&gt;For 2017, I&amp;rsquo;m hoping to be able restructure parts of the product  to be able to cut costs down to a level where it stops draining my bank account.&lt;/p&gt;

&lt;h2&gt;New endeavors&lt;/h2&gt;

&lt;p&gt;In previous years, I tried to focus on Stage as my one and only side project. In the second half of the year, I allowed myself to think about new ideas.&lt;/p&gt;

&lt;p&gt;One result of this was &lt;a href="http://evgadgets.com"&gt;EVGadgets&lt;/a&gt;, an affiliate website for electric vehicle accessories. The plan was to outsource the writing on Upwork, publish a lot of reviews, get picked up by Google and earn a lot of money using affiliate links.&lt;/p&gt;

&lt;p&gt;I totally underestimated how hard it is to find good people on Upwork, though. None of the reviews I got was great and all had to be edited quite a bit. A few of the reviews were so bad, it didn&amp;rsquo;t even make sense to edit them.&lt;/p&gt;

&lt;p&gt;What really kicked all my motivation out of me, was dealing with the accounting and legal side of hiring through Upwork. The amount of work I had to put in to be on the safe side made it unbearable.&lt;/p&gt;

&lt;p&gt;Right now, the site is still up and I&amp;rsquo;m not ruling out giving it another try. But it&amp;rsquo;s definitely not going to make any money anytime soon.&lt;/p&gt;

&lt;h2&gt;The Podcast&lt;/h2&gt;

&lt;p&gt;In 2016, Christoph Engelhardt and I continued recording episodes for our (German) podcast &lt;a href="http://nebenberufstartup.de"&gt;Nebenberuf Startup&lt;/a&gt;. In total, we published 17 episodes. It&amp;rsquo;s fewer episodes than we set out to publish. We failed to stick to our bi-weekly schedule because of our other commitments. Still, it was fun to record episodes with people like Steli Efti, Åke Brattberg, and Daniel Alm.&lt;/p&gt;

&lt;h3&gt;FemtoConf&lt;/h3&gt;

&lt;p&gt;The most exciting thing coming out of the podcast this year is &lt;a href="http://femtoconf.com"&gt;FemtoConf 2017&lt;/a&gt;. What was meant as a meetup with some of our listeners, turned into an international mini conference. People form Germany, Finland, Czech Republic, Spain, France, Slovenia, Russia, and even the US bought a ticket to be part of it. Check back in a year to learn how it went.&lt;/p&gt;

&lt;h2&gt;Rails Girls Summer of Code and Refugees on Rails&lt;/h2&gt;

&lt;p&gt;There was no &lt;a href="http://railsgirls.com/frankfurt"&gt;Rails Girls Frankfurt&lt;/a&gt; event in 2016. Instead I joined &lt;a href="https://www.facebook.com/roraschaffenburg/"&gt;Refugees on Rails Aschaffenburg&lt;/a&gt; as a coach and &lt;a href="http://railsgirlssummerofcode.org"&gt;Rails Girls Summer of Code&lt;/a&gt; as a supervisor. As always, I loved meeting new people while teaching how to become a software developer.&lt;/p&gt;

&lt;h2&gt;Goals for 2017&lt;/h2&gt;

&lt;h3&gt;Maintain overall revenue&lt;/h3&gt;

&lt;p&gt;While I&amp;rsquo;m a little wary about the usefulness of this goal, I&amp;rsquo;d like to maintain my revenue generated in 2016. While it helps to have a clear number in mind, it also makes it easy to choose the quicker ways to make money over the ones that take time to grow.&lt;/p&gt;

&lt;h3&gt;Relaunch my website with productized consulting offerings&lt;/h3&gt;

&lt;p&gt;I definitely want to relaunch my website this year. For one, I&amp;rsquo;m getting a bit tired by the design. More importantly though, I finally want to focus on offering productized consulting packages.&lt;/p&gt;

&lt;h3&gt;Launch a new product or relaunch Stage with a twist&lt;/h3&gt;

&lt;p&gt;During 2016, it became clear that Stage is a tough product to sell. Maybe it&amp;rsquo;s the target audience, the product, or me just being bad at selling. For 2017, I want to come up with something new and launch it. That might be a new product, or a new major feature of Stage. Whatever it&amp;rsquo;ll be, I want to make sure, it&amp;rsquo;ll fix a pain people are having, making it easier to sell it.&lt;/p&gt;

&lt;h3&gt;Move into a new apartment&lt;/h3&gt;

&lt;p&gt;On the personal side of things, I want to move into a new apartment. We&amp;rsquo;ve been kinda looking for a new one for a while now, but it was never a serious undertaking. Promoting this to a goal for the year will help to make it happen.&lt;/p&gt;

&lt;h2&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;As mentioned earlier, I&amp;rsquo;m a bit disappointed about 2016. I was wishing for a better result when it comes to Stage. Nonetheless, I learned a lot of things while trying to make it work. Most importantly: There are no silver bullets for anything.&lt;/p&gt;

&lt;p&gt;I hope you got some value out of reading this. I tried to include all business related ups and downs I had this year. Thanks for reading and a &lt;strong&gt;Happy New Year 2017&lt;/strong&gt;!&lt;/p&gt;
</content>
  </entry>
  <entry>
    <title>2015 in Review</title>
    <link rel="alternate" href="https://benediktdeicke.com/2016/01/year-in-review/"/>
    <id>https://benediktdeicke.com/2016/01/year-in-review/</id>
    <published>2016-01-04T00:00:00+00:00</published>
    <updated>2016-01-04T00:00:00+00:00</updated>
    <author>
      <name>Benedikt Deicke</name>
    </author>
    <summary type="html">&lt;p&gt;This is the first time I’m writing a “Year in Review” post. In the past, I wasn’t sure about the value of me writing a post like this. However, I always liked to read similar posts by people I know. I hope there’s something in this post that is of value to you, or at least inspires you in some way or another.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;</summary>
    <content type="html">&lt;p&gt;This is the first time I’m writing a “Year in Review” post. In the past, I wasn’t sure about the value of me writing a post like this. However, I always liked to read similar posts by people I know. I hope there’s something in this post that is of value to you, or at least inspires you in some way or another.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Let me introduce myself: I’m Benedikt Deicke. I’m currently running two businesses side-by-side. Most of the time I’m working as a consultant, helping small and medium businesses with their software development projects. Additionally I’m working on a product called Stage, a &lt;a href="http://stagecms.com"&gt;content management system for band websites&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;The year in general&lt;/h2&gt;

&lt;p&gt;All in all, I’m quite happy about how this year turned out to be. The fact that it was off to a rough start probably had some influence on that. One of my clients of 2014 was close to bankruptcy and still owed me a bunch of cash. As a result, I barely made enough money to pay my rent and living in 2014. That was a real downer, but it also had a positive effect on me. For the first time ever, I made the choice to set goals for 2015. I set an &lt;em&gt;overall revenue goal&lt;/em&gt;, committed on &lt;em&gt;launching Stage&lt;/em&gt;, agreed to &lt;em&gt;write 20 blog posts&lt;/em&gt;, and made the decision to &lt;em&gt;refurnish our bedroom&lt;/em&gt;.&lt;/p&gt;

&lt;h2&gt;The SaaS business&lt;/h2&gt;

&lt;p&gt;I started the year focusing on Stage. It wasn’t launched to the public yet, and I was working on some new features to be able to onboard a new client. Everything got done in time and their website launched in late March. After this was done, I considered the product as “good enough” and started working on the marketing website. It took a while, but I was able to achieve my goal and launch it in June.&lt;/p&gt;

&lt;p&gt;In preparation for the launch, I also started sending emails to my launch list. It was a nice learning experience and a first for me. I also set up an email course about building band websites.&lt;/p&gt;

&lt;p&gt;A few weeks after the launch, &lt;a href="https://www.producthunt.com/tech/stage"&gt;Stage got posted on Product Hunt&lt;/a&gt;. It did quite well, and I got a bunch of traffic to its website. Unfortunately, none of this had any effect. I haven&amp;rsquo;t gotten a single signup since the launch of Stage.&lt;/p&gt;

&lt;p&gt;I did some experiments with advertising on Facebook. That got me a few signups on the email course, but none of them converted to signups for the actual product.&lt;/p&gt;

&lt;p&gt;If I didn&amp;rsquo;t have paying customers, I’d consider the product a total failure. For now, I’m assuming that I wasn’t able to reach the target audience with the approaches I tried. As a result, I’ll try some direct sales approaches in 2016. We’ll see how that goes…&lt;/p&gt;

&lt;h2&gt;The consulting business&lt;/h2&gt;

&lt;p&gt;As I mentioned before, the consulting business was off to a rough start this year. Luckily, my client avoided bankruptcy and was able to pay me in February. Still, it wasn’t a great experience. Amazingly enough, something similar happened a few months later. I didn’t get paid for months, because of a delayed investment. Eventually, the client got their investment and has been able to pay on time since.&lt;/p&gt;

&lt;p&gt;During the time I was waiting to get paid, the taxes for both 2013 and 2014, as well as the tax deposits for 2015 were due. That cut a huge hole into my savings account. Luckily, I had known this day would come eventually. I had set aside enough to pay the taxes and still be able to pay my rent while waiting for my invoices to get paid.&lt;/p&gt;

&lt;p&gt;I definitely learned two things this year:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I won’t let anyone talk their way out of an upfront deposit anymore.&lt;/li&gt;
&lt;li&gt;I’ll always keep enough cash in the bank to pay my living for at least 6 months.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Against all odds, I hit my revenue goal for this year. I was able to make enough money to end this year with a profit.&lt;/p&gt;

&lt;h2&gt;The Podcast&lt;/h2&gt;

&lt;p&gt;I’m co-hosting a (German) podcast together with my friend &lt;a href="http://christophengelhardt.com"&gt;Christoph Engelhardt&lt;/a&gt;. It’s called &lt;a href="http://www.nebenberufstartup.de"&gt;Nebenberuf Startup&lt;/a&gt;, and we’re mostly talking about building a software related product business on the side. It started in early 2014 as a result of our bi-weekly mastermind-y calls. We still joke about it being our very own group therapy session. Nonetheless, it became quite successful in 2015. We recorded our &lt;a href="http://nebenberufstartup.de/folge50"&gt;50th episode&lt;/a&gt; just a few weeks back, grew an online community of more than 50 people around it, and interviewed some amazing people.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(If you&amp;rsquo;re into Year in Review posts, &lt;a href="http://www.christophengelhardt.com/year-review-2015/"&gt;check out Christoph&amp;rsquo;s over here&lt;/a&gt;…)&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;Rails Girls and Rails Girls Summer of Code&lt;/h2&gt;

&lt;p&gt;Once again, I was involved in the &lt;a href="http://railsgirls.com/frankfurt"&gt;Rails Girls Frankfurt&lt;/a&gt; event that took place in autumn. This time not just as a coach, but also as a part of the organizing team. The event was a success, and it was great to meet so many inspiring people.&lt;/p&gt;

&lt;p&gt;I also joined the &lt;a href="http://railsgirlssummerofcode.org"&gt;Rails Girls Summer of Code&lt;/a&gt; as a supervisor and coach again. While I’ve been a supervisor in the previous two years, this year was an even better experience. It was awesome to work with so many people on making the world a little bit better.&lt;/p&gt;

&lt;h2&gt;Conferences&lt;/h2&gt;

&lt;p&gt;I only visited one conference in 2015: &lt;a href="http://microconfeurope.com"&gt;MicroConf Europe&lt;/a&gt;. Once again, it was an amazing experience. I was stoked to meet some of my online friends, talk to inspiring people and learn a thing or two during the talks.&lt;/p&gt;

&lt;h2&gt;The personal side of things&lt;/h2&gt;

&lt;p&gt;Okay, I’ll admit it right away: I failed to accomplish my goal to refurnish the bedroom. While my girlfriend and I did some planning, we never found the time to actually pull it off.&lt;/p&gt;

&lt;p&gt;At least we found the time to spend a beautiful weekend in Bremen, as well as some amazing 2,5 weeks in sunny Spain. It was lovely to just relax in the sun for a while.&lt;/p&gt;

&lt;h2&gt;My goals for 2016&lt;/h2&gt;

&lt;p&gt;Having goals for 2015 helped a lot to stay focused and not get distracted by too many things. It sometimes made it easier to come to a decision, because I was able to judge things about their effect on my goals.&lt;/p&gt;

&lt;h3&gt;Increase overall revenue&lt;/h3&gt;

&lt;p&gt;In 2016, want to increase my overall revenue by about 30%. In theory, I could accomplish that by just doing consulting the entire year. However, that would probably have a negative effect on my other goals.&lt;/p&gt;

&lt;h3&gt;Grow Stage to at least 1000€ MRR&lt;/h3&gt;

&lt;p&gt;As I mentioned above, Stage did not grow at all this year. Yes, I onboarded a new client and increased the MRR this way, I already had their commitment in 2014. When it comes to Stage, my primary focus for 2016 will be on growing both revenue and the number of customers.&lt;/p&gt;

&lt;h3&gt;Launch a productized consulting offering&lt;/h3&gt;

&lt;p&gt;Throughout the year, I had several discussions with fellow consultants about productized offerings. I have some ideas in mind, that I’d want to try in 2016. My goal is to have at least one productized consulting offer launched to the public and making some money.&lt;/p&gt;

&lt;h2&gt;Conclusion&lt;/h2&gt;

&lt;p&gt;As always, the year had its ups and downs. In the end, I’d consider it one of my most successful years so far. Let’s hope 2016 exceeds it!&lt;/p&gt;

&lt;p&gt;If you made it this far, I hope you got some value out of this post. All that remains is wishing you a very &lt;strong&gt;Happy New Year 2016&lt;/strong&gt;!&lt;/p&gt;
</content>
  </entry>
</feed>
