<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Shalvah</title>
    <description>The latest articles on DEV Community by Shalvah (@shalvah).</description>
    <link>https://dev.to/shalvah</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F52230%2F87936f32-1b89-4d87-97af-09dfe698930d.jpg</url>
      <title>DEV Community: Shalvah</title>
      <link>https://dev.to/shalvah</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shalvah"/>
    <language>en</language>
    <item>
      <title>Dealing with domain modelling mismatches on external services</title>
      <dc:creator>Shalvah</dc:creator>
      <pubDate>Sun, 23 Nov 2025 03:35:59 +0000</pubDate>
      <link>https://dev.to/shalvah/dealing-with-domain-modelling-mismatches-on-external-services-2gh6</link>
      <guid>https://dev.to/shalvah/dealing-with-domain-modelling-mismatches-on-external-services-2gh6</guid>
      <description>&lt;p&gt;Integrating with external services is a pain. The technical challenges—reliability, performance, caching, downtime, and so on—are well-known and often solvable with infrastructure. But a less-obvious non-technical challenge is &lt;em&gt;domain modelling mismatches&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Domain model mismatches happen because people see the same world differently. To you, it's a House; to them, it's a Building. To you, it's a User; to system A, a Customer; to system B, it's a Client. And the way we describe our world defines how we interact with it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0o55gh1yzywp1xj33sop.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F0o55gh1yzywp1xj33sop.jpg" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I've found that these mismatches are especially tough in these cases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Multiple entrypoints.&lt;/strong&gt; When entities in your system may be created or modified via other systems. Example: Your app has “Customers” that can sign up directly, but your sales/finance team can also create clients in Stripe for invoicing. You need to sync these new users into your system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bidirectional sync.&lt;/strong&gt; When you need to also sync entities created in your system to external systems. For instance, you may need to push some data about your users to your helpdesk such as Intercom or Zendesk, so agents can have richer context about a user when providing assistance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Partial domain coverage.&lt;/strong&gt; When each system only covers a slice of your overall picture. For example, your sales CRM stores sales data, your billing system stores payment info, your helpdesk stores support tickets, and your analytics system stores usage activity. To your system, all these form a complete picture of the “Customer” model.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's a sample system that might have to deal with all these cases. It stores a User object, some parts of which it syncs to some other systems, and some parts which it syncs back from others. Each of the other systems only stores a few pieces of the User object.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fdomain%2520modelling%2520slices.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fdomain%2520modelling%2520slices.png" title="domain modelling slices.png" width="800" height="468"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Of course, ideally, you want to avoid these. And you especially want to avoid them occurring on the same project. But you can't always avoid it.&lt;/p&gt;

&lt;p&gt;I've worked on projects that somehow checked all of these. In our case, we wanted to do things such as tracking a user's journey from way before they actually became users, so we allowed them multiple entrypoints into our system.&lt;/p&gt;

&lt;p&gt;I'll explore some of the ways mismatches show up, with some real examples I've encountered.&lt;/p&gt;

&lt;h3&gt;
  
  
  Naming
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Different systems may use different names.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We had a model called an Appointment. On our calendar tool (Calendly), it was an Event; on our sales tool (Pipedrive), an Activity; on our contact tool (Aircall), a Call.&lt;/li&gt;
&lt;li&gt;Similarly, a Customer on our end was a Calendly Invitee, a Pipedrive Person, and an Aircall Contact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes these are just differences in nomenclature, with no differences in behaviour. In such cases, the only headache is keeping track of the names, especially when they clash with something else in your system.&lt;/p&gt;

&lt;p&gt;Other times, the names reflect the difference in semantic purposes and implicit context that these models carry. Calendly uses the term "event" because their domain is concerned with the generic act of scheduling something to happen at some time; we use "appointment" because we are interested in the time, but also the meeting with someone. Similarly, a call in Aircall focuses only on a conversation via a specific medium, and does not have to be scheduled. And Pipedrive Activities are not necessarily appointments; they only represent an action taken in the sales process.&lt;/p&gt;

&lt;p&gt;Apart from the models themselves, the actions available might have different names: &lt;code&gt;Appointment.create&lt;/code&gt; vs &lt;code&gt;Event.schedule&lt;/code&gt;, &lt;code&gt;Invoice.mark_as_paid&lt;/code&gt; vs &lt;code&gt;Transaction.pay&lt;/code&gt;, etc. Once again, these could be significant behaviour differences or not.&lt;/p&gt;

&lt;p&gt;Overall, these semantic differences are usually pointers to more serious behavioural differences, as we'll see below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Spelling
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;The same thing might be spelt (or **spelled&lt;/em&gt;&lt;em&gt;) differently.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The least significant difference, but it's not to be overlooked. Spelling mistakes can cause bugs if the right tooling isn't in place—for example, checking &lt;code&gt;if event[:status] == :cancelled&lt;/code&gt; (British spelling) when Calendly spells it &lt;code&gt;canceled&lt;/code&gt; (American).&lt;/p&gt;

&lt;h3&gt;
  
  
  Structure
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;One system might represent some information using one field, another might split it across multiple fields or even multiple models.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the mapping is straightforward, this isn't too annoying; you just need to remember to do the conversion at the service boundaries.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We needed to pull in card transactions from an external card provider. On our end, we just cared about the time the user performed the transaction. However, the provider's API had multiple timestamps: &lt;code&gt;authorisationTime&lt;/code&gt;, &lt;code&gt;paymentTime&lt;/code&gt;, &lt;code&gt;paymentLocalTime&lt;/code&gt;, and &lt;code&gt;createdAt&lt;/code&gt;, fields with different meanings. We don't care as much about the distinction between these, so we built a &lt;code&gt;transactionTime&lt;/code&gt; in our system based on a combination of these.&lt;/li&gt;
&lt;li&gt;A user's sales journey was tracked in Pipedrive typically as a Lead object, which transitioned into a Deal object. Even though Pipedrive had a Person object, sales agents tended to track information about the user right on the Deal object. So syncing users to our backend meant we had to pull in data from the Deal as well.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Validations
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;One service might allow things another rejects, and vice versa.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When creating customer accounts for testing, we often used an email ending with a &lt;code&gt;.test&lt;/code&gt; TLD. Our new customer support tool, Dixa, rejected such domains, so when we began importing our existing users into their system, we would run into failures.&lt;/li&gt;
&lt;li&gt;Our sales agents sometimes signed users up with phone numbers only, and a "fake" email (for instance, older people who weren't very Internet-savvy). In many cases, these emails were technically valid, but not allowed by Dixa, so we were unable to import these users into it.&lt;/li&gt;
&lt;li&gt;Pipedrive allowed agents to create "Activities", which mapped to Appointments on our end, but did not enforce the presence of a date and time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Flows
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;The lifecycles and workflows of corresponding domain models might be different across services.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A model in one system might go through states or transitions that don't exist on yours, or vice versa. This hurts especially in a many-to-one setup, where every new provider has its own state machine, and it might not.&lt;/p&gt;

&lt;p&gt;Sometimes, an external model may have states or properties that you have no idea about. Sometimes, no one on the team knows!&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On Calendly, an event can only have two states: &lt;code&gt;active&lt;/code&gt; or &lt;code&gt;canceled&lt;/code&gt;. On our end, our Appointments could be &lt;code&gt;created&lt;/code&gt;, &lt;code&gt;rescheduled&lt;/code&gt;, &lt;code&gt;canceled&lt;/code&gt;, or &lt;code&gt;completed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;We added a wallet provider who performs KYC on each user. They track this via their User model's status. However, our internal user model was much simpler than their KYC workflow, so we couldn't track the KYC status on it.&lt;/li&gt;
&lt;li&gt;We had a bank partner that would send us transactions labelled &lt;code&gt;deleted&lt;/code&gt; and &lt;code&gt;hidden&lt;/code&gt;. Somehow these made their way into our own Transaction model, but there was never any real clarity on what those meant.&lt;/li&gt;
&lt;li&gt;We were syncing card transactions from a card provider. At first, we synced all transactions, but later realized we only cared about &lt;code&gt;Settled&lt;/code&gt; transactions. But then we found out a card transaction's lifecycle is more complex, and it can be &lt;code&gt;Settled&lt;/code&gt; multiple times, or never, or with a different amount. This flow was new to us, so we displayed wrong info to customers in some cases. Only after a few reports did we dive deep into understanding the lifecycle and deciding how we would model it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Restricted and allowed behaviours
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;A service might attribute certain behaviours and rules to their models that you don't.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;These can either limit what you can do or force you to add extra validations.&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Dixa, being a customer service platform, allows multiple emails and phone numbers to be associated to a user. We did not. This created a problem when syncing these users to our system.&lt;/li&gt;
&lt;li&gt;We allowed multiple users to share a phone number, but Dixa does not.&lt;/li&gt;
&lt;li&gt;Calendly events are nigh-immutable. Rescheduling an event cancels the old one and creates a new one. Calendly sends a separate webhook for &lt;code&gt;cancelled&lt;/code&gt;, and one for &lt;code&gt;created&lt;/code&gt;. On our end, this looked like two separate actions.&lt;/li&gt;
&lt;li&gt;Pipedrive activities are &lt;em&gt;very&lt;/em&gt; mutable. This caused us a ton of grief, as the sales agents did a lot of strange (or normal but unexpected) things that regularly caused problems in our system. For instance, we synced only Activities of type "Consultation" to our system (as Appointments). But sometimes, the activity type may be changed later, which wrecks our model. Also, an agent might delete an Activity, which was okay (we can just cancel the appointment on our end), but meant we could not fetch any information about it from the Pipedrive API.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Identifiers
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;The same resource might be identified by different keys on different systems.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Calendly: The email is the unique identifier for an event attendee. However, sometimes our users used a different email to book the appointment on Calendly, which meant we couldn't find them in our database!&lt;/li&gt;
&lt;li&gt;Dixa requires a user to have &lt;em&gt;at least one of&lt;/em&gt; email or phone. This makes sense for their domain (they're a customer contact platform), but led to a duplicates situation for us: if an unsynced user contacted us via email and via phone separately, Dixa would create this as two separate accounts, one with their email, and one with their phone.&lt;/li&gt;
&lt;li&gt;Pipedrive did not require emails or phone numbers for users, or enforce uniqueness of those. This led to a ton of duplicate users (at some point, over two thousand).&lt;/li&gt;
&lt;li&gt;Dixa supports an &lt;code&gt;external_id&lt;/code&gt; field, allowing us to uniquely link a user on their end with one on ours. But this only worked for users created in our system, and synced to theirs. For users created on their end, we had to use heuristics to match them uniquely to a user on ours.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Scheduling
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Different systems might operate on a different schedules.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Some of our banking providers would notify us of new transactions in real time, which is nice, so our database is always up to date. Other providers provide us a list of new transactions once a day, and others once a week. This is a modelling mismatch—to us, a BankStatement is a report of all transactions on your bank account; to them, it's a report of at least all transactions up to the past week.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Dealing with domain model mismatches
&lt;/h2&gt;

&lt;p&gt;First, &lt;strong&gt;think again&lt;/strong&gt;. Before adding a new domain model, ask, "Do we need this? Does this entity represent an actual object intrinsic to our domain, or is it merely a record we mirror from elsewhere?" I've seen setups with domain models representing external entities that had no semantic value to the service. This led to the rest of the codebase being coupled to these unnecessary models. If the model isn't core to your domain or doesn't affect any key business logic, then avoid maintaining yours. In this case, sometimes all you need is a few lines of code mapping one field to another.&lt;/p&gt;

&lt;p&gt;Next, &lt;strong&gt;pick your battles&lt;/strong&gt;. Now that you've decided your own model is probably needed, you must decide how much of a mismatch you're willing to tolerate. Should you try to follow their architecture? Should you relax some of the constraints on your end? Should you assume any future partners will follow the same model? It is tricky, especially when you don't yet know much about the domain. One strategy is to just go with how the external service models it, but this can have the long-term consequence of locking you in.&lt;/p&gt;

&lt;p&gt;The best engineers I've worked with have had the ability to come up with a model that was flexible enough to allow for change or different external providers, without over-engineering things. At the very least, you'll have to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;research further into the domain and the external service's model to understand use cases, limitations, and comparison with your business case&lt;/li&gt;
&lt;li&gt;communicate and brainstorm with product and business leads to better understand their vision&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the technical side, some things we've done include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;external_reference&lt;/code&gt; and &lt;code&gt;external_service&lt;/code&gt; columns. In cases where a single model on our end might map to multiple entities on other services, we used a separate table, consisting of &lt;code&gt;reference&lt;/code&gt;, &lt;code&gt;service&lt;/code&gt;, and &lt;code&gt;entity&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;JSON(B) columns to dump some additional service-specific fields that may be relevant only for that field. Some people find this controversial, but I've found worked better than having sparse columns for each service provider and was easier to maintain than entity-attribute-value.&lt;/li&gt;
&lt;li&gt;decoupling models that represent a sub-process or sub-component of another, such as in our KYC mismatch case above&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consider &lt;strong&gt;separating domain models&lt;/strong&gt;, especially useful when workflows/structures don't match. For instance, in the case of the KYC lifecycle above, we created a &lt;code&gt;User::KYC&lt;/code&gt; model to track specifically the KYC process for that user. On our provider, this information was still a part of the User object, but for us, it allowed us to examine the KYC process independently without needing to clutter our User model. It also served as a form of &lt;a href="https://www.martinfowler.com/bliki/BoundedContext.html" rel="noopener noreferrer"&gt;Bounded Context&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Embrace the workarounds.&lt;/strong&gt; You &lt;em&gt;will&lt;/em&gt; run into cases where there's no "clean" solution, and it's okay to do something unorthodox. Some examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Scheduling mismatch: Calendly webhooks sometimes were delayed, so to prevent users seeing missing data, we implemented a workaround where we would call their API on demand, to "prefetch" the expected webhook. And since we could potentially be processing the webhook at the same time, we had to handle this race condition with a mutex.&lt;/li&gt;
&lt;li&gt;Behaviour mismatch: Since Calendly models appointment rescheduling as cancelling + creating new, we had to at some point implement a "waiting" loop in our webhook processor to check if an appointment had just been rescheduled or truly cancelled. In a related problem, we implemented something called &lt;a href="https://blog.shalvah.me/posts/making-non-atomic-actions-atomic-using-intents" rel="noopener noreferrer"&gt;intents&lt;/a&gt; to get around some restrictions on rescheduling.&lt;/li&gt;
&lt;li&gt;Identifiers mismach: Since there was no exact way to &lt;em&gt;prevent&lt;/em&gt; Pipedrive duplicates, we ended up adjusting our sync process to also look for and merge duplicates whenever syncing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Workarounds must be accompanied by documentation. &lt;strong&gt;Document key differences and details of the integration&lt;/strong&gt;. This applies to any aspect of external service integrations, but can be super helpful for domain modelling, because there are so many decisions, small and large. Every kind of documentation helps, from code comments to full Wiki pages. Future developers who need to touch this code can see at a glance that a certain external service represents information in this weird way, so this is why we must do this random piece of witchcraft.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Establishing strong domain boundaries&lt;/strong&gt; is also key. As much as possible, aim to keep the mapping between different systems at the outer edges of your system. Your core domain models should stay agnostic of the providers you choose, to an extent, and then a Translation Layer can deal with figuring out the various kinks.&lt;/p&gt;

&lt;p&gt;I've also found that &lt;strong&gt;setting up good tooling&lt;/strong&gt; can help smooth things over. Custom lint rules can help, from complex rules that enforce inter-service dependencies to simple ones that correct all spellings of "Cancelled" to "Canceled". Good integration libraries also help. I'm increasingly of the opinion that it's better to make your own API client, because the external SDKs often come with the assumptions of their model that don't translate cleanly to yours.&lt;/p&gt;

&lt;p&gt;And don't forget &lt;strong&gt;observability&lt;/strong&gt;. Make your code &lt;em&gt;shout&lt;/em&gt; at you. Let your system tell you about its state. Don't just set up logs, metrics, and traces; store information that is useful, such as change history, event history, etc, and build useful tools and admin panels to explore that information. For our syncs, we invested a lot of effort in making our webhook processing as observable as possible: we could explore, track, and replay webhook payloads; we could see the result of a specific webhook, and what entities it had created or modified. This greatly aided us in finding the errors caused by mismatches. In the case where we needed to prefetch items from Calendly, we added metrics to tell us how often we had to do this, how often it was actually useful, and the overall impact on our system.&lt;/p&gt;

&lt;p&gt;Lastly, &lt;strong&gt;architect for change&lt;/strong&gt;. I've learnt that migrations are inevitable. Assume that someday we will have to migrate to another provider, or APi, or infrastructure, and so on. So don't architect to the current provider, but also don't make your architecture as generic as possible. Instead, focus making it robust, observable, documented and clear enough that changing it is not an expensive process—its dependencies are not hidden, its tradeoffs are understood, and its structure is clear.&lt;/p&gt;

&lt;p&gt;In general, &lt;a href="https://en.wikipedia.org/wiki/Domain-driven_design" rel="noopener noreferrer"&gt;domain driven design&lt;/a&gt; has several patterns that help here. Of course, they come with their own tradeoffs, so choose appropriately. I'm personally still exploring many of these patterns to see how I can leverage them without taking on the overhead.&lt;/p&gt;

</description>
      <category>softwaredesign</category>
      <category>engineeringtechniques</category>
    </item>
    <item>
      <title>All decisions are wrong, but some are better</title>
      <dc:creator>Shalvah</dc:creator>
      <pubDate>Tue, 04 Nov 2025 17:16:32 +0000</pubDate>
      <link>https://dev.to/shalvah/all-decisions-are-wrong-but-some-are-better-5gbh</link>
      <guid>https://dev.to/shalvah/all-decisions-are-wrong-but-some-are-better-5gbh</guid>
      <description>&lt;p&gt;&lt;em&gt;Everything is fucked, no systems are sound, all decisions are wrong, and we should destroy all software.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I've noticed this recurring trend at my last several companies:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We have an existing project. People take jabs at it, commenting on how it's "a mess" and "legacy code". I've seen this applied to decades-old systems as well as three-year-old ones.&lt;/li&gt;
&lt;li&gt;We start a new project. People are happy, commenting about how it's nice to get a fresh start, stick to "clean code", and avoid making the mistakes made on the other project.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These are perfectly reasonable attitudes. Software systems grow in complexity, and some can turn into messes really quickly. Working on a greenfield project is always great—who doesn't like a fresh start? A decade ago, I was the one advocating for and proposing software rewrites. &lt;em&gt;Let's clean up the mess!&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;But there's a last part of the loop:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Eventually, people start realizing: We may not make the mistakes of the other project, but we'll make our own set of mistakes. Our code ends up being legacy code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Flegacy-code-cycle.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Flegacy-code-cycle.png" title="legacy-code-cycle.png" width="502" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But here's the thing: it's the circle of life!&lt;/p&gt;

&lt;p&gt;There are no perfect systems. Our code evolves, has to handle more cases, becomes legacy. It becomes full of cruft, tradeoffs and pain. How do we avoid this?&lt;/p&gt;

&lt;p&gt;First, know that these two things are true:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;All decisions are wrong. All architectures are inadequate. All abstractions are poor. All nontrivial code is hard to read.&lt;/li&gt;
&lt;li&gt;Some decisions are less wrong than others. Some architectures are better than others. Some abstractions are better. Some code is easier to read than others. Some systems &lt;em&gt;are&lt;/em&gt; easier to work with.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;However, the line between "wrong" and "less wrong" is more than just your feeling of legacy and confusion. Unless you start a new project every three years, you'll still end up with that feeling. There must be a better way.&lt;/p&gt;

&lt;h2&gt;
  
  
  All decisions are wrong
&lt;/h2&gt;

&lt;p&gt;You'll never architect the &lt;em&gt;perfect&lt;/em&gt; system. Oh, using integers for IDs is bad because of enumeration and overflow? How about UUIDs? Oh, that's bad for storage and search performance? How about some other random ID format? And so on, and so forth. There will always be some bottleneck, some situation that you can't fix.&lt;/p&gt;

&lt;p&gt;During design/architecture discussions, I tell folks, "We need to accept that whatever decision we take is the wrong one. We just need to pick one we can live with." Once you go beyond CRUD, you begin to solve problems where you have to play by certain constraints. Because of this, you can't merely pick an idealized solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  But some are better
&lt;/h2&gt;

&lt;p&gt;That said, it is undeniable: there are clearly some ways that are worse. Fetching all items from a database and letting the client paginate it is obviously a worse decision than implementing pagination on the backend. But in real-world business cases, we deal with a lot more non-obvious situations. Should we normalize or inline this data? Should we use strings or integers here?&lt;/p&gt;

&lt;p&gt;My litmus test is this: If you claim that a certain architecture/design is worse than another, you must be able to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;point out its problems (with data, preferably)&lt;/li&gt;
&lt;li&gt;point out its strengths (Yes, you need to try to understand why it was chosen in the first place.)&lt;/li&gt;
&lt;li&gt;explain why the alternative fits this situation better&lt;/li&gt;
&lt;li&gt;point out the problems of the alternative&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In summary, it is not enough to say "This code sucks." You must be able to say why it sucks, why it might have been chosen, point out a better option, and acknowledge the tradeoffs involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  It's not just about what you do, it's about how you do it
&lt;/h2&gt;

&lt;p&gt;In the best systems I've worked on, there were some unconventional things that had to be done. These were things that might make a new joiner immediately go "WTF". And yet they did not make the system a pain to work with. How?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First, picking and sticking with &lt;strong&gt;conventions&lt;/strong&gt;.
I used to be very anti-convention, feeling that they stifled creative freedom and experimentation. That can be true. But I've also come to see that going with a convention, even a poor one, makes things easier for everyone. We don't have to judge these situations afresh every time, deciding again on each one. We can save our energy for the important decisions.&lt;/li&gt;
&lt;li&gt;Next, &lt;strong&gt;documenting&lt;/strong&gt; assumptions, decisions and details. Make things easy to find.
Document the conventions. Document when and why you stray from these conventions. One of the biggest things that makes a codebase "age" quickly is developers doing strange things and leaving no explanation. Write, write, write. Write comments, write pull request descriptions, write wiki docs, draw diagrams. Leave breadcrumbs. Even if the system is a mess, make it an understandable mess. Better overdocumented than undocumented.&lt;/li&gt;
&lt;li&gt;Finally, an attitude of &lt;strong&gt;constant improvement&lt;/strong&gt;. The best systems I worked with recognized that the system was a living, evolving being. Rather than fixating on "getting it right" from the start, we were open to suggestions for improvements. We encouraged people to fix things they saw wrong, or propose new conventions if they solved problems we were having. Sometimes, we had a running list of technical improvements we wanted to work on, and encouraged folks to pick up tasks from there.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The human cost
&lt;/h2&gt;

&lt;p&gt;We struggle with these legacy systems, not merely because they're technically worse, but because they overload us. They either require more developers, or they place a larger mental burden on each developer. They require more intricate solutions to common problems, or more effort to change or adjust things. One of my worst experiences was a project where changing a simple label in the UI took me three days. It really sucked, and had me questioning my skills.&lt;/p&gt;

&lt;p&gt;So whether architecting our new system, or living with the old, an important question to ask is &lt;em&gt;How can I reduce the human cost here?&lt;/em&gt; How can I make things easy to discover? How can I make the system easy to change?&lt;/p&gt;

</description>
      <category>softwaredesign</category>
      <category>engineeringtechniques</category>
    </item>
    <item>
      <title>Making non-atomic actions atomic using intents</title>
      <dc:creator>Shalvah</dc:creator>
      <pubDate>Mon, 27 Oct 2025 23:08:28 +0000</pubDate>
      <link>https://dev.to/shalvah/making-non-atomic-actions-atomic-using-intents-25lg</link>
      <guid>https://dev.to/shalvah/making-non-atomic-actions-atomic-using-intents-25lg</guid>
      <description>&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;At a previous job, we built a service we called our operations platform. Its aim was to unify the SaaS tooling used by our ops teams with domain data from our core business, so we could directly see how the ops efforts contributed to our growth. A key feature here was an appointment booking system, where users could book consultations with our sales team. For a start, we chose to store appointments in our database, but rely on Calendly for the actual scheduling. I'm not sure this was the right choice, but the key reason was that our sales team depended on several of Calendly's features, and we needed to move fast.&lt;/p&gt;

&lt;p&gt;With this approach, the sales team continued to work in Calendly, and we embedded the Calendly UI in our app for users to schedule appointments. We would then receive webhooks from Calendly and create the appointment in our backend. Our backend served as the source of truth for our app.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fcalendly-operations-platform-sequence-diagram.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fcalendly-operations-platform-sequence-diagram.png" title="calendly-operations-platform-sequence-diagram.png" width="745" height="558"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As one might expect, we encountered several issues in this approach. This post is about one of those.&lt;/p&gt;

&lt;h2&gt;
  
  
  Constraints
&lt;/h2&gt;

&lt;p&gt;One of the features our sales team relied on was Calendly's pooling feature (&lt;a href="https://help.calendly.com/hc/en-us/articles/4402432846999-Round-robin-distribution-overview" rel="noopener noreferrer"&gt;round robin&lt;/a&gt;). They could create pools consisting of several agents, and a customer who booked an appointment would be assigned to one of the agents in the pool. However, if the customer later rescheduled, the customer would keep the same agent they were assigned to. The business wanted to change this, so that reschedulings would go back into the pool.&lt;/p&gt;

&lt;p&gt;Calendly didn't provide any option for this. The only solution was to do it in two steps: cancel the existing appointment on Calendly and book a new one, thereby picking a new agent from the pool. But this was untenable: we needed the process to be seamless for our users.&lt;/p&gt;

&lt;h2&gt;
  
  
  Solution
&lt;/h2&gt;

&lt;p&gt;To solve this, I came up with a high-level API called &lt;code&gt;PendingCancellation&lt;/code&gt;s. A PendingCancellation represents an &lt;em&gt;intent&lt;/em&gt; to cancel an appointment. This intent may be &lt;strong&gt;acted upon&lt;/strong&gt; (thereby cancelling the appointment) or &lt;strong&gt;discarded&lt;/strong&gt;. We would still do rebooking in multiple steps, but with a PendingCancellation, it would look like this (at a high level):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;create PendingCancellation (intent to cancel old appointment)
 -&amp;gt; book new appointment
  -&amp;gt; execute PendingCancellation (actually cancel old appointment)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the implementation, we wanted to present this as a single step to our customers. We also need to keep in mind this detail: appointments are actually created on Calendly (which calls them "events"), then synced to our database via webhooks.&lt;/p&gt;

&lt;p&gt;So the full implementation looked like this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User clicks "Reschedule" in our app&lt;/li&gt;
&lt;li&gt;The app tells our backend to create a &lt;code&gt;PendingCancellation&lt;/code&gt;, linked to the user and appointment in our database.&lt;/li&gt;
&lt;li&gt;Then the app redirects to Calendly for booking a &lt;em&gt;new&lt;/em&gt; appointment (not rescheduling)&lt;/li&gt;
&lt;li&gt;User completes the booking&lt;/li&gt;
&lt;li&gt;Backend receives Calendly's &lt;code&gt;event.created&lt;/code&gt; webhook.&lt;/li&gt;
&lt;li&gt;We process the webhook. If there is a PendingCancellation, we go ahead and cancel the old appointment and create the new one.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fcalendly-pendingcancellation-sequence-diagram.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fcalendly-pendingcancellation-sequence-diagram.png" title="calendly-pendingcancellation-sequence-diagram.png" width="800" height="497"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure modes
&lt;/h2&gt;

&lt;p&gt;This was a fine API. But, of course, I also had to think about failure modes:&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;em&gt;What happens if the user changes their mind and exits without completing the new booking?&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;Nothing. The old appointment remains active. All unprocessed PendingCancellations are ignored and cleaned up after a few hours. This is the beauty of the intent system: if it isn't explicitly acted upon, it has no effect.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;em&gt;What happens if the user enters this flow (clicks "Reschedule") multiple times?&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;We only allow for one PendingCancellation per appointment. If there is an existing PendingCancellation, we simply update its timestamp.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;em&gt;What about transactional guarantees (consistency)? Since this is not an atomic operation, what happens if one action fails (cancelling the old one or booking the new one)?&lt;/em&gt;
&lt;/h3&gt;

&lt;p&gt;This was tricky. Normally, we could wrap the whole operation on our backend in a database transaction (cancel old + create new). This way, if one failed, there would be no side effects, and we could safely retry the webhook.&lt;/p&gt;

&lt;p&gt;However, since the appointments are first created on Calendly before being synced to our database, we don't have control over the "create" part. Regardless of what happens in our database transaction, the new appointment already exists on Calendly, and it might cause confusion for a user if they saw no change in their app.&lt;/p&gt;

&lt;p&gt;So I chose to do something different: when we receive the webhook, first create the new appointment before cancelling the old one. Also don't bother wrapping it in a transaction. That way, if the cancellation failed, we would end up showing the user both old and new appointments. This is undesirable, of course, but, to my mind, it was better to see 2 appointments than 0 (if cancel was executed first).&lt;/p&gt;

&lt;p&gt;In hindsight, I'm not sure this was the better decision. Seeing 2 appointments after rebooking might be worse, since it might tempt the user to try cancelling the old one. It also meant we had to make the webhook processing idempotent so we could safely retry the webhook without creating a third appointment. 😄&lt;/p&gt;

&lt;h2&gt;
  
  
  Dealing with limitations
&lt;/h2&gt;

&lt;p&gt;We still had a few more challenges to solve. A key one was linking the new appointment from Calendly to the old one in our database. Essentially, &lt;em&gt;when we receive Calendly's &lt;code&gt;event.created&lt;/code&gt; webhook, how do we know if this is a rebooking&lt;/em&gt;? Calendly's UI doesn't allow us to attach any metadata to the event (such as &lt;code&gt;old_appointment_id=abc&lt;/code&gt;), so we have no way of differentiating between our rebooked appointments and truly new appointments.&lt;/p&gt;

&lt;p&gt;The only thing we had was the user ID (sort of—but that's a story for another time). We also knew two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a user can only have one appointment of a given &lt;code&gt;type&lt;/code&gt; at any time&lt;/li&gt;
&lt;li&gt;the new and old appointments must have the same &lt;code&gt;type&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I worked with that: when we receive a new event booking webhook, fetch all PendingCancellations for that user. From there, we use heuristics: find the &lt;em&gt;most recent&lt;/em&gt; PendingCancellation matching the new appointment's &lt;em&gt;type&lt;/em&gt;, and pick that. This worked reliably.&lt;/p&gt;

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

&lt;p&gt;I'm really proud of the PendingCancellation API. It's a nice, lightweight solution that fits neatly into our constraints and is generally reliable. It of course comes with tradeoffs, but is generally pleasant to work with.&lt;/p&gt;

&lt;p&gt;Two sources of inspiration I had were Stripe's &lt;a href="https://docs.stripe.com/payments/paymentintents/lifecycle" rel="noopener noreferrer"&gt;PaymentIntents&lt;/a&gt; and &lt;a href="https://developer.android.com/reference/android/content/Intent" rel="noopener noreferrer"&gt;Android Intents&lt;/a&gt;. They're not exactly similar, but the name "Intent" conveys a desire to perform an action, without any actual commitment. It is entirely possible for the desired action to expire or be invalidated before it can be completed, and the intent should handle this gracefully. I was also inspired by the concept of &lt;a href="https://en.wikipedia.org/wiki/Change_request" rel="noopener noreferrer"&gt;change requests&lt;/a&gt;, something we already used quite effectively in our codebase.&lt;/p&gt;

</description>
      <category>softwaredesign</category>
      <category>errors</category>
      <category>engineeringconcepts</category>
      <category>engineeringtechniques</category>
    </item>
    <item>
      <title>DIY Smart home project: Presence-activated lights</title>
      <dc:creator>Shalvah</dc:creator>
      <pubDate>Thu, 01 May 2025 12:43:25 +0000</pubDate>
      <link>https://dev.to/shalvah/diy-smart-home-project-presence-activated-lights-5fnn</link>
      <guid>https://dev.to/shalvah/diy-smart-home-project-presence-activated-lights-5fnn</guid>
      <description>&lt;h2&gt;
  
  
  Background
&lt;/h2&gt;

&lt;p&gt;I got &lt;a href="https://www.amazon.de/-/en/dp/B0DJ8QJL5L?" rel="noopener noreferrer"&gt;these&lt;/a&gt; Antela smart bulbs a while ago. They connect to my home network, and I control them from my phone via either the &lt;a href="https://play.google.com/store/apps/details?id=com.tuya.smart&amp;amp;hl=en" rel="noopener noreferrer"&gt;Tuya Smart app&lt;/a&gt; (Tuya is a Chinese IoT service provider) or Google Home. I got the smart bulbs rather than smart switches because they allow me to customize the brightness and colour, a feature I found helpful when my eyes were super sensitive to light after eye surgery.&lt;/p&gt;

&lt;p&gt;I also recently began to use &lt;a href="https://www.home-assistant.io/" rel="noopener noreferrer"&gt;Home Assistant&lt;/a&gt; as my smart home software. It's an open-source and more powerful alternative to Google Home/Amazon Alexa/Apple HomeKit/Samsung SmartThings. With those vendor platforms, you're limited to what they support. With Home Assistant, you can build your own setups and connect things as you wish, whether it's totally DIY or consumer-ready products from vendors. You install it onto an always-on computer (I have mine running on a Raspberry Pi 5) and connect it to your other smart devices, and it manages them for you according to your configuration&lt;/p&gt;

&lt;p&gt;I recently got an idea for a project. In my typical daily workflow, I spend most time in my office and living room area, occasionally dashing to the bedroom to get something. I find it a minor annoyance having to stop to turn on the lights, so I wanted to have the bedroom lights turn on whenever I walked in.&lt;/p&gt;

&lt;p&gt;This means I need some sort of motion- or proximity-activated light. In the world of smart homes, there are always many different options. There are consumer-ready solutions, such as &lt;a href="https://www.amazon.de/-/en/Auraglow-Operated-Activated-Removable-Cordless/dp/B010NEW16I/ref=sr_1_6" rel="noopener noreferrer"&gt;this light&lt;/a&gt;—plug it in, and that's all. When you come close, the light comes on. There are kits such as the &lt;a href="https://shop.everythingsmart.io/products/everything-presence-one-kit" rel="noopener noreferrer"&gt;Everything Presence One&lt;/a&gt; that only provide the sensor + networking functionality—plug it in, connect to your network, and it shows up as a device in your Home Assistant instance. From there, you can configure Home Assistant to turn on your lights based on the device state.&lt;/p&gt;

&lt;p&gt;I wanted to go a little more DIY. Instead of buying the presence sensor kit, I would build mine. My plan was to get an actual presence sensor element (such as &lt;a href="https://de.rs-online.com/web/p/entwicklungstools-sensorik/7813024" rel="noopener noreferrer"&gt;this&lt;/a&gt;) and set that up to send data to Home Assistant, which can then control my lights.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schematic
&lt;/h2&gt;

&lt;p&gt;Here's the setup:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fpresence-sensor-schematic.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fpresence-sensor-schematic.png" title="presence-sensor-schematic.png" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The presence sensor detects human presence in an area.&lt;/li&gt;
&lt;li&gt;The microcontroller (physically wired to the sensor) powers the sensor and relays readings from it wirelessly to my Home Assistant instance.&lt;/li&gt;
&lt;li&gt;Home Assistant turns on or off my lights when needed, depending on the readings from the sensor and my configured automation.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of manually programming the microcontroller, I'll run &lt;a href="https://esphome.io/" rel="noopener noreferrer"&gt;ESPHome&lt;/a&gt; on it, for two reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ESPHome is made by the makers of Home Assistant, so it already integrates with it&lt;/li&gt;
&lt;li&gt;ESPHome simplifies the programming so you can do everything with a few lines of YAML. No code required.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Equipment
&lt;/h2&gt;

&lt;p&gt;For the microcontroller, I got an ESP32, specifically the &lt;code&gt;ESP32-C3-DevKitM-1U&lt;/code&gt; (that's a mouthful!). The ESP32 naming convention still confuses me (&lt;a href="https://core-electronics.com.au/guides/esp-32-naming-conventions/" rel="noopener noreferrer"&gt;somewhat helpful article&lt;/a&gt; and &lt;a href="https://docs.espressif.com/projects/esp-idf/en/v5.0/esp32c3/hw-reference/chip-series-comparison.html" rel="noopener noreferrer"&gt;chips comparison&lt;/a&gt;), but here are my reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I got an ESP32 because I've seen it recommended a lot. It's small and cheap, easy to program to do "one thing". Other options could be the Raspberry Pi Pico W (which ESPHome also supports) and an Arduino (no idea about support).&lt;/li&gt;
&lt;li&gt;I got the C3 series because it has WiFi and &lt;a href="https://esphome.io/guides/faq" rel="noopener noreferrer"&gt;ESPHome recommended it&lt;/a&gt; as the low-power version.&lt;/li&gt;
&lt;li&gt;I got the DevKit version because it comes with some conveniences for noobs: USB-to-serial converter, voltage regulator, GPIO pins already exposed, and reset &amp;amp; boot buttons.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the sensor, I got a passive infrared (PIR) sensor, &lt;a href="https://www.adafruit.com/product/4666" rel="noopener noreferrer"&gt;Adafruit’s BS412&lt;/a&gt;. It has a great range of several meters, and is easy to wire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Aside: Active vs Passive IR sensors
&lt;/h2&gt;

&lt;p&gt;I initially got the &lt;a href="https://www.vishay.com/en/product/82910/" rel="noopener noreferrer"&gt;Vishay TSSP93038SS1ZA&lt;/a&gt; infrared sensor, but this was a mistake. During testing, I realized that this is an active infrared sensor. It detects a source of IR light pulsating at a specific frequency, like an IR LED or a TV remote, which means it needs a dedicated IR emitter.&lt;/p&gt;

&lt;p&gt;Active IR systems are good for "light barriers": you have an emitter at one end and the receiver at the other. When something blocks the beam coming from the emitter, you trigger something. This kind of system is used in elevator doors, automatic taps, soap dispensers, towel dispensers, hand dryers, item counters on conveyor belts etc. But it wouldn’t work well for my desired setup; I need to sense the presence of a human within a given area (not only in a straight line). PIR sensors detect natural radiation of the human body, and are a better fit for the presence sensing application.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Flight-barrier.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Flight-barrier.png" title="light-barrier.png" width="800" height="266"&gt;&lt;/a&gt;&lt;em&gt;A light barrier simply detects when the light source is interrupted. &lt;a href="https://www.ifm.com/de/en/shared/technologies/light-curtains/technology" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fhuman-presence-sensor-installation-and-placement-instructions-image-pnfh6wdy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fhuman-presence-sensor-installation-and-placement-instructions-image-pnfh6wdy.png" title="human-presence-sensor-installation-and-placement-instructions-image-pnfh6wdy.png" width="800" height="333"&gt;&lt;/a&gt;&lt;em&gt;A presence sensor has a wider, 3D detection area, and works by sensing the IR radiation from humans. &lt;a href="https://knowledge.akuvox.com/docs/human-presence-sensor-installation-and-placement-instructions" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting used to the ESP32 and the sensor
&lt;/h2&gt;

&lt;p&gt;From here, I could jump straight to installing ESPHome on the ESP32 and configuring it. But of course I had to first program it manually and test some basic behaviour. There are multiple ways to program an ESP32, such as with Python or via the Arduino IDE, but I followed Espressif's &lt;a href="https://docs.espressif.com/projects/esp-idf/en/stable/esp32/get-started/index.html" rel="noopener noreferrer"&gt;Getting Started guide&lt;/a&gt;, which uses their official C framework, ESP-IDF.&lt;/p&gt;

&lt;p&gt;I did get stuck a few times, but nothing too serious. The only other microcontroller I've programmed is the Raspberry Pi Pico Zero, and that was much easier, because the it uses Python and has simpler &lt;a href="https://picozero.readthedocs.io/en/latest/index.html" rel="noopener noreferrer"&gt;guide docs&lt;/a&gt;. I'm new to ESP32, and it seems more powerful but more complex. ESP-IDF is in C and has fewer beginner guides.&lt;/p&gt;

&lt;p&gt;It also took me a while to find &lt;a href="https://docs.espressif.com/projects/esp-idf/en/v4.4.5/esp32c3/hw-reference/esp32c3/user-guide-devkitm-1.html#pin-layout" rel="noopener noreferrer"&gt;the right pinout diagram&lt;/a&gt;, but it helped that the ESP32 had labels on the actual board (DevKit feature?).&lt;/p&gt;

&lt;p&gt;To connect the BS412 to the ESP32, I followed the &lt;a href="https://www.adafruit.com/product/4666" rel="noopener noreferrer"&gt;product page&lt;/a&gt; and &lt;a href="https://cdn-shop.adafruit.com/product-files/4666/Datasheet.pdf" rel="noopener noreferrer"&gt;datasheet [PDF]&lt;/a&gt;: pin 1 and 2 to GND (ground), pin 3 to 3.3V, and pin 4 (output) to a GPIO pin. (If you have trouble identifying which pin is which, there's a tiny tab on the sensor's head. Check the datasheet for the cross-section of the circular head to see the numbering of each pin relative to the tab.)&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fpir-basic.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fpir-basic.svg" title="pir-basic.svg" width="320" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I also connected a LED to another GPIO as an indicator (note: this isn't necessary, since the DevKit comes with an RGB LED on GPIO8). Then I wrote an IDF program to read its value and light the LED. Interestingly, the PIR sensor has a HIGH output when it detects motion, different from the active IR sensor, which has LOW output when it detects an IR signal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight c"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;stdio.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"freertos/FreeRTOS.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"freertos/task.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"driver/gpio.h"&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;"esp_log.h"&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;TAG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"demo"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="cp"&gt;#define PRESENCE_SENSOR_PIN 19
#define INDICATOR_PIN 10
&lt;/span&gt;
&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;app_main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;void&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;gpio_reset_pin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PRESENCE_SENSOR_PIN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;gpio_set_direction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PRESENCE_SENSOR_PIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GPIO_MODE_INPUT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;gpio_reset_pin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INDICATOR_PIN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;gpio_set_direction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INDICATOR_PIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GPIO_MODE_OUTPUT&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;sensor_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;gpio_get_level&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PRESENCE_SENSOR_PIN&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sensor_value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ESP_LOGI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"No motion detected (LOW)"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;gpio_set_level&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INDICATOR_PIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;ESP_LOGI&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TAG&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Motion (HIGH), %d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sensor_value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
            &lt;span class="n"&gt;gpio_set_level&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INDICATOR_PIN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;vTaskDelay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pdMS_TO_TICKS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;  &lt;span class="c1"&gt;// Wait for 500ms before reading again&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aaaand this works. Nice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tuning the sensor
&lt;/h2&gt;

&lt;p&gt;Now we begin to tweak and configure it so it works how we want.&lt;/p&gt;

&lt;p&gt;The key sensor parameter to be tuned is the &lt;em&gt;on-time&lt;/em&gt;, the length of time the signal stays HIGH after detecting motion. There's also a &lt;em&gt;lock time&lt;/em&gt;, which is a minimum amount of time the signal will stay LOW before entering another HIGH phase.&lt;/p&gt;

&lt;p&gt;The sensor works in phases:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Motion detected → output goes HIGH and stays like this for at least the on-time&lt;/li&gt;
&lt;li&gt;Motion stops → after a short time (on-time exceeded), output goes LOW&lt;/li&gt;
&lt;li&gt;Lock time starts now → during this, no new detection possible&lt;/li&gt;
&lt;li&gt;After lock time ends → PIR is ready to detect again&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Presence sensors are usually used in such phased setups: when a person enters the area, turn on the lights. Then turn them off if the person has not moved for a while. If the person is still in the area, they'll move and the lights will come back on.&lt;/p&gt;

&lt;p&gt;The sensor's datasheet explains how to adjust the on-time, but the lock time is not configurable for this sensor. Luckily, it's small enough (2.3 seconds) that it doesn't matter. Adjusting the on-time is done by adjusting the potential difference between pin 2 and ground using a voltage divider setup, diagrammed below:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fpir-with-voltage-divider-2.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fpir-with-voltage-divider-2.svg" title="pir-with-voltage-divider-2.svg" width="500" height="360"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The datasheet has the recommended resistor values for the resistors in order to achieve different on-times. The shortest on-time is 2 seconds, which you get by connecting the pin directly to ground (0 V difference). The longest on-time is 1 hour (potential difference of 0.5 * the supply voltage). For my case, I wanted an on-time of around a minute, so I used a couple of resistors that added up to 310 kΩ as R2 (R1 is always 1 MΩ).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fesp32-initial-setup-with-voltage-divider-and-led-on.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fesp32-initial-setup-with-voltage-divider-and-led-on.jpg" title="esp32-initial-setup-with-voltage-divider-and-led-on.jpg" width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;An alternate way to adjust the on-time is by &lt;em&gt;not&lt;/em&gt; adjusting it, but using software to override the effect. You can add a delay in your programming, which is essentially implementing a state machine: when the sensor goes HIGH, your program sets its internal state to HIGH and keeps it that way until your desired on-time expires, ignoring any sensor changes during that time. I eventually switched to this. More on that later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Switching to ESPHome
&lt;/h2&gt;

&lt;p&gt;The next step: install ESPHome onto the microcontroller, and program it via ESPHome's YAML config. There are 3 stages to this (see &lt;a href="https://esphome.io/guides/getting_started_hassio" rel="noopener noreferrer"&gt;Getting Started&lt;/a&gt;):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prerequisite: Install ESPHome Device Builder add-on in Home Assistant&lt;/li&gt;
&lt;li&gt;Install ESPHome on the microcontroller&lt;/li&gt;
&lt;li&gt;Configure the device to connect to your WiFi&lt;/li&gt;
&lt;li&gt;Add the device as a Home Assistant entity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The installation is mostly straightforward: plug it in via USB (if it has a serial-to-USB chip, which the DevKits do), and then use ESPHome Device Builder to flash it with the firmware (for which you'll likely be redirected to &lt;a href="https://web.esphome.io/" rel="noopener noreferrer"&gt;ESPHome Web&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;I had trouble with this though. For anyone who runs into this in future (or myself), I'm documenting my issues and fixes in asides. Click to see the details.After running “Prepare for first use”, I kept getting “An error occurred. Improv Wi-Fi Serial not detected”. I think that ESPHome Web is supposed to use &lt;a href="https://www.improv-wifi.com/" rel="noopener noreferrer"&gt;Improv WiFi&lt;/a&gt; to send my WiFi credentials to the newly flashed microcontroller, but somehow that was failing. I eventually found an alternative on &lt;a href="https://community.home-assistant.io/t/new-device-an-error-occurred-improv-wi-fi-serial-not-detected/425360/16" rel="noopener noreferrer"&gt;the forums&lt;/a&gt;: the flashing process also turns my ESP32 into a temporary WiFi access point, and all I needed to do was connect to that, then visit the gateway address (highlighted below). That page showed me a form where I could enter the credentials for my home network, and that was done. From there, it turned off the AP, then took about 2 minutes before showing me a newly discovered device in ESPHome.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2FESP32%2520AP%2520mode" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2FESP32%2520AP%2520mode" title="ESP32 AP mode" width="1000" height="592"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fesp32-ap-wifi-config" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fesp32-ap-wifi-config" title="esp32-ap-wifi-config" width="1000" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once that is done, you should see the newly discovered device in ESPHome, from where you can "Take Control".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fesphome-discovered-device" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fesphome-discovered-device" title="esphome-discovered-device" width="1000" height="791"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Taking control installs a basic ESPHome config onto the device, prompting you for some initial config such as a friendly name.&lt;/p&gt;

&lt;p&gt;I ran into another issue here. It complained about the device not being able to connect to my WiFi, even though I had already done that in the previous step. I guess the WiFi configuration in ESPHome Web and ESPHome Device Builder are different, but I'm not sure why it didn't prompt me to add the WiFi credentials to the config first before trying to connect. The resolution for this was clicking "Edit" to edit the newly generated config YAML file, and then add a section containing my WiFi credentials ([docs](&lt;a href="https://esphome.io/components/wifi.html%5C)%5C" rel="noopener noreferrer"&gt;https://esphome.io/components/wifi.html\)\&lt;/a&gt;), then click "Save" and "Install".Finally, the device was successfully set up, and my device had a basic config:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fesphome-initial-config" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fesphome-initial-config" title="esphome-initial-config" width="1000" height="555"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now time to configure my sensor. Here's what I added to the file at first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;binary_sensor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpio&lt;/span&gt;
    &lt;span class="na"&gt;pin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GPIO19&lt;/span&gt;
    &lt;span class="na"&gt;device_class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;occupancy&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bedroom&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Presence"&lt;/span&gt;
&lt;span class="na"&gt;light&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;status_led&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Presence&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;detected"&lt;/span&gt;
    &lt;span class="na"&gt;pin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GPIO10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This declares a &lt;a href="https://esphome.io/components/binary_sensor/" rel="noopener noreferrer"&gt;Binary Sensor&lt;/a&gt;, a sensor which can be in one of two states (on/off). This component allows you to use any HIGH/LOW sensor without needing a custom integration for it. The &lt;code&gt;occupancy&lt;/code&gt; &lt;a href="https://www.home-assistant.io/integrations/binary_sensor/#device-class" rel="noopener noreferrer"&gt;device class&lt;/a&gt; tells Home Assistant how to interpret the states.&lt;/p&gt;

&lt;p&gt;The second section declares a &lt;a href="https://esphome.io/components/light/status_led" rel="noopener noreferrer"&gt;Status LED&lt;/a&gt;. ESPHome automatically knows to use it as an indicator—turn it on when the sensor is on, and vice versa.&lt;/p&gt;

&lt;p&gt;This is beautiful. A few lines of YAML and I had this working!&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating with Home Assistant
&lt;/h2&gt;

&lt;p&gt;The final piece was to hook it up with Home Assistant. To do this, I went to Home Assistant's &lt;em&gt;Settings -&amp;gt; Devices&lt;/em&gt;. The newly added ESPHome device had been discovered, so I clicked Add, assigned it to my Bedroom, and we were ready.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fhomeassistant-new-device-info" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fhomeassistant-new-device-info" title="homeassistant-new-device-info" width="1000" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I went to add the automation, and discovered Home Assistant already comes with an inbuilt automation for "Motion-activated lights". I tried to use this at first.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fhomeassistant-motion-activated-lights-automation" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fhomeassistant-motion-activated-lights-automation" title="homeassistant-motion-activated-lights-automation" width="1000" height="521"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But this didn't match my use case, since I don't want to turn on the lights every time, just from the afternoon until when I go to bed. So I made mine instead. Two actually—one to turn on the lights when the sensor detects a person, and one to turn them off when the detection status clears. I set them up to only work from 1 pm to 12 am.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fhomeassistant-turn-on-lights-automation" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fhomeassistant-turn-on-lights-automation" title="homeassistant-turn-on-lights-automation" width="1000" height="530"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fhomeassistant-turn-off-lights-automation" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fhomeassistant-turn-off-lights-automation" title="homeassistant-turn-off-lights-automation" width="1000" height="513"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You could also do this with only one automation, but I found it easier to wrap my head around this.&lt;/p&gt;

&lt;p&gt;And voila! It worked. But there's more...&lt;/p&gt;

&lt;h2&gt;
  
  
  Power issues and more tweaking
&lt;/h2&gt;

&lt;p&gt;The next thing I tried to do was make this mobile. Powering microcontroller boards is always a challenge. Thus far, I had simply plugged it into an old USB charger, which delivered the 5V needed. But I wanted to try a different location for the sensor, so I wanted to see if I could power it off battery.&lt;/p&gt;

&lt;p&gt;I connected a 9V battery to the GND and 5V inputs (the DevKits come with a voltage regulator that ensures the board only gets 5V), and this worked...for about 2 days, and then the battery was empty! That was unsettling. I thought microcontrollers were supposed to be energy-efficient? Well, it was time for me to do some more tuning and learning.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fesp32-setup-with-voltage-divider-and-led-battery.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fesp32-setup-with-voltage-divider-and-led-battery.jpg" title="esp32-setup-with-voltage-divider-and-led-battery.jpg" width="800" height="601"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;First, I got rid of the LED. It was only meant to be a temporary aid during development; no need to use that extra power.&lt;/p&gt;

&lt;p&gt;I also ended up getting rid of the voltage divider setup and going back to the original circuit, where pin 2 was connected to ground, and on-time was the default 2s. To implement the on-time in software, I added a delay in ESPHome.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;binary_sensor&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;gpio&lt;/span&gt;
    &lt;span class="na"&gt;pin&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;GPIO19&lt;/span&gt;
    &lt;span class="na"&gt;device_class&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;occupancy&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bedroom&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Presence"&lt;/span&gt;
    &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;delayed_off&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;45s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I don't know if this saves a lot of battery, but I'm much happier with the software version of on-time. It makes the hardware connection simpler, it's easier to maintain and adjust, and it's much more flexible. (Alternatively, I could add this delay into the Home Assistant automation, but I preferred to have it as part of the device, so the state of the sensor always matched the state of the lights).&lt;/p&gt;

&lt;p&gt;Still, I learnt (thanks to ChatGPT and forums) that the common 9V batteries are pretty bad for powering microcontrollers. Despite providing a large voltage, they have low capacity (~500–600 mAh), worse than a typical 1.5V AA battery. Additionally, some energy is wasted in the voltage regulator.&lt;/p&gt;

&lt;p&gt;And that's when I learnt about &lt;a href="https://docs.espressif.com/projects/esp-idf/en/stable/esp32/api-reference/system/sleep_modes.html" rel="noopener noreferrer"&gt;sleep mode&lt;/a&gt;. ESP32 can go into sleep and be woken up after a certain time, a sensor, or a touch. ESPHome supports this via the &lt;a href="https://esphome.io/components/deep_sleep.html" rel="noopener noreferrer"&gt;Deep Sleep component&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I only needed the sensor to be active from 1 pm to 1 am, so there was no point keeping it awake outside of that time. I experimented with things, and ended up adding a config like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;deep_sleep&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deep_sleep_1&lt;/span&gt;
  &lt;span class="na"&gt;sleep_duration&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;12h&lt;/span&gt;

&lt;span class="na"&gt;time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sntp&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sntp_time_1&lt;/span&gt;
    &lt;span class="na"&gt;timezone&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Europe/Berlin"&lt;/span&gt;
    &lt;span class="na"&gt;on_time&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="c1"&gt;# At 01:01 pm every day, block deep sleep&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
        &lt;span class="na"&gt;minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
        &lt;span class="na"&gt;hours&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;13&lt;/span&gt;
        &lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;deep_sleep.prevent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deep_sleep_1&lt;/span&gt;
      &lt;span class="c1"&gt;# At 01:01 am every day, enter deep sleep&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
        &lt;span class="na"&gt;minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
        &lt;span class="na"&gt;hours&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;
        &lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;deep_sleep.allow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deep_sleep_1&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;deep_sleep.enter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deep_sleep_1&lt;/span&gt;
              &lt;span class="na"&gt;until&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;13:00:00"&lt;/span&gt;
              &lt;span class="na"&gt;time_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sntp_time_1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are three parts here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;code&gt;deep_sleep&lt;/code&gt; configuration to sleep for 12 hours, called &lt;code&gt;deep_sleep_1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;An &lt;a href="https://esphome.io/components/time/index.html#on-time-trigger" rel="noopener noreferrer"&gt;&lt;code&gt;on_time&lt;/code&gt; trigger&lt;/a&gt; that enters &lt;code&gt;deep_sleep_1&lt;/code&gt; at 1 am every day (I gave a buffer of +1 minute).&lt;/li&gt;
&lt;li&gt;An &lt;a href="https://esphome.io/components/time/index.html#on-time-trigger" rel="noopener noreferrer"&gt;&lt;code&gt;on_time&lt;/code&gt; trigger&lt;/a&gt; that blocks deep sleep at 1 pm every day.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;As long as my controller remains on, it will switch in and out of deep sleep at these times. But since sometimes I may unplug or disconnect my device to work on it, it might not be on when the time trigger comes around. So I also added an &lt;a href="https://esphome.io/components/esphome.html#on-boot" rel="noopener noreferrer"&gt;&lt;code&gt;on_boot&lt;/code&gt;&lt;/a&gt; trigger to check the time whenever the ESP32 is turned on, and enter deep sleep if needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;esphome&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="na"&gt;on_boot&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;delay&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1min&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="na"&gt;and&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;time.has_time&lt;/span&gt;
              &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;lambda&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;return&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;id(sntp_time_1).now().hour&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;gt;=&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;13;"&lt;/span&gt;
          &lt;span class="na"&gt;then&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;logger.log&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;It&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;past&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1pm.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Sensor&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;activated."&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;deep_sleep.prevent&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deep_sleep_1&lt;/span&gt;
          &lt;span class="na"&gt;else&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;logger.log&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;It&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;is&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;not&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;yet&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1pm.&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Entering&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;deep&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;sleep."&lt;/span&gt;  
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;deep_sleep.allow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deep_sleep_1&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;deep_sleep.enter&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
                &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deep_sleep_1&lt;/span&gt;
                &lt;span class="na"&gt;until&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;13:00:00"&lt;/span&gt;
                &lt;span class="na"&gt;time_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sntp_time_1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;deep_sleep.prevent&lt;/code&gt; action (which blocks deep sleep) isn't really necessary in both triggers, but I kept it in there as a sort of failsafe.&lt;/p&gt;

&lt;p&gt;As with most things here, there are multiple ways to achieve this configuration. This approach looks a little messy, but I found it relatively easy to reason about for me.&lt;/p&gt;

&lt;p&gt;So now, when the time rolls around, or I plug in my ESP32 before 1 pm, I see logs like these:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fdeep-sleep-logs-on-boot" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fdeep-sleep-logs-on-boot" title="deep-sleep-logs-on-boot" width="1000" height="239"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It enters deep sleep and we can see that it has an "expected disconnect" from the ESPHome API. During this time, the device shows as "Unavailable" or "Unknown" in Home Assistant (although sometimes it just sticks on the last known state). And then when the wakeup time rolls around, it is able to reconnect:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fdeep-sleep-wakeup-logs" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fdeep-sleep-wakeup-logs" title="deep-sleep-wakeup-logs" width="1000" height="111"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Deep sleep is pretty cool. I'm currently testing with a new battery to see how long it lasts in this setup. I may eventually switch back to USB power, though, since the sensor won't be moving around, so battery power is less necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fesp32-setup-with-battery.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fesp32-setup-with-battery.jpg" title="esp32-setup-with-battery.jpg" width="752" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Finishing
&lt;/h3&gt;

&lt;p&gt;At the end, the product still looks like a work in progress. It's on a breadboard, which is used for prototyping, and there are jumper wires everywhere. To make this a truly polished product, I could:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;move this from a breadboard to a custom PCB (printed circuit board)&lt;/li&gt;
&lt;li&gt;replace the DevKit with a regular ESP32, which is more energy-efficient&lt;/li&gt;
&lt;li&gt;make a custom case and mount for the whole thing (good use case for 3D printing)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;However, these are secondary to my current goal, and I don't have the time to dedicate to them, so I'll move on for now.&lt;/p&gt;

&lt;h3&gt;
  
  
  Debugging and observability
&lt;/h3&gt;

&lt;p&gt;A tricky thing about working with microcontrollers is debugging. In the initial development phase (when it's plugged in to your laptop and you're coding), it's easy, since you can log things to your console. But when it's out on its own, running in the wild, it's like a black box. The device might randomly stop working, and you have no idea why. Microcontollers are very tiny devices so they don't store logs, which means you'll never know what exactly happened in the past. You can only connect to your laptop and try to reproduce it.&lt;/p&gt;

&lt;p&gt;Using a LED as an indicator to show the current state is a tiny help. A thing I want to try in future is having the ESP32 wirelessly send logs to another device, then having that device store the logs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Buying parts
&lt;/h3&gt;

&lt;p&gt;A surprising challenge here was figuring out the best place to get parts (cost and speed).&lt;/p&gt;

&lt;p&gt;I started with &lt;a href="http://conrad.de" rel="noopener noreferrer"&gt;Conrad&lt;/a&gt; (also because they have a store near me), but their selection is a bit limited and prices are higher (for example, an ESP32 DevKit costs €22). This is similar to what you get if you shop on Amazon.&lt;/p&gt;

&lt;p&gt;Next up were European/German retailers like &lt;a href="http://mouser.de" rel="noopener noreferrer"&gt;mouser.de&lt;/a&gt;, &lt;a href="https://www.digikey.de/en" rel="noopener noreferrer"&gt;digikey.de&lt;/a&gt;, &lt;a href="https://www.distrelec.de/en/" rel="noopener noreferrer"&gt;Distrelec&lt;/a&gt;, and &lt;a href="https://de.rs-online.com/web/" rel="noopener noreferrer"&gt;Reichelt&lt;/a&gt;. These were super cheap (ESP32 DevKit for €9.31, and PIR sensor for €1.85), but had shipping times of around 5 days.&lt;/p&gt;

&lt;p&gt;It turns out that most of these electronics parts come either from China or the US (and that's also probably from China). I considered going directly to AliExpress for maximum cost savings, but I find the site much harder to navigate, and the shipping would take even longer.&lt;/p&gt;

&lt;p&gt;I ended up using Mouser. Shipping took around 4 days, but it took me 2 weeks to find time for the project again. That's the annoying thing about hardware development—when you don't have a part, you're forced to pause, and by the time the part arrives, you've switched contexts. This tweet is so accurate:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;i’m just sitting here with like 6 stalled hardware projects waiting for physical components to arrive. yall live like this?&lt;/p&gt;

&lt;p&gt;— yung perf papi (&lt;a class="mentioned-user" href="https://dev.to/ken_wheeler"&gt;@ken_wheeler&lt;/a&gt;) &lt;a href="https://twitter.com/ken_wheeler/status/1909736892544798887?ref_src=twsrc%5Etfw" rel="noopener noreferrer"&gt;April 8, 2025&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;### Credits&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://x.com/biodunch" rel="noopener noreferrer"&gt;Biodun&lt;/a&gt; for helping me understand some of my initial requirements and being a sounding board.&lt;/p&gt;

</description>
      <category>hardware</category>
      <category>smarthome</category>
    </item>
    <item>
      <title>In defence of complexity</title>
      <dc:creator>Shalvah</dc:creator>
      <pubDate>Tue, 24 Dec 2024 11:05:18 +0000</pubDate>
      <link>https://dev.to/shalvah/in-defence-of-complexity-32ef</link>
      <guid>https://dev.to/shalvah/in-defence-of-complexity-32ef</guid>
      <description>&lt;p&gt;I noticed a pattern when I join a new company. I'm being onboarded, someone is explaining their systems to me, and they make a joke about how it's unnecessarily complex and probably could be simplified. We laugh, but I often think to myself, "Maybe, maybe not".&lt;/p&gt;

&lt;p&gt;I'm a strong advocate for simplicity in software design, but in this article I want to try to explain, with some personal examples, why we should have a different attitude to complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Minimum Necessary Complexity
&lt;/h2&gt;

&lt;p&gt;Every engineering problem has a &lt;em&gt;minimum necessary complexity&lt;/em&gt; associated with any solution.&lt;/p&gt;

&lt;p&gt;Yes, sometimes, they are over-engineered, and the level of complexity is higher than it needs to be, but this is not something easily determinable. Even the simplest of solutions to any non-trivial problem will need to overcome a certain amount of complexity. As software architects, our job is to determine how much complexity is appropriate and how we wish to distribute it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distributing complexity
&lt;/h2&gt;

&lt;p&gt;Take an example of a text-based note-taking app such as Google Keep. Compared to richer organizers such as OneNote, Keep has always been more basic, which makes it appealing for simple cases—the equivalent of a set of digital sticky notes. In the early days, Keep only supported text. If you wanted to add an image, you would have to upload it somewhere else and then insert the URL.&lt;/p&gt;

&lt;p&gt;As the developer, you might be fine with this. It's a win-win: your software remains simple to use, while users can still put in whatever they want, as long as they can convert it to text. But supposing you want to provide a better experience for your users and so you add image support. This will undoubtedly raise the complexity of your software. Now you need to think about file storage, backwards compatibility (keeping all notes text-based internally), presentation (how should images appear), interface (UI for uploading images), limits, abuse, and so on.&lt;/p&gt;

&lt;p&gt;Another example is Twitter's "Tweet all" feature (writing a thread at once). Users can post one tweet and then reply to it, and so on. But Twitter added the ability to post multiple tweets that would then be linked as one thread. Again, we're trading some simplicity in our system for a better UX.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Ftwitter-tweet-all.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Ftwitter-tweet-all.png" title="twitter-tweet-all.png" width="740" height="421"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Of course, the amount of complexity that will be involved in both cases will vary dependeing on factors like your tech stack and architecture, as well as your skills. But there will be some.&lt;/p&gt;

&lt;h2&gt;
  
  
  Taking complexity on: Simple does not mean "not complex"
&lt;/h2&gt;

&lt;p&gt;I call this the act of taking on complexity. We don't &lt;em&gt;have to&lt;/em&gt;, but we want to reduce friction for our end users and make things "simple" to them. So we step in and shift some of this complexity to our end.&lt;/p&gt;

&lt;p&gt;So, yes, something that appears simple to the user may in fact belie a lot of complexity. Adding a new feature, supporting alternatives, adding redundancy, caching to improve performance and so on.&lt;/p&gt;

&lt;p&gt;There are also non-product reasons to add complexity, chief among them probably being security. Rate limits, authorization, RBAC, MFA, and the like all add complexity.&lt;/p&gt;

&lt;p&gt;Complexity happens in various forms. It could be:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;combinatorial (if you have 6 possible toggles, this is actually 2⁶ possible scenarios)&lt;/li&gt;
&lt;li&gt;infrastructure&lt;/li&gt;
&lt;li&gt;performance&lt;/li&gt;
&lt;li&gt;system architecture&lt;/li&gt;
&lt;li&gt;code architecture&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can't build good (intuitive, reliable, secure, insert-adjective-here) software without these two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the &lt;strong&gt;willingness&lt;/strong&gt; to take on complexity&lt;/li&gt;
&lt;li&gt;the &lt;strong&gt;discernment&lt;/strong&gt; to know what complexity should be taken on.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Modern civilization runs on complex systems. Water and power delivered to your house, plumbing, telecommunications, Internet, traffic lights, airports, manufacturing of cans and bottles—these are examples of complex systems we've come to take for granted, because of the simplicity of their outputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complexity's problems
&lt;/h2&gt;

&lt;p&gt;But yes, complexity does have problems...&lt;/p&gt;

&lt;h3&gt;
  
  
  Cost
&lt;/h3&gt;

&lt;p&gt;For one, it's costly, especially when it's intentional and well-designed. It takes time, effort and the right skills. This is why there's another option: offload it to an external provider. This lets you achieve the final goal, such as a better UX, without the full investment. The cost is still borne by you (monetarily), but your core system can remain "simple".&lt;/p&gt;

&lt;p&gt;Offloading complexity also works as a starting point, similar to starting with a cloud provider before moving to self-hosted. Especially when you don't have the needed skills, using an external provider gives you a chance to get progressively familiar with the challenges of such systems, until you get to a point where you can finally take it on.&lt;/p&gt;

&lt;h3&gt;
  
  
  Growth
&lt;/h3&gt;

&lt;p&gt;A second problem is that complexity grows. Complex systems beget complex systems. Once you've accepted the complexity, problems that could be previously "simple" may now take additional work. For example, improving performance in a monolith would be fairly straightforward; with microservices, you pay the price for the extra network calls, which means taking more extreme measures like aggressive caching, switching network protocols and serialization formats, and the like.&lt;/p&gt;

&lt;p&gt;This growth is often by accident. You could start out with an architecture that makes sense for current &lt;em&gt;and&lt;/em&gt; future use cases, but then the product evolves in unforeseen ways, trapping you in an unplanned mess. Unfortunately, this happens; you can't always avoid this.&lt;/p&gt;

&lt;h3&gt;
  
  
  Accidental complexity
&lt;/h3&gt;

&lt;p&gt;And this leads us to our main enemy. Accidental complexity happens when a process is complex in a way we didn't expect. This usually indicates a mismatch between our mental model of how complex the process should be versus the requirements forced on us.&lt;/p&gt;

&lt;p&gt;A common example today is deployment of web apps, especially with tools like Kubernetes. Sometimes people adopt Kubernetes because they've heard about the reliability features, and then they get frustrated by all of the extra work that it imposes on them. The complexity may be necessary, but only because Kubernetes is engineered towards a specific class of problems. And to this user, who doesn't care about those problems, all of this feels &lt;em&gt;accidental&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Avoiding accidental complexity is hard, especially when it's not created by us. Sometimes the complexity is necessary, but we're just poorly informed. The best option is probably to get properly informed, weigh it up and decide if it is worth it for us. If it is, can we convert it into intentional complexity, via proper planning, documentation, and changing our approach? If not, it probably makes sense to migrate to a different solution.&lt;/p&gt;

&lt;h2&gt;
  
  
  A personal story
&lt;/h2&gt;

&lt;p&gt;An example of complexity that sticks with me is a project from a previous job. We had an appointment booking system in our app, which was simply an embedded Calendly widget. We did this because we didn't want to deal with the vagaries of scheduling. Calendly was good at this stuff—timezones, round-robin scheduling, managing availability, group scheduling, etc. Unfortunately, they require you to use their UI for scheduling, hence the widget.&lt;/p&gt;

&lt;p&gt;But even with this, we still had a lot of complexity to deal with.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We had to sync appointments from Calendly into our system, but our models did not match 1:1&lt;/li&gt;
&lt;li&gt;We had to do this sync via listening to their webhooks. Let's just say: there's a reason people are trying to build startups out of webhook processing.&lt;/li&gt;
&lt;li&gt;Their API was limited in some annoying ways, so things that should actually be simple weren't.&lt;/li&gt;
&lt;li&gt;Worse, we had a second external service where agents could set up appointments on their own. This provider had a different model from Calendly and an even more frustrating API.&lt;/li&gt;
&lt;li&gt;Even worse, an appointment booked via Calendly could also show up in this other service and be managed from there. (PS: "Single source of truth" is a lie.)&lt;/li&gt;
&lt;li&gt;The icing on the cake was that the process was driven by humans, so you couldn't rely on them not doing stupid things. They would often do random things that would cause undefined behaviour, such as changing an item mid-processing (with no change history).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fsingle%2520source%2520of%2520truth" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fsingle%2520source%2520of%2520truth" title="single source of truth" width="888" height="499"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Complex? As fuck.&lt;/p&gt;

&lt;p&gt;But was this really necessary? Mostly.&lt;/p&gt;

&lt;p&gt;Primarily because this was a process that existed before we began incorporating it into our backend, so we couldn't simply rip it up and build it from scratch in an ideal manner. Also, we didn't have the resources (people or time) to work on a whole appointment scheduling + CRM system.&lt;/p&gt;

&lt;p&gt;As the lead developer on this system, I spent days just thinking about the complexity of it all, looking for ways we could reduce it. I rewrote a bunch of the code several times. Wrote tons of comments and documents describing how the system worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  More problems
&lt;/h2&gt;

&lt;p&gt;A second example: a problem caused by the complexity of this system. I mentioned that we used the Calendly scheduling widget in our app. The flow was like this: user is on appointments page (our app) → user clicks "New appointment" → we redirect to Calendly widget → they finish booking → Calendly page closes, they are back in our app and they should see the newly booked appointment.&lt;/p&gt;

&lt;p&gt;The problem? We relied on webhooks for notifying us of new appointments, so if the Calendly webhook had not arrived or been processed when the user was redirected back to our app, they would still see an empty list of appointments, creating confusion.&lt;/p&gt;

&lt;p&gt;To solve this, we added redundancy: whenever the user landed on that page, we would call the Calendly API proactively to check if there were any appointments we hadn't yet processed via webhook, and thus process them preemptively, while of course avoiding race conditions (processing the same appointment twice). This worked reliably; the only downside was the latency introduced by that API call.&lt;/p&gt;

&lt;p&gt;A third example: an evolution of the system. Users could reschedule an appointment in our app, which would use our UI and Calendly's API. However, we wanted to change the flow in certain cases. Rescheduling in Calendly keeps the same agent, but we wanted to return the user back to the pool of available agents.&lt;/p&gt;

&lt;p&gt;The solution here was straightforward: instead of rescheduling via Calendly, we would cancel and and create a new appointment. But in order to do this seamlessly for the user, we had to come up with a trick: whenever the user clicked our "Reschedule" link, we would create a record on our backend called a "PendingCancellation", attached to the existing appointment. We would then redirect the user to the Calendly "new appointment" page. From here, we would process new appointments as normal (Calendly webhook), but also check for any PendingCancellation recently created by the user, and go ahead and cancel the old appointment. I wrote mote about this solution &lt;a href="https://blog.shalvah.me/posts/making-non-atomic-actions-atomic-using-intents" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We didn't &lt;em&gt;have&lt;/em&gt; to do this; we could have pushed the complexity on to the user by asking them to first cancel the previous one. But that didn't align with our business goals, so we took it on ourselves.&lt;/p&gt;

&lt;p&gt;You can probably see a pattern: we took on complexity initially, and this led to more complex problems we had to solve with more complex solutions. But most of this was needed, and was ultimately managed quite well. Some of this complexity was accidental (due to Calendly's API making things harder). But since we had already bought in to creating a complex system at the start, we could take a step back and come up with reliable ways for handling these. It was ultimately quite fun (when I wasn't tearing my hair out)!&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing complexity
&lt;/h2&gt;

&lt;p&gt;So the panacea is not necessarily rejecting complexity outright, but knowing how to manage it. Some things I've learnt that help:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Discernment&lt;/strong&gt;: It's super important to know what complexity is worth taking on, and where to distribute it in your stack. Some kinds of complexity, such as combinatorial, are best avoided as much as possible. Take the time to assess the situation and decide whether it's worth it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Documentation&lt;/strong&gt;: Be sure to document things. Describe:

&lt;ul&gt;
&lt;li&gt;the desired results&lt;/li&gt;
&lt;li&gt;the path we're taking to achieve them&lt;/li&gt;
&lt;li&gt;why
Everything from comments in code to diagrams and external docs helps here.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Standardization&lt;/strong&gt;: It helps if you can stick to common conventions. This means you only need to document the places where you diverge.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Observability&lt;/strong&gt;: Complex systems can and will fail in surprising ways. A system is only as good as the confidence you have in it. You must invest in your ability to understand your systems (here's &lt;a href="https://drive.google.com/file/d/1-NjkbAXrbjaNLZU6UygzNIrUmZgQ5K_F/view" rel="noopener noreferrer"&gt;my free book&lt;/a&gt; on Observability Basics). This means architecting the system at a low and high level in such a way that changes can be understood and problems debugged easily. This entails:

&lt;ul&gt;
&lt;li&gt;thinking about &lt;a href="https://asq.org/quality-resources/fmea" rel="noopener noreferrer"&gt;failure modes&lt;/a&gt; and consequences&lt;/li&gt;
&lt;li&gt;making the system verbose via logs, metrics and traces&lt;/li&gt;
&lt;li&gt;collecting metrics to track certain paths&lt;/li&gt;
&lt;li&gt;building features like history and changelogs to track changes&lt;/li&gt;
&lt;li&gt;adding the ability to observe and test behaviour in production (eg via feature flags or parallel experiments)&lt;/li&gt;
&lt;li&gt;adding the ability to simulate specific scenarios
This isn't easy, and also adds complexity. But they will increase your confidence in the system. For example, in my Calendly story, we often didn't even have IDs we could rely on for looking up models, but had to use heuristics like "how long ago was this scheduled?" For cases like this, I used metrics, logs and occasional spot checks to compare the results of the heuristics with what was expected.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Iteration&lt;/strong&gt;: Don't be afraid to rewrite things. This architecture may have made sense last year, but we've grown and now it's hard to keep up. There's never a perfect setup.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;"Git gud"&lt;/strong&gt;: Finally, you have to level up in your stack. Being knowledgeable in your tools will give you more options and save you from building things afresh. Additionally, not all implementations will be readable or easy to understand; building up your expertise will make them more approachable.&lt;/li&gt;

&lt;/ul&gt;

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

&lt;p&gt;Always question complexity. But don't assume it's always unnecessary. And when you need to take it on, be equipped for how to manage it.&lt;/p&gt;

</description>
      <category>engineeringtechniques</category>
      <category>engineeringconcepts</category>
    </item>
    <item>
      <title>Reflections on Year 1 of my engineering studies</title>
      <dc:creator>Shalvah</dc:creator>
      <pubDate>Fri, 18 Oct 2024 00:11:18 +0000</pubDate>
      <link>https://dev.to/shalvah/reflections-on-year-1-of-my-engineering-studies-2lg8</link>
      <guid>https://dev.to/shalvah/reflections-on-year-1-of-my-engineering-studies-2lg8</guid>
      <description>&lt;p&gt;I've finally completed the first year (of three) of my Bachelor's degree in Engineering at IU Hochschule. Technically speaking, it's my second first year, since I quit school some years ago.&lt;/p&gt;

&lt;p&gt;Some background:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's a general engineering degree. There are electives for various specializations in the final year, but to the best of my knowledge, the degree you get is a B.Eng.&lt;/li&gt;
&lt;li&gt;It's a three-year program full-time, but flexible if you want to do part-time. You can spread it out over 4 or 6 years, or even longer. It took me just under a year to complete the first full-time year, but I expect the next two years will go slower, as there's more content, and most of it is new to me.&lt;/li&gt;
&lt;li&gt;I chose to study Engineering, not Computer Science, because I already work as a software professional. CS would fill some gaps in my knowledge, but mostly in a theoretical way. There'll be some learnings, but I don't think it's worth a multiyear program. Plus, engineering still fascinates me.&lt;/li&gt;
&lt;li&gt;It's entirely online, including exams. This has pros and cons, some of which I'll discuss.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  General thoughts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;School still sucks&lt;/strong&gt;. Like many other things in life, it's a broken system. Curriculum. Course materials. Lecturers. Exams. The system will favour some people and not others. It rewards conformity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;School is awesome&lt;/strong&gt;. Formal education is a great concept. My entire programming journey has been informal (I'd say "self-taught", but we all learnt from the works of others). It's great to have a curriculum, that structures things and tells you where to go next. It relieves you of a huge burden.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Things I've (re-)learnt:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Focus on the journey&lt;/strong&gt;. This came after working hard on some courses but ultimately having a less-than-stellar score on the exam. I have the privilege of not needing this to get a job. It means I can afford not to worry too much about grades. Instead, I want to optimize for learning as much as I can, even if my grades suffer.
It &lt;em&gt;seems&lt;/em&gt; obvious, but it's hard for me as a completionist and a perfectionist. This has meant:

&lt;ul&gt;
&lt;li&gt;intentionally skipping some sections of a textbook because I saw no use in them except for passing an exam&lt;/li&gt;
&lt;li&gt;not being stressed out about memorising random things like dates, which have no bearing on my actual skill as an engineer&lt;/li&gt;
&lt;li&gt;finding extra materials and challenges that go well beyond the course content, even when they prolong the length of the course&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Learning takes time&lt;/strong&gt;. In the first several months, my priority was "progression": finishing one course ASAP and moving to the next. But I ultimately realized that learning takes time, and there's no way to short-circuit it. Sometimes, just doing nothing after studying helps you learn better than moving directly to the next thing.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Progress is not linear&lt;/strong&gt;. Another thing that's deeply personal to me, who gets easily frustrated if there are no signs of progress. I have to accept that I will forget things, I won't understand everything, I will need to go look up old courses, I will jump back and forth, and that's okay.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Continuous tweaking&lt;/strong&gt;. At first, I was worried I didn't know how to study since I get easily distracted. Over time I've found that I can do whatever works, rather than trying to find the optimal process. I've tried different things and made a note of what works in what situation. And I feel free to adjust it if it isn't working. Some things I've tried:

&lt;ul&gt;
&lt;li&gt;studying seated/standing/lying down&lt;/li&gt;
&lt;li&gt;studying outside/in a restaurant/in a park/at a bus stop/on a train&lt;/li&gt;
&lt;li&gt;studying with no music/chill music/fun music (this one does NOT work)&lt;/li&gt;
&lt;li&gt;studying in bursts/for long periods&lt;/li&gt;
&lt;li&gt;studying via a printed textbook/phone/tablet/desktop&lt;/li&gt;
&lt;li&gt;writing notes by hand/typing&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;Others can help&lt;/strong&gt;: I'm super independent and love "figuring it out", so it took effort to learn to ask for help. But it's worth it. I've gotten help and learnt useful techniques from my classmates. It's always illuminating to see how others approach things.&lt;/li&gt;

&lt;li&gt;

&lt;strong&gt;I can help others&lt;/strong&gt;. In the same vein, I've tried to help my classmates when I can, explaining concepts, giving directions, or providing tips. I even became a de facto class representative when we needed to complain to the school administration.&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Things I've enjoyed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Learning&lt;/strong&gt;. Learning is so cool. Especially when it's getting to know something you didn't before that explains how some other thing you were aware of works. Human beings have made so many awesome things.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connecting dots&lt;/strong&gt;. Even better, I've enjoyed seeing my knowledge compound. One delightful experience for me was visiting a technology museum while studying a course on Automation Technology, which got me through a rut I was stuck in. And every time I find myself able to understand a course by building on things I learnt from another course feels like a lightbulb going off in my head. These moments make me feel more confident in the curriculum.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspiration and experimentation&lt;/strong&gt;. I love how really studying a course sometimes inspires me to build or try something. For instance, my Linear Algebra course inspired me to make the vector graphing library I used in &lt;a href="https://blog.shalvah.me/posts/an-exploration-of-vector-search" rel="noopener noreferrer"&gt;this post&lt;/a&gt; and &lt;a href="https://blog.shalvah.me/posts/learn-svg-by-drawing-an-arrow" rel="noopener noreferrer"&gt;this post&lt;/a&gt;. My IoT course showed me some of the real benefits of smart devices, and I have a bunch of ideas written down to try sometime. My Production Engineering course has me itching to try 3D printing.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Things I wish were better
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Classroom environment&lt;/strong&gt;. I still wish I could be in a classroom (I get this urge a lot when watching lecture videos from MIT's OpenCourseWare). I miss having more free time and being surrounded by smart, motivated folks with similar goals.
But, truth be told: I'm romanticizing it a bit. I'd bet half of my class back in my first program didn't know what the hell they were doing. Plus, most of us didn't maximize the opportunity. We were young and foolish. Oh, and being in a classroom sounds cool now, but actually having to be in one for several hours a day is not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Labs&lt;/strong&gt;. The program is online-only, but obviously you can't become an engineer by reading alone. There has to be some practice. Thus far, everything we've done has been theoretical, but I think there's a provision for "simulations" in Year 2 or 3. Still, I think the course materials have done a poor job of encouraging a hands-on approach. They've seemed like "theory dumps", with the occasional picture or text case study. I would like if they included suggested practical exercises at least. I've had to give myself such exercises, but I just do it for things that pique my curiosity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exams&lt;/strong&gt;. The program unfortunately has only one final exam, which can range in difficulty from "lol, that was too easy" to "what the fuck." It's an unfair system (although you can retry if you fail). I picked up a habit from a classmate of making up midterm exams for myself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The curriculum&lt;/strong&gt;. There's been some useless stuff, both for me personally and for the overall program. Some courses are mandatory for everyone but focus on niche stuff that people in those industries will be specially trained in. Some courses are merely "memorize-and-dump". I'm mostly lucky, though; my interests are in electronics, and I think our program is biased towards that.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Overall I'm happy. I think the next semester is going to be pretty tough. I'm quite excited about the upcoming Control Engineering course, but less enthused about the three Mechanics/Materials courses. For now, I hope to take things a bit slow by experimenting with some interesting learnings from Signals and Systems, and perhaps doing a boring nontechnical course. (Update: I decided to step back and work on some small projects for each of my courses, to help solidify my knowledge.)&lt;/p&gt;

&lt;p&gt;Most importantly, I'm grateful. Years ago when I quit school, I worried that my mental health would prevent me from holding a full-time job. And when I started up school again, I was scared I couldn't do it. Yet here I am, job in hand, and one year of school down. Grateful.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Exploring software design problems and solutions: Transactions and side effects</title>
      <dc:creator>Shalvah</dc:creator>
      <pubDate>Fri, 19 Jul 2024 07:30:32 +0000</pubDate>
      <link>https://dev.to/shalvah/exploring-software-design-problems-and-solutions-transactions-and-side-effects-13ig</link>
      <guid>https://dev.to/shalvah/exploring-software-design-problems-and-solutions-transactions-and-side-effects-13ig</guid>
      <description>&lt;p&gt;As practice in thinking and talking about practical software design approaches (at the high and low level), I want to explore some scenarios from first principles. The goal of this exercise is to analyse problems, the solutions we apply to them, and the new problems those bring up. I encourage you to pause the article at certain points and think through it yourself. Code examples are Ruby/Rails-ish, but should be easy enough to follow.&lt;/p&gt;

&lt;p&gt;Importantly, these thoughts are subjective, as a lot of design is. Feel free to question my reasoning, and leave a comment to let me know what you think.&lt;/p&gt;




&lt;p&gt;We start with a simple system for a shopping website: you have an endpoint where you want to create an account and a cart for a customer at the same time.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-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;call&lt;/span&gt;
  &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
  &lt;span class="no"&gt;CartItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_many!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Add items to the cart&lt;/span&gt;
  &lt;span class="c1"&gt;# Maybe also create some other things ...&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a problem here, which is that if the cart item creation fails, you end up with inconsistent state. The endpoint will return an error, and the account exists, and maybe the cart does, but it's empty.&lt;/p&gt;

&lt;p&gt;We can solve this by having the caller retry on failure. But we need to change &lt;code&gt;Account.create!&lt;/code&gt; to &lt;code&gt;Account.find_or_create!&lt;/code&gt; to prevent duplicates, and so on. What problems does this bring?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Creation logic gets more confusing and messy, as now another non-trivial yet orthogonal concern (idempotency) has been mixed in.&lt;/li&gt;
&lt;li&gt;All future changes to this endpoint need to know about and carry on with the &lt;code&gt;find_or_create!&lt;/code&gt; pattern, or risk incorrectness. This gets more difficult to enforce if we extract parts of the endpoint into other methods or classes.&lt;/li&gt;
&lt;li&gt;If the caller does not retry, you're left with orphaned entities (such as the Account which will never be used).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We know the right answer here: &lt;em&gt;use database transactions!&lt;/em&gt; If one step fails, the transaction is rolled back, so nothing is persisted. But before we jump to that, I want to explore &lt;em&gt;why&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;You don't have to use a database transaction; you could also write your custom rollback implementation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-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;call&lt;/span&gt;
  &lt;span class="vi"&gt;@account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="vi"&gt;@cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="vi"&gt;@cart_items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CartItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_many!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@cart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;rescue&lt;/span&gt;
  &lt;span class="vi"&gt;@cart_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete_all&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@cart_items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;present?&lt;/span&gt;
  &lt;span class="vi"&gt;@cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@cart&lt;/span&gt;
  &lt;span class="vi"&gt;@account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@account&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'm not judging. There &lt;em&gt;may&lt;/em&gt; be valid reasons for this. You may want to avoid nested, sequential, or long-running transactions, or you just work in a weird place where they shun database transactions.&lt;/p&gt;

&lt;p&gt;But let's step back. Why is this frowned upon?&lt;/p&gt;

&lt;p&gt;Major reason: &lt;strong&gt;It's still susceptible to failure.&lt;/strong&gt; If there's a bug in your code or something goes wrong in your database while you're trying to delete the orphaned entities, you're screwed.&lt;/p&gt;

&lt;p&gt;Minor reason: It leaves traces. For instance, the deleted entities will still likely take up disk space until the disk is purged. Any autoincrementing sequences (like IDs) will also likely not reset.&lt;/p&gt;

&lt;p&gt;This is why we in the industry agreed to rely on database transactions for this. Instead of treating the data layer as a dumb store and trying to handle everything ourselves, we rely on it to help us ensure correctness, which allows us to leverage its &lt;a href="https://en.wikipedia.org/wiki/ACID" rel="noopener noreferrer"&gt;ACID guarantees&lt;/a&gt;. This means that when we open a transaction, the database can promise us that all the operations in that transaction will be considered one atomic operation. They will succeed together or fail together. Database transactions also help us mitigate concurrency problems.&lt;/p&gt;

&lt;p&gt;We've made progress. Databases which have ACID guarantees (typically SQL databases) are really good at this. We should lean on them.&lt;/p&gt;

&lt;p&gt;So now we have:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-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;call&lt;/span&gt;
  &lt;span class="n"&gt;in_transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
    &lt;span class="no"&gt;CartItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_many!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&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;But things can get murky. Suppose we want to send an email to the user to verify their email after we create the account. Let's say we do this by enqueueing an async job which is pushed to Redis or a separate database and then processed by Sidekiq.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-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;call&lt;/span&gt;
  &lt;span class="n"&gt;in_transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;SendVerificationEmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# or&lt;/span&gt;
    &lt;span class="c1"&gt;# SendVerificationEmail.perform_later(account.email)&lt;/span&gt;
    &lt;span class="n"&gt;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
    &lt;span class="no"&gt;CartItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_many!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&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;We have a problem again. If the account creation succeeds, but the cart creation does not, the whole transaction will be rolled back. But the &lt;code&gt;SendVerificationEmail&lt;/code&gt; job will have already been enqueued! It will either fail (no account found), or succeed (if we passed the account email directly).&lt;/p&gt;

&lt;p&gt;This is what happens when you introduce a &lt;strong&gt;side effect&lt;/strong&gt;. I see side effects as a change in a different system outside the control of the current executor (the database). Here, it is an async job, but it could also be, for instance, a synchronous API call to a payment provider to charge a user's card. How do we solve this?&lt;/p&gt;

&lt;p&gt;Let's consider some options. The simplest fix is moving the side effect outside of the transaction.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-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;call&lt;/span&gt;
  &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;in_transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cart&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Cart&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
    &lt;span class="no"&gt;CartItems&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_many!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="c1"&gt;# Return account&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="c1"&gt;# If the transaction was rolled back, the method would exit.&lt;/span&gt;
  &lt;span class="c1"&gt;# This means that, at this point, account definitely exists.&lt;/span&gt;
  &lt;span class="no"&gt;SendVerificationEmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&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;If you can, you should do this. But this isn't always possible. For instance, maybe the account creation logic is not directly in this method, but encapsulated in a service class somewhere in your application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-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;AccountCreator&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;SendVerificationEmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&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;and then used here and other places:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-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;call&lt;/span&gt;
  &lt;span class="n"&gt;in_transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;AccountCreator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# Maybe you also have a CartCreator&lt;/span&gt;
    &lt;span class="n"&gt;cart_with_items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CartCreator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&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;The &lt;code&gt;AccountCreator&lt;/code&gt; is probably used in a few other places, like the normal sign up flow. It's not so easy to extract the email sending from the transaction without affecting all these places. And this could happen for many other service classes: they innocently initiate their own external calls without knowing we're in a transaction.&lt;/p&gt;

&lt;p&gt;In some ways, this is a silly problem. This is not a case of race conditions, possible error scenarios or high-level design problems. This is purely because of &lt;em&gt;how we organize our code&lt;/em&gt;. If we choose to not use service classes, and rather inline this, it takes us back to the previous version, and we can now simply move the job to the end.&lt;/p&gt;

&lt;p&gt;But it's a real problem. Code structure matters. The design and domain model we choose at the high level influences how we structure our code, and this in turn locks us into certain paths of development.&lt;/p&gt;

&lt;p&gt;So how is this solved in the Rails' world? To solve this, Rails has a callback called &lt;a href="https://api.rubyonrails.org/v7.1.3.4/classes/ActiveRecord/Transactions/ClassMethods.html#method-i-after_commit" rel="noopener noreferrer"&gt;&lt;code&gt;after_commit&lt;/code&gt;&lt;/a&gt;. We could use it like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# It only works in models, so we must put the job enqueueing in our Account model&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Account&lt;/span&gt;
  &lt;span class="c1"&gt;# This method will be called whenever anyone does Account.create!&lt;/span&gt;
  &lt;span class="n"&gt;after_commit&lt;/span&gt; &lt;span class="ss"&gt;:send_verification_email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;on: :create&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;send_verification_email&lt;/span&gt;
    &lt;span class="no"&gt;SendVerificationEmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&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="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;With this we can solve the problem of nested service classes: move all external system calls into &lt;code&gt;after_commit&lt;/code&gt; in the model. But you can probably already spot some problems this raises:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Actions are now coupled to models rather than business processes, which means they are always invoked, even when not relevant. &lt;code&gt;send_verification_email&lt;/code&gt; will always be called, even in some cases where we may not want to (for example, backfilling accounts via a one-off script). We could add filters in the callbacks, but this increases the amount of logic they hold.&lt;/li&gt;
&lt;li&gt;Dependencies are now hidden. It's easy to add a new flow for creating accounts that calls &lt;code&gt;Account.create!&lt;/code&gt;, without realizing that it will also send emails.&lt;/li&gt;
&lt;li&gt;Logic is fragmented. Rather than having each service class handle a business process, some of the logic needs to be moved into the model. The code becomes harder to follow, and we end up with hidden side effects. And remember, code structure matters. We will pay the price.&lt;/li&gt;
&lt;li&gt;Visibility (and thus debugging) becomes harder, since we're now relying on a chain of responsibility handled by the framework, not our code, thus introducing a new layer of indirection. Stack traces will now jump from your code deep into the framework, before getting back to your code. And when multiple callbacks or multiple models are involved, it's hard to be sure of the order in which they were executed. I have personally wasted several minutes of my life hunting down the source of some external change. I had to do a mix of code reading and runtime debugging, tracing it across several boundaries, and I can tell you, &lt;em&gt;it sucks&lt;/em&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One more big problem is that &lt;code&gt;after_commit&lt;/code&gt; can have surprising behaviour when you have &lt;a href="https://makandracards.com/makandra/42885-nested-activerecord-transaction-pitfalls" rel="noopener noreferrer"&gt;nested transactions&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;A way to improve on this is with the library &lt;a href="https://github.com/Envek/after_commit_everywhere" rel="noopener noreferrer"&gt;after_commit_everywhere&lt;/a&gt;. This library makes it possible for you to use &lt;code&gt;after_commit&lt;/code&gt; outside models, so now the logic can remain in the corresponding service class.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-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;AccountCreator&lt;/span&gt;
  &lt;span class="kp"&gt;include&lt;/span&gt; &lt;span class="no"&gt;AfterCommitEverywhere&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;after_commit&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;SendVerificationEmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&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;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I much prefer this, as we can now keep related concerns together. But I still see some minor problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;after_commit_everywhere&lt;/code&gt; depends on monkey-patching, which feels like a hack.&lt;/li&gt;
&lt;li&gt;It doesn't solve the problem of indirection. It in fact makes it a bit worse, as we've now added another layer on top the framework.&lt;/li&gt;
&lt;li&gt;Is this a leaky abstraction? I don't know. Someone could argue that it is, as it means the service class needs to know/care about the existence of a database transaction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But overall, after_commit_everywhere seems like a good solution for most cases.&lt;/p&gt;

&lt;p&gt;Taking an assessment of where we are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We introduced transactions to solve the problem of incorrectness due to partial failure of a multi-step process.&lt;/li&gt;
&lt;li&gt;We introduced &lt;code&gt;after_commit&lt;/code&gt; to solve the problem of inconsistency due to nested service classes interacting with external systems during a transaction.&lt;/li&gt;
&lt;li&gt;We introduced &lt;code&gt;after_commit_everywhere&lt;/code&gt; to solve the fragmentation of regular &lt;code&gt;after_commit&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Great! Let's backtrack a bit and consider some alternative approaches to after_commit.&lt;/p&gt;

&lt;p&gt;I haven't seen this done anywhere yet, but in theory you could make each service class declare or return its side effects, and leave it to the caller to execute them. This could look something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:side_effects&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AccountCreator&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;Result&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;item: &lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
      &lt;span class="ss"&gt;side_effects: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nb"&gt;lambda&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="no"&gt;SendVerificationEmail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perform_later&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&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;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# ... In the original caller&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
  &lt;span class="vi"&gt;@side_effects&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
  &lt;span class="n"&gt;in_transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;AccountCreator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@side_effects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;concat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;side_effects&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cart_with_items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CartCreator&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="vi"&gt;@side_effects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:call&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;I like this because it separates side effects clearly. But now it relies on the caller to explicitly dispatch those side effects. Our custom &lt;code&gt;Result&lt;/code&gt; class makes it harder to miss, but it's still fairly easy to forget to write that last line. It also gets pretty unwieldy with nesting, as each nested service class will have to pass to its parent not just its result and side effects, but also those from all other service classes it invoked.&lt;/p&gt;

&lt;p&gt;Let's consider yet another alternative: If we look at the problem some more (enqueueing external jobs), we can see that it arises due to &lt;em&gt;control&lt;/em&gt;. The queue system which handles the &lt;code&gt;SendVerificationEmail&lt;/code&gt; job is outside the control of our database, so these two systems cannot synchronize. By moving the job to after the transaction, we're saying, "Since this external system (queued jobs) is outside the control of our transaction, let's wait until the database relinquishes control back to us." But what if we brought the jobs under the control of our database?&lt;/p&gt;

&lt;p&gt;If we changed our queue system to store jobs in the same database, the &lt;code&gt;SendVerificationEmail&lt;/code&gt; job would become part of the current transaction, and so it would not be visible to our queue executor until the transaction commits. And if the transaction is rolled back, so is the job. Problem solved.&lt;/p&gt;

&lt;p&gt;I think this is a fair solution for a small app, but suboptimal for larger ones. Database jobs are a different kind of workload from application records. Storing them in the same database means your job executions can impact your regular application (for example, queue workers constantly polling your database could affect app performance, jobs could contribute to database disk bloat).&lt;/p&gt;

&lt;p&gt;So let's iterate on this. How about if we put the jobs under our database's control, but only temporarily? This is what the &lt;a href="https://microservices.io/patterns/data/transactional-outbox.html" rel="noopener noreferrer"&gt;transactional outbox&lt;/a&gt; pattern essentially is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Rather than enqueuing jobs to our queue system, we store them as messages in a table in our app's database. This allows our database to retain control over these messages, and roll them back if the transaction is rolled back.&lt;/li&gt;
&lt;li&gt;We then have a separate process (for example, a cron job) which is responsible for checking for new messages regularly and enqueueing them as jobs in our queue system. The table of messages is our outbox, containing messages from our application to our queue system.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It could look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Now we write messages&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;AccountCreator&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="n"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# JobMessage is the model for our outbox table&lt;/span&gt;
    &lt;span class="no"&gt;JobMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;class: &lt;/span&gt;&lt;span class="no"&gt;SendVerificationEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;params: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;account&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&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="c1"&gt;# A cron that runs every n seconds, and enqueues the jobs&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EnqueueJobsFromMessages&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="no"&gt;JobMessage&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;message&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;message&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;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:perform_later&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&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;Transactional outbox is awesome. We are able to synchronize our queue system to the app database while still keeping them decoupled.&lt;/p&gt;

&lt;p&gt;Of course, it has its problems too!&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;We've now introduced infrastructure complexity. We've added one more component in between our application and queue workers, and every new component is a new point of failure.&lt;/li&gt;
&lt;li&gt;We'll likely have an increase in latency (probably tiny though), since now we don't process jobs immediately, but wait for the cron job to pick them up. Our throughput may also decrease since batch jobs like this creates a temporary bottleneck until they are fanned out to the workers.&lt;/li&gt;
&lt;li&gt;We also need to watch for concurrency issues in the cron job. With enough usage, we will quickly run into funny situations such as different instances of the cron job overlapping and then trying to enqueue the same messages. A more robust version of this cron job would look like:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-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;EnqueueJobsFromMessages&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="n"&gt;in_transaction&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="c1"&gt;# FOR UPDATE will lock each selected message so another cron job instance doesn't pick it up&lt;/span&gt;
      &lt;span class="no"&gt;JobMessage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"FOR UPDATE SKIP LOCKED"&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;message&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
        &lt;span class="n"&gt;message&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;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:perform_later&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;delete&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;ol&gt;
&lt;li&gt;What about side effects which aren't jobs? For example, calling an external service API. To make these work with transactional outbox, we'll need to wrap them in jobs, which also means we can no longer process them synchronously. At that point, I'd say we need to rethink our paradigm and consider whether we needed these to be synchronous in the first place. It makes me wonder about &lt;a href="https://www.inngest.com/blog/how-durable-workflow-engines-work" rel="noopener noreferrer"&gt;durable workflows&lt;/a&gt;, something I hope to explore in the future.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;And now I'll call it a day. We could still expand on these with more solutions and problems, both common and novel. For me, this exercise in deconstructing the patterns we use demonstrated again that we make solutions to common problems which introduce problems themselves. They don't exist in isolation, and for every solution we pick we must pay a price. Sometimes they increase complexity, sometimes they require a change of paradigm, sometimes they reduce visibility, but sometimes they're good enough.&lt;/p&gt;

&lt;p&gt;I don't think this is a surprise, as we all know there are tradeoffs. Still, there are patterns that we've come to universally accept/reject, and it's always interesting to dig into the why, so we can more confidently pick one over the other. Adios!&lt;/p&gt;

&lt;p&gt;(PS: This article was inspired by poring over &lt;a href="https://dmitrytsepelev.dev/service-objects-anti-patterns" rel="noopener noreferrer"&gt;this&lt;/a&gt;.)&lt;/p&gt;

</description>
      <category>softwaredesign</category>
      <category>ruby</category>
    </item>
    <item>
      <title>Lessons for me on leadership, from a film about apes</title>
      <dc:creator>Shalvah</dc:creator>
      <pubDate>Sun, 21 Apr 2024 19:25:13 +0000</pubDate>
      <link>https://dev.to/shalvah/lessons-for-me-on-leadership-from-a-film-about-apes-34ca</link>
      <guid>https://dev.to/shalvah/lessons-for-me-on-leadership-from-a-film-about-apes-34ca</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--e5gRXuC4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn.swish.ink/caf2243d-94c4-49d1-9204-11e4c63a6b5f/media/caesar-apes.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--e5gRXuC4--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://cdn.swish.ink/caf2243d-94c4-49d1-9204-11e4c63a6b5f/media/caesar-apes.png" alt="" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Watching &lt;em&gt;Dawn of the Planet of the Apes&lt;/em&gt; gave me some thoughts about dictatorship, and more broadly, leadership. Mostly regarding at work, but also in other areas of my life. These are things that are also present in the real world, but somehow, it took a movie on apes for me to grasp them.&lt;/p&gt;

&lt;p&gt;Dictators (both Caesar and Koba) &lt;em&gt;take&lt;/em&gt; power. They don't wait around for a consensus that they're the leader. They take decisions, and you must accept it (or take a chance and rebel). This—taking one-sided decisions—is something I suck at. I always try to be "reasonable" and listen to others' opinions. This is nice (it makes me a great colleague) but makes me poor in situations where consensus is unclear, or where strong leadership is needed.&lt;/p&gt;

&lt;p&gt;This assumption of power is connected to confidence. For it to work, the dictator must not entertain doubt within themselves. They must believe completely in the legitimacy of their rule. For Caesar and Koba to be effective, they couldn't afford to have moments of repentance. No impostor syndrome. No wondering, "Who even gave me the right to be in charge? What if these people realize I'm no more special than they are?" Nope, you move forward. Even when you seize power via a coup, you must not doubt that you're right.&lt;/p&gt;

&lt;p&gt;Dictators create their own authority. Sometimes through fear, sometimes through respect.&lt;/p&gt;

&lt;p&gt;I think that these things are why dictatorship works, and also why it doesn't work. A dictator is always taking a gamble. Sure, they don't depend on consensus, but that also means consensus could someday overthrow them. Or another dictator could rise against them.&lt;/p&gt;

&lt;p&gt;Translating this to my work (and life): I'm often reluctant to &lt;em&gt;take&lt;/em&gt; power. I'm fine with being appointed or elected to lead, but I rarely decide to be in charge of others. I'd rather do shit myself than ask people over whom I have no explicit authority (eg superiors, peers, even juniors that don't explicitly report to me) to do it. But that's because I play it safe. I don't like taking a gamble, taking the risk of people refusing to cooperate or my leadership being questioned or my decisions backfiring. This is something I should improve on.&lt;/p&gt;

&lt;p&gt;My ability to listen to others' opinions is gold, and I should not lose it. But at the same time, I should be ready to matter-of-factly take decisions when needed, and not always shy away.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Postscript&lt;/strong&gt;: Interestingly, the feedback I got in my last peer review kinda amounted to this. Positive feedback talking about how I'm a great engineer and teammate, and critical feedback saying I could improve in confidence. It makes sense now.&lt;/p&gt;

</description>
      <category>nontech</category>
      <category>musings</category>
    </item>
    <item>
      <title>Learn SVG by drawing an arrow</title>
      <dc:creator>Shalvah</dc:creator>
      <pubDate>Thu, 11 Jan 2024 17:45:41 +0000</pubDate>
      <link>https://dev.to/shalvah/learn-svg-by-drawing-an-arrow-54n5</link>
      <guid>https://dev.to/shalvah/learn-svg-by-drawing-an-arrow-54n5</guid>
      <description>&lt;h2&gt;
  
  
  How SVG works
&lt;/h2&gt;

&lt;p&gt;The common way of representing digital images is via &lt;strong&gt;pixels&lt;/strong&gt;. Take an image and break it down into small squares ("pixels"), where each square has a single colour. If you have many tiny squares, the colours blend in, and the image looks smooth to the human eye. If you don't have enough squares, the image looks rough and jagged, and you can see the edges of the individual squares. This is the difference between high-resolution screens/images (more pixels) and low-resolution ones (fewer pixels).&lt;/p&gt;

&lt;p&gt;But there's also &lt;a href="https://en.wikipedia.org/wiki/Vector_graphics" rel="noopener noreferrer"&gt;vector graphics&lt;/a&gt; (SVG = Scalable Vector Graphics). They don't use pixels, but instead work by drawing the image on the display, using geometry (maths). For example, an arrow in PNG would consist of many, many squares lined up together in the shape of an arrow, while in SVG, it could consist of a line starting from some coordinates (x1, y1), then turning and turning until the shape of the arrow head is complete.&lt;/p&gt;

&lt;p&gt;Both types of graphics have their use cases. Adobe has a good introductory explainer on &lt;a href="https://www.adobe.com/creativecloud/file-types/image/comparison/raster-vs-vector.html" rel="noopener noreferrer"&gt;raster graphics vs vector graphics&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;(PS: all of the illustrations in this article are SVGs. If you try zooming into them, you'll see that they don't get pixelated.)&lt;/p&gt;

&lt;p&gt;This article will demonstrate SVGs by constructing an arrow in HTML/JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drawing with geometry
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;SVG shapes are defined based on geometry&lt;/strong&gt;. When you define an &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt; element, you get an invisible Cartesian plane; there are x and y axes, and all points you will draw in this SVG are specified relative to an origin. Note that by default, x increases to the right as normal, but y increases downwards, and the origin is at the top left. I've made a grid to illustrate this:&lt;/p&gt;

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

&lt;p&gt;For instance, to draw a circle, we specify the coordinates of its centre (&lt;code&gt;cx&lt;/code&gt; and &lt;code&gt;cy&lt;/code&gt;), and the value of its radius; for a line, we specify its start (&lt;code&gt;x1&lt;/code&gt;, &lt;code&gt;y1&lt;/code&gt;) and end (&lt;code&gt;x2&lt;/code&gt;, &lt;code&gt;y2&lt;/code&gt;) coordinates; for a rectangle, start coordinates (&lt;code&gt;x&lt;/code&gt;, &lt;code&gt;y&lt;/code&gt;), width and height:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"300"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"300"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 300 300"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;circle&lt;/span&gt; &lt;span class="na"&gt;cx=&lt;/span&gt;&lt;span class="s"&gt;"150"&lt;/span&gt; &lt;span class="na"&gt;cy=&lt;/span&gt;&lt;span class="s"&gt;"100"&lt;/span&gt; &lt;span class="na"&gt;r=&lt;/span&gt;&lt;span class="s"&gt;"50"&lt;/span&gt; 
    &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"fill: none; stroke: red; stroke-width: 2px;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/circle&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;line&lt;/span&gt; &lt;span class="na"&gt;x1=&lt;/span&gt;&lt;span class="s"&gt;"50"&lt;/span&gt; &lt;span class="na"&gt;y1=&lt;/span&gt;&lt;span class="s"&gt;"50"&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"200"&lt;/span&gt; &lt;span class="na"&gt;y2=&lt;/span&gt;&lt;span class="s"&gt;"200"&lt;/span&gt; 
    &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"fill: none; stroke: black; stroke-width: 2px;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/line&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;rect&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"100"&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"150"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"50"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"75"&lt;/span&gt;
    &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"fill: none; stroke: green; stroke-width: 2px;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/rect&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;(Note that the grid lines and axes are not automatically added; these are separate SVG lines I added to make things clearer.)&lt;/p&gt;

&lt;p&gt;You may have noticed the &lt;code&gt;style&lt;/code&gt; attributes; SVGs can be styled like regular HTML elements, with inline CSS, classes, and selectors.&lt;/p&gt;

&lt;p&gt;We've used &lt;code&gt;&amp;lt;circle&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;rect&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;line&amp;gt;&lt;/code&gt;, but at the basic level, you can construct any shape by describing its path (the &lt;code&gt;&amp;lt;path&amp;gt;&lt;/code&gt; element). Here's one way to draw the same shapes as above:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"300"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"300"&lt;/span&gt; &lt;span class="na"&gt;viewBox=&lt;/span&gt;&lt;span class="s"&gt;"0 0 300 300"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- line --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M 50,50 L 200,200"&lt;/span&gt;
    &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"fill: none; stroke: black; stroke-width: 2px;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/path&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- rectangle --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M 100,150 l 50,0 l 0,75 l -50,0 Z"&lt;/span&gt;
    &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"fill: none; stroke: green; stroke-width: 2px;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/path&amp;gt;&lt;/span&gt;

  &lt;span class="c"&gt;&amp;lt;!-- circle --&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M 100,100 A 50 50 0 0 1 200,100 A 50 50 0 0 1 100,100"&lt;/span&gt;
    &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"fill: none; stroke: red; stroke-width: 2px;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/path&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;The &lt;code&gt;d&lt;/code&gt; attribute has specific syntax, which I like to think is for instructing the "artist" on how to move their pen.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;For the line: &lt;code&gt;M 50,50 L 200,200&lt;/code&gt; says "&lt;em&gt;**M&lt;/em&gt;&lt;em&gt;ove your pen to x1=&lt;/em&gt;&lt;em&gt;50&lt;/em&gt;&lt;em&gt;,y1=&lt;/em&gt;&lt;em&gt;50&lt;/em&gt;&lt;em&gt;, then draw a straight **L&lt;/em&gt;&lt;em&gt;ine until you get to x2=&lt;/em&gt;&lt;em&gt;200&lt;/em&gt;&lt;em&gt;,y2=&lt;/em&gt;&lt;em&gt;200&lt;/em&gt;**".&lt;/li&gt;
&lt;li&gt;For the rectangle, &lt;code&gt;M 100,150 l 50,0 l 0,75 l -50,0 Z&lt;/code&gt; draws a line from corner to corner: "&lt;em&gt;**M&lt;/em&gt;&lt;em&gt;ove your pen to x1=&lt;/em&gt;&lt;em&gt;100&lt;/em&gt;&lt;em&gt;,y1=&lt;/em&gt;&lt;em&gt;150&lt;/em&gt;&lt;em&gt;, then draw a straight **l&lt;/em&gt;&lt;em&gt;ine until you get to x2=x1 + **50&lt;/em&gt;&lt;em&gt;,y2=y1 + **0&lt;/em&gt;&lt;em&gt;, then another **l&lt;/em&gt;&lt;em&gt;ine until x3=x2 + **0&lt;/em&gt;&lt;em&gt;,y3=y2 + **75&lt;/em&gt;&lt;em&gt;, then another **l&lt;/em&gt;&lt;em&gt;ine until x4=x3 *&lt;/em&gt;- 50*&lt;em&gt;,y4=y3 + **0&lt;/em&gt;&lt;em&gt;, then connect back to the start point (&lt;/em&gt;&lt;em&gt;Z&lt;/em&gt;&lt;em&gt;)&lt;/em&gt;".&lt;/li&gt;
&lt;li&gt;For the circle, &lt;code&gt;M 100,100 A 50 50 0 0 1 200,100 A 50 50 0 0 1 100,100&lt;/code&gt; moves to 100,100, and then draws one arc (&lt;strong&gt;A&lt;/strong&gt;) with a radius of 50 until it gets to 200,100. Then from that point, it draws another arc with the same radius to connect to the start point. (I won't explain the Arc syntax in detail because it's fairly complex.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I like &lt;a href="https://kurtbruns.github.io/svg-tutorial/" rel="noopener noreferrer"&gt;this article&lt;/a&gt; for a quick explainer on the syntax, and of course &lt;a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d#path_commands" rel="noopener noreferrer"&gt;MDN&lt;/a&gt; for a full reference.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;&amp;lt;path&amp;gt;&lt;/code&gt; is useful when you need to draw an irregular shape. It can get super complex, though; for regular images, you'll probably use some graphic design software that generates it for you. You'll likely only need to work with paths directly when animating something or making some custom graphic element, although &lt;a href="https://css-tricks.com/when-to-use-svg-vs-when-to-use-canvas/" rel="noopener noreferrer"&gt;you may want to use the Canvas API instead&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manipulating SVGs with JavaScript
&lt;/h2&gt;

&lt;p&gt;There's no special SVG JavaScript API; we must use the DOM APIs, similar to other HTML elements. An important difference is that you need to use &lt;code&gt;document.createElementNS()&lt;/code&gt; instead of &lt;code&gt;document.createElement()&lt;/code&gt;, otherwise the elements won't be rendered.&lt;/p&gt;

&lt;p&gt;For instance, here's a small class for this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Svg&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;static&lt;/span&gt; &lt;span class="nx"&gt;NAMESPACE_URI&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://www.w3.org/2000/svg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;domElementId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$svg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElementNS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Svg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NAMESPACE_URI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;svg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$svg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;xmlns&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Svg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NAMESPACE_URI&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$svg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;width&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$svg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;height&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$svg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;viewBox&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`0 0 &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;domElementId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$svg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elementType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attributes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{},&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;$element&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElementNS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Svg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NAMESPACE_URI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;elementType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;attributes&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;$element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(([&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;$element&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;$svg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$element&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;$element&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"container"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Svg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`container`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;300&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`path`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`M 50,50 L 200,200`&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;strokeWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`2px`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`green`&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`circle`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;cx&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`150`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`100`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`50`&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;strokeWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`2px`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`red`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;none&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a full-featured API, you can use a library such as &lt;a href="http://snapsvg.io/" rel="noopener noreferrer"&gt;Snap.svg&lt;/a&gt; or &lt;a href="https://svgjs.dev/" rel="noopener noreferrer"&gt;SVG.js&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Constructing an arrowhead
&lt;/h2&gt;

&lt;p&gt;To add an arrowhead to this line, let's go with the &lt;code&gt;&amp;lt;path&amp;gt;&lt;/code&gt; approach. Think about the shape of an arrowhead for a moment. How do you move your pen when drawing this?&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Tip: There's an easier way to draw an arrowhead in SVG (skip straight to the "marker" section). But if you want to have fun with the maths or go deeper, enjoy!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To draw an arrowhead as a path, we need four things: the coordinates of the starting point P, and those of the corner points Q, R, and S. Then our SVG path will be (assuming the pen is already at P): &lt;code&gt;L Qx,Qy L Rx,Ry L Sx,Sy L Px,Py&lt;/code&gt;. However, we only know the coordinates of the starting point P.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgist.github.com%2Fshalvah%2F293dbc0da774dad451063af57f20e4d8%2Fraw%2F98d1d891b7d9b1912e7be87c0e68f21d1c766441%2Farrowhead-points.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgist.github.com%2Fshalvah%2F293dbc0da774dad451063af57f20e4d8%2Fraw%2F98d1d891b7d9b1912e7be87c0e68f21d1c766441%2Farrowhead-points.svg"&gt;&lt;/a&gt;We also know the vertical height of the arrowhead, h, and its base, b, because an arrowhead is an isosceles triangle, and we can pick whatever height and base we want to make it look good.&lt;/p&gt;

&lt;p&gt;So let's do a little trigonometry to find the unknowns. For each unknown point, we will calculate the "delta" (Δ) of its coordinates (the distance from P in x and y).&lt;/p&gt;

&lt;p&gt;To find R, we can use the angle between line PR and the horizontal.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Farrowhead-trig-1.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Farrowhead-trig-1.svg"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This is a right-angled triangle, so this gives us \Delta R_x = h \cos \theta and \Delta R_y = h \sin \theta. But the second diagram shows that the angle \theta is also present in the smaller P-triangle (vertically opposite angles are equal), so \tan \theta = \frac{\Delta P_y}{\Delta P_x}.&lt;/p&gt;

&lt;p&gt;So we have&lt;/p&gt;

&lt;p&gt;\theta = \tan^{-1} \frac{\Delta P_y}{\Delta P_x}&lt;/p&gt;

&lt;p&gt;\Delta R_x = h \cos \theta&lt;/p&gt;

&lt;p&gt;\Delta R_y = h \sin \theta&lt;/p&gt;

&lt;p&gt;This gives R's coordinates in terms of P's (R_x = P_x + \Delta R_x and R_y = P_y + \Delta R_y).&lt;/p&gt;

&lt;p&gt;To find Q, we take the angle \alpha between the arrowhead's base and the horizontal, which gives us \Delta Q_x = b \cos \alpha and \Delta Q_y = b \sin \alpha&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Farrowhead-trig-2.svg" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Farrowhead-trig-2.svg"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now, this angle adds up with \theta to make 90° (or π/2 radians), so we have that:&lt;/p&gt;

&lt;p&gt;\Delta Q_x = b \cos (\frac{\pi}{2} - \theta)&lt;/p&gt;

&lt;p&gt;\Delta Q_y = b \sin (\frac{\pi}{2} - \theta)&lt;/p&gt;

&lt;p&gt;And Q is:&lt;/p&gt;

&lt;p&gt;Q_x = P_x - \Delta Q_x&lt;/p&gt;

&lt;p&gt;Q_y = P_y + \Delta Q_y&lt;/p&gt;

&lt;p&gt;We subtract the x-delta, since the line \overrightarrow{\rm PQ} is going to the left (x is decreasing).&lt;/p&gt;

&lt;p&gt;S is the reflection of Q, so it has the same deltas as Q, but in the opposite direction. For S, the y-delta is negative, since \overrightarrow{\rm PS} is going downwards (decreasing y).&lt;/p&gt;

&lt;p&gt;S_x = P_x + \Delta Q_x&lt;/p&gt;

&lt;p&gt;S_y = P_y - \Delta Q_y&lt;/p&gt;

&lt;p&gt;And here's all this as a JavaScript function (with h = 10, b = 5):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;makeArrow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;P&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// I'd normally write this out as "theta", but JavaScript supports Unicode identifiers, nice 😎&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;θ&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;atan&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;α&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PI&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;θ&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ΔQ&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;α&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;α&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;Q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;P&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;ΔQ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;P&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ΔQ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;S&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;P&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ΔQ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;P&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;ΔQ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ΔR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;θ&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;θ&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;R&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;P&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ΔR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;P&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;ΔR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`path`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;d&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`M &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; L &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;P&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;P&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; L &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; L &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;R&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; L &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;S&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; L &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;P&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;P&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;strokeWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`2px`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`black`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;makeArrow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's an interactive playground. You can edit the values to see how this arrow behaves. Can you spot its limitations?&lt;/p&gt;

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

&lt;p&gt;Arrow end (x, y):&lt;/p&gt;

&lt;p&gt;Arrow start (x, y):&lt;/p&gt;

&lt;p&gt;Two things you may have noticed:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The arrowhead doesn't handle rotation correctly. For instance, &lt;code&gt;makeArrow({ x: 30, y: 0 }, {x: 100, y: 200})&lt;/code&gt; produces an inverted arrowhead.&lt;/li&gt;
&lt;li&gt;A minor annoyance is that the arrowhead actually points beyond the destination. For instance an arrow to (100, 200) will have its arrowhead a few units past (100,200).&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We can adapt the calculations to account for these scenarios, but that's enough maths for today. Instead, we'll switch to using the &lt;code&gt;&amp;lt;marker&amp;gt;&lt;/code&gt; element, which can automatically handle these issues for us.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using &lt;code&gt;&amp;lt;marker&amp;gt;&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The good news is that SVG already has an element for this. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/SVG/Element/marker" rel="noopener noreferrer"&gt;The &lt;code&gt;&amp;lt;marker&amp;gt;&lt;/code&gt; element&lt;/a&gt; is meant for placing "markers" on a shape. For instance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;arrowheads (duh) or some other shape you want to put at the end of a line&lt;/li&gt;
&lt;li&gt;path breakpoints&lt;/li&gt;
&lt;li&gt;resize handles on an element&lt;/li&gt;
&lt;li&gt;drag-and-drop handles&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The marker approach builds on the "manual" maths we've already done, but fixes these two limitations:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;By setting &lt;code&gt;orient="auto-start-reverse"&lt;/code&gt; on the marker, the arrowhead will be rotated appropriately to match the line's placement.&lt;/li&gt;
&lt;li&gt;By setting &lt;code&gt;marker-end={url-to-the-marker}&lt;/code&gt; on the line, the arrowhead will end exactly at our destination.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The marker approach comprises a few things: first, a &lt;code&gt;&amp;lt;marker&amp;gt;&lt;/code&gt; element which has an &lt;code&gt;id&lt;/code&gt;. We set &lt;code&gt;orient="auto-start-reverse"&lt;/code&gt;, and set the &lt;code&gt;markerHeight&lt;/code&gt; and &lt;code&gt;markerWidth&lt;/code&gt; to our triangle height (10) and 2 * base (5), respectively.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;marker&lt;/span&gt;
  &lt;span class="na"&gt;markerWidth=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt; &lt;span class="na"&gt;markerHeight=&lt;/span&gt;&lt;span class="s"&gt;"10"&lt;/span&gt; &lt;span class="na"&gt;orient=&lt;/span&gt;&lt;span class="s"&gt;"auto-start-reverse"&lt;/span&gt; 
  &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"arrowhead"&lt;/span&gt; &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"stroke-width: 2px; stroke: black;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;/marker&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within the &lt;code&gt;&amp;lt;marker&amp;gt;&lt;/code&gt; element, we add a nested &lt;code&gt;&amp;lt;path&amp;gt;&lt;/code&gt; describing the arrowhead (only the triangle, not the rest of the arrow). We don't have to use &lt;code&gt;&amp;lt;path&amp;gt;&lt;/code&gt;, by the way; we could also use three &lt;code&gt;&amp;lt;line&amp;gt;&lt;/code&gt;s.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;marker&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
   &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"...something..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/marker&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How do we determine &lt;code&gt;d&lt;/code&gt;? Well, we can use the same values we calculated earlier, but we don't need to. One benefit of &lt;code&gt;&amp;lt;marker&amp;gt;&lt;/code&gt; is that its location on the grid doesn't matter. It can even be in another file; it will be rotated and rendered properly for any element it's attached to. This means our path does not need to depend on the &lt;em&gt;actual&lt;/em&gt; coordinates of the line (P, Q, R, S). Instead, we can pretend P is at (0, 0) and we're facing upwards. This allows us to directly use the base and height as our deltas, giving us:&lt;/p&gt;

&lt;p&gt;Q_x = P_x - b = -b&lt;/p&gt;

&lt;p&gt;Q_y = P_y + 0 = 0&lt;/p&gt;

&lt;p&gt;R_x = P_x + 0 = 0&lt;/p&gt;

&lt;p&gt;R_y = P_y + h = h&lt;/p&gt;

&lt;p&gt;S_x = P_x + b = b&lt;/p&gt;

&lt;p&gt;S_y = P_y - 0 = 0&lt;/p&gt;

&lt;p&gt;And so we have the arrowhead path, which will remain the same regardless of the line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;marker&lt;/span&gt; &lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
   &lt;span class="nt"&gt;&amp;lt;path&lt;/span&gt; &lt;span class="na"&gt;d=&lt;/span&gt;&lt;span class="s"&gt;"M 0,0 L -5,0 L 0,10 L 5,0 Z"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/path&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/marker&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally, a &lt;code&gt;&amp;lt;line&amp;gt;&lt;/code&gt; for the rest of the arrow as usual, setting its &lt;code&gt;marker-end&lt;/code&gt; to the &lt;code&gt;id&lt;/code&gt; of the marker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;line&lt;/span&gt; 
  &lt;span class="na"&gt;x1=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;y1=&lt;/span&gt;&lt;span class="s"&gt;"0"&lt;/span&gt; &lt;span class="na"&gt;x2=&lt;/span&gt;&lt;span class="s"&gt;"100"&lt;/span&gt; &lt;span class="na"&gt;y2=&lt;/span&gt;&lt;span class="s"&gt;"200"&lt;/span&gt;
  &lt;span class="na"&gt;marker-end=&lt;/span&gt;&lt;span class="s"&gt;"url(#arrowhead)"&lt;/span&gt; 
  &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"stroke-width: 2px; stroke: black;"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/line&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Markers are reusable elements; we can draw many different lines which have the same marker, and it will be rendered and appropriately rotated for each. You can also set &lt;code&gt;marker-start&lt;/code&gt; instead (or both), if you wish to have the arrow on the other end.&lt;/p&gt;

&lt;p&gt;Porting this to JavaScript:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;$marker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`marker`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;markerWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;markerHeight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;orient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`auto-start-reverse`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;arrowhead&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;strokeWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`2px`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`black`&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;$path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElementNS&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Svg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NAMESPACE_URI&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`d`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`M 0,0 L -&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,0 L 0,&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; L &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,0 Z`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nx"&gt;$marker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;appendChild&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;$path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;makeArrowWithMarker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`line`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;x1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;y1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;x2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;y2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;marker-end&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`url(#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;$marker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;strokeWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`2px`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stroke&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`black`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;makeArrowWithMarker&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And finally, here it is in action:&lt;/p&gt;

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

&lt;p&gt;Arrow end (x, y):&lt;/p&gt;

&lt;p&gt;Arrow start (x, y):&lt;/p&gt;

&lt;h2&gt;
  
  
  Further reading
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Peter Collingridge has a good collection of &lt;a href="https://www.petercollingridge.co.uk/tutorials/svg/" rel="noopener noreferrer"&gt;interactive SVG tutorials&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>javascript</category>
      <category>maths</category>
    </item>
    <item>
      <title>Building a PHP client for Faktory, Part 6: Higher-level usage</title>
      <dc:creator>Shalvah</dc:creator>
      <pubDate>Sat, 25 Nov 2023 23:43:51 +0000</pubDate>
      <link>https://dev.to/shalvah/building-a-php-client-for-faktory-part-6-higher-level-usage-4g6h</link>
      <guid>https://dev.to/shalvah/building-a-php-client-for-faktory-part-6-higher-level-usage-4g6h</guid>
      <description>&lt;p&gt;We have the low-level tools for connecting to the Faktory server, executing commands, fetching data, logging, and so on. Now, I'll work on the high-level API this Faktory library will expose. I plan to make it flexible enough for a user to use directly, or maybe plug it into their framework.&lt;/p&gt;

&lt;p&gt;We need to provide a producer API (enqueuing jobs) and a consumer API (fetching and executing jobs).&lt;/p&gt;

&lt;h2&gt;
  
  
  Producer API
&lt;/h2&gt;

&lt;p&gt;For the producer, we can expose a base &lt;code&gt;Job&lt;/code&gt; class and a &lt;code&gt;Dispatcher&lt;/code&gt; which enqueues jobs.&lt;/p&gt;

&lt;p&gt;My proposed usage looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Create a dispatcher&lt;/span&gt;
&lt;span class="nv"&gt;$dispatcher&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Dispatcher&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;logLevel&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;\Monolog\Level&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'tcp://localhost'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Dispatch a job now&lt;/span&gt;
&lt;span class="nv"&gt;$dispatcher&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SendWelcomeEmail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="c1"&gt;// Schedule a job for later&lt;/span&gt;
&lt;span class="nv"&gt;$dispatcher&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;SendWelcomeEmail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;delaySeconds&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// Enqueue multiple instances in bulk&lt;/span&gt;
&lt;span class="nv"&gt;$dispatcher&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dispatchMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nc"&gt;SendWelcomeEmail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nv"&gt;$user1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$user2&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt; 
  &lt;span class="n"&gt;delaySeconds&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I believe in convenience, so I'll also provide a global dispatcher:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nc"&gt;Dispatcher&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;logLevel&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;\Monolog\Level&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nc"&gt;Debug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;hostname&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'tcp://dreamatorium'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;SendWelcomeEmail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;SendWelcomeEmail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatchIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$userId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nc"&gt;SendWelcomeEmail&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;dispatchMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;inSeconds&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="nv"&gt;$user1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;$user2&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Okay, implementing. The job class allows a user to specify options for the the job payload sent to Faktory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;abstract&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Job&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$retry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$reserveFor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1800&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$custom&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt; &lt;span class="c1"&gt;// From the Faktory spec; allows users to add custom data&lt;/span&gt;

  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Dispatcher&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;dispatchIn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;?int&lt;/span&gt; &lt;span class="nv"&gt;$seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Dispatcher&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delaySeconds&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$seconds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;dispatchMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;?int&lt;/span&gt; &lt;span class="nv"&gt;$inSeconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;Dispatcher&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;dispatchMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;delaySeconds&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$inSeconds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Dispatcher creates and passes configuration to the Faktory client, and transforms job operations (enqueueing/scheduling) to the appropriate Faktory operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Dispatcher&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;dispatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$jobClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$delaySeconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$jobPayload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;toJobPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$jobClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$delaySeconds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$jobPayload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;dispatchMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$jobClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$argumentsListing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$delaySeconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$basicPayload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;toJobPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$jobClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="nv"&gt;$delaySeconds&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="nv"&gt;$jobPayloads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$argumentsListing&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$index&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;$jobPayloads&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_merge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$basicPayload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s2"&gt;"jid"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;$basicPayload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'jid'&lt;/span&gt;&lt;span class="p"&gt;]&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="nv"&gt;$index&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"args"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$arguments&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;]);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;pushBulk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$jobPayloads&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;toJobPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$jobClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nv"&gt;$delaySeconds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;PayloadBuilder&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="n"&gt;jobType&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$jobClass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$jobClass&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nv"&gt;$queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;retry&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$jobClass&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nv"&gt;$retry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;reserveFor&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$jobClass&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="nv"&gt;$reserveFor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;delaySeconds&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;$delaySeconds&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the above snippet, I've omitted some code from the dispatcher, mainly boilerplate like constructors and managing the underlying client. You can view the full code &lt;a href="https://github.com/shalvah/faktory-php/commit/006c684ff8cfb9991d32d63ad7105f8ec97e03f6"&gt;on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;PayloadBuilder&lt;/code&gt; class is a helper class to generate the needed payload shape for the Faktory server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PayloadBuilder&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;build&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$jobType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;?string&lt;/span&gt; &lt;span class="nv"&gt;$queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;?int&lt;/span&gt; &lt;span class="nv"&gt;$retry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;?int&lt;/span&gt; &lt;span class="nv"&gt;$reserveFor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kt"&gt;?int&lt;/span&gt; &lt;span class="nv"&gt;$delaySeconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="s1"&gt;'jid'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'job_'&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;bin2hex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;random_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
      &lt;span class="s1"&gt;'jobtype'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$jobType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s1"&gt;'args'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;];&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$queue&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'queue'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$queue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;is_null&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$retry&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="c1"&gt;// 0 is a possible value, meaning no retries&lt;/span&gt;
      &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'retry'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$retry&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$reserveFor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'reserve_for'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$reserveFor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$delaySeconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;$executionTime&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;\DateTimeImmutable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'@'&lt;/span&gt;&lt;span class="mf"&gt;.&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nv"&gt;$delaySeconds&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
      &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'at'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$executionTime&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;\DateTimeInterface&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ATOM&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$payload&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it for the dispatcher. Next up: retrieving and executing jobs. For this, I'm creating an &lt;code&gt;Executor&lt;/code&gt; class. It's quite similar to the Dispatcher in terms of managing the client and having a global instance, so I'll omit those parts. Here's the key logic::&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Executor&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$queues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

    &lt;span class="nv"&gt;$queuesListening&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$queues&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;"all queues"&lt;/span&gt; &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"queues"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;implode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;","&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$queues&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Listening on %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$queuesListening&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;$jobPayload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getNextJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$queues&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;processAndReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$jobPayload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="cd"&gt;/**
   * Process a retrieved job, and ACK or FAIL it to Faktory.
   */&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;processAndReport&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$jobPayload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$jobPayload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;reportSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$jobPayload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Throwable&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;reportFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$jobPayload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="cd"&gt;/**
   * Process a retrieved job. This merely instantiates the job and executes it.
   * Any exceptions thrown by the job are not handled.
   */&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$jobPayload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$jobInstance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;instantiateJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$jobPayload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nv"&gt;$jobInstance&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;$jobPayload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'args'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="cd"&gt;/**
   * Manually fetch the next job to be executed from the specified queues.
   * Faktory will block for a few seconds if no job available, then return null.
   * The $retryUntilAvailable parameter forces the executor to try again when this happens.
   */&lt;/span&gt;
  &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;getNextJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$queues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nv"&gt;$retryUntilAvailable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;array&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nv"&gt;$job&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;...&lt;/span&gt;&lt;span class="nv"&gt;$queues&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$job&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$retryUntilAvailable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$job&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;reportSuccess&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$jobPayload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Processed job result=success class=%s id=%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$jobPayload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'jobtype'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$jobPayload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'jid'&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;ack&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s2"&gt;"jid"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$originalJobPayload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"jid"&lt;/span&gt;&lt;span class="p"&gt;]]);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;reportFailure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$jobPayload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;Throwable&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Processed job result=failure class=%s id=%s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$jobPayload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'jobtype'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nv"&gt;$jobPayload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'jid'&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
    &lt;span class="nv"&gt;$this&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;fail&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
      &lt;span class="s2"&gt;"jid"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$originalJobPayload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"jid"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
      &lt;span class="s2"&gt;"errtype"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;class&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="s2"&gt;"message"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="s2"&gt;"backtrace"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;explode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$exception&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getTraceAsString&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt;
    &lt;span class="p"&gt;]));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;protected&lt;/span&gt; &lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;instantiateJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;array&lt;/span&gt; &lt;span class="nv"&gt;$jobPayload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;Job&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$jobPayload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'jobtype'&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;new&lt;/span&gt; &lt;span class="nv"&gt;$class&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I'll bring this article to a close here, because I got interrupted multiple times and ended up spending several months on it. 😅 I'm off to tackle the next stages; &lt;a href="https://github.com/shalvah/faktory-php/commit/006c684ff8cfb9991d32d63ad7105f8ec97e03f6"&gt;here's&lt;/a&gt; the code thus far.&lt;/p&gt;

&lt;p&gt;Thus far, we have a basic working API. I'm not yet sure how it would fit into a production app, but it's good for a start. In the next phase, I'd like to address some limitations of the current setup.&lt;/p&gt;

</description>
      <category>queues</category>
      <category>php</category>
    </item>
    <item>
      <title>Exploring concurrent rate limiters, mutexes, semaphores</title>
      <dc:creator>Shalvah</dc:creator>
      <pubDate>Mon, 11 Sep 2023 22:39:11 +0000</pubDate>
      <link>https://dev.to/shalvah/diving-into-concurrent-rate-limiters-mutexes-semaphores-42i6</link>
      <guid>https://dev.to/shalvah/diving-into-concurrent-rate-limiters-mutexes-semaphores-42i6</guid>
      <description>&lt;p&gt;This is a dump of my learnings and experiments while going down a little rabbit hole.&lt;/p&gt;

&lt;h1&gt;
  
  
  Concurrent rate limiters
&lt;/h1&gt;

&lt;p&gt;I was studying &lt;a href="https://github.com/sidekiq/sidekiq/wiki/Ent-Rate-Limiting"&gt;Sidekiq's page on rate limiters&lt;/a&gt;. The first type of rate limiting mentioned is the concurrent limiter: only &lt;em&gt;n&lt;/em&gt; tasks are allowed to run at any point in time. Note that this is independent of time units (e.g. per second), or how long they take to run. The only limitation is the number of concurrent tasks/requests.&lt;/p&gt;

&lt;p&gt;So I asked myself, how would I implement a concurrent rate limiter? I'm fairly familiar with locking (via Redis and the database, for instance), so that was what came to mind, but in its usual form, that only works as a mutex (number of allowed tasks, &lt;em&gt;n&lt;/em&gt; = 1). I wasn't sure about how to implement that when &lt;em&gt;n&lt;/em&gt; &amp;gt; 1. Decided to dig into it from first principles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Concurrency control scenarios
&lt;/h2&gt;

&lt;p&gt;In this case, that meant stepping back to think about concurrency control in general, and the scenarios I know of.&lt;/p&gt;

&lt;p&gt;The first scenario is &lt;strong&gt;process-local&lt;/strong&gt;: you have multiple threads within a process, and you want to ensure only &lt;em&gt;n&lt;/em&gt; threads can access a resource at once. I already knew how to do this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;when &lt;em&gt;n&lt;/em&gt; = 1, use a &lt;a href="https://vaneyckt.io/posts/ruby_concurrency_in_praise_of_the_mutex/"&gt;mutex&lt;/a&gt;. Only the thread with the lock on the mutex can execute; others have to wait.&lt;/li&gt;
&lt;li&gt;when &lt;em&gt;n&lt;/em&gt; &amp;gt; 1, use a &lt;a href="https://en.wikipedia.org/wiki/Semaphore_(programming)"&gt;semaphore&lt;/a&gt;. I wasn't too familiar with semaphores, so I decided to brush up.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Semaphores are similar to mutexes, but they are less about guaranteeing exclusive access and more about keeping track of who has access. A semaphore starts out with a fixed number of "permits". A thread can request a permit (similar to acquiring a lock), and that reduces the number of available permits. When all permits are in use, any requesting threads will have to wait until one is released. In this sense, a semaphore is kinda like a bouncer at a club—it regulates the number of people who can get in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Semaphores via mutexes
&lt;/h3&gt;

&lt;p&gt;There are many semaphore implementations available for Ruby. I decided to implement one myself. The key thing is that the semaphore governs access to a resource (the number of permits), so we need a way to ensure the semaphore can do this job safely. I used Ruby's native Mutex to achieve this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-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;Semaphore&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;max_permits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@max_permits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max_permits&lt;/span&gt;
    &lt;span class="vi"&gt;@used_permits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="vi"&gt;@mutex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Mutex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&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;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;acquire&lt;/span&gt;
    &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="n"&gt;release&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;acquire&lt;/span&gt;
    &lt;span class="n"&gt;acquired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="k"&gt;until&lt;/span&gt; &lt;span class="n"&gt;acquired&lt;/span&gt;
      &lt;span class="vi"&gt;@mutex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;synchronize&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
        &lt;span class="n"&gt;acquired&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;permit_acquired?&lt;/span&gt;
      &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;acquired&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;def&lt;/span&gt; &lt;span class="nf"&gt;permit_acquired?&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@used_permits&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="vi"&gt;@max_permits&lt;/span&gt;
      &lt;span class="vi"&gt;@used_permits&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="kp"&gt;false&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;release&lt;/span&gt;
    &lt;span class="vi"&gt;@mutex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;synchronize&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="vi"&gt;@used_permits&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@used_permits&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@max_permits&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="vi"&gt;@used_permits&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; permit(s) available"&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;Usage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;semaphore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Semaphore&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;t1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;semaphore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s1"&gt;'Thread 1 acquired semaphore'&lt;/span&gt;
    &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="nb"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;p&lt;/span&gt; &lt;span class="s2"&gt;"Thread 1 releasing"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;t2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;semaphore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s1"&gt;'Thread 2 acquired semaphore'&lt;/span&gt;
    &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="nb"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;p&lt;/span&gt; &lt;span class="s2"&gt;"Thread 2 releasing"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="n"&gt;t3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;semaphore&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;permit&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s1"&gt;'Thread 3 acquired semaphore'&lt;/span&gt;
    &lt;span class="nb"&gt;sleep&lt;/span&gt; &lt;span class="nb"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;p&lt;/span&gt; &lt;span class="s2"&gt;"Thread 3 releasing"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t3&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;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:join&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sample output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Thread 1 acquired semaphore
Thread 2 acquired semaphore
"Thread 2 releasing"
1 permit(s) available
Thread 3 acquired semaphore
"Thread 1 releasing"
1 permit(s) available
"Thread 3 releasing"
2 permit(s) available
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach checks whether there are any available permits and returns one if so. Otherwise, it will sleep for 0.05 seconds and check again. The mutex guarantees that we can safely increment or decrement the number of permits without race conditions. This is a basic implementation, btw; one important thing missing is wait timeouts—we shouldn't have to wait forever.&lt;/p&gt;

&lt;p&gt;Also note that there is no expiry on a permit—a client could get a permit and refuse to release it! Apparently, that's by design; semaphores don't control what you do with the permit. The onus is on you to be responsible with it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Fair semaphores
&lt;/h3&gt;

&lt;p&gt;This approach suffers from &lt;em&gt;unfairness&lt;/em&gt;. Suppose thread A has been waiting for a permit to become available, and finally, another thread releases one. If at that moment, thread A is still sleeping, and a new thread (thread B) is launched, B might acquire the permit instead of A. In essence, an unlucky thread could wait for a very long time (or forever!) while newer threads get a permit. This is like if the bouncer selected people at random, instead of who's been waiting the longest.&lt;/p&gt;

&lt;p&gt;Also, the constant sleeping and waking up (polling) is suboptimal. We're giving the Ruby interpreter and OS more work to do (constantly scheduling and waking up the thread) when there might not be actually any permits available, and we're potentially taking time away from threads which have actual work to do.&lt;/p&gt;

&lt;p&gt;So I decided to try a different approach, using Ruby's &lt;code&gt;Thread::Queue&lt;/code&gt;. &lt;code&gt;Thread::Queue&lt;/code&gt; is a queue data structure designed for concurrent use; requesting an item (via &lt;code&gt;#pop&lt;/code&gt;) will either return an item if one is available, or block until another thread adds one to the queue (via &lt;code&gt;#push&lt;/code&gt;). So we model the list of permits as a queue (you can put anything you want in the queue, as long as it's the same number as the permits). Acquiring = popping, releasing = pushing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-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;Semaphore&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;max_permits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@permits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Thread&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Queue&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;max_permits&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;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;acquire&lt;/span&gt;
    &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="n"&gt;release&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;acquire&lt;/span&gt;
    &lt;span class="vi"&gt;@permits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&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;release&lt;/span&gt;
    &lt;span class="vi"&gt;@permits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@permits&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;size&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; permit(s) available"&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 works too (and the code is also much shorter). I'm not a 100% certain the waiting threads are served in order of who asked first (the docs don't say exactly that), but I think that's the case.&lt;/p&gt;

&lt;h3&gt;
  
  
  Condition variables
&lt;/h3&gt;

&lt;p&gt;After this, I took a look at the semaphore class in the popular library, &lt;a href="https://github.com/ruby-concurrency/concurrent-ruby/"&gt;concurrent-ruby&lt;/a&gt; to see how they implement it, and I learnt about something new: &lt;a href="https://vaneyckt.io/posts/ruby_concurrency_in_praise_of_condition_variables/"&gt;condition variables&lt;/a&gt;. And Ruby comes with this included!&lt;/p&gt;

&lt;p&gt;The name sounds super technical, but it's quite approachable in reality: a condition variable lets you tell other threads waiting on a resource that it's now available. It's meant to be used with a mutex. Instead of having the thread constantly poll, as in my initial implementation, the thread sleeps forever (or with a timeout), and gets woken up by the condition variable when a new permit is available. Here's the new implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-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;Semaphore&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;max_permits&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@max_permits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max_permits&lt;/span&gt;
    &lt;span class="vi"&gt;@used_permits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="vi"&gt;@mutex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Mutex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
    &lt;span class="vi"&gt;@condition_variable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;ConditionVariable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&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;permit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;acquire&lt;/span&gt;
    &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;call&lt;/span&gt;
    &lt;span class="n"&gt;release&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;acquire&lt;/span&gt;
    &lt;span class="vi"&gt;@mutex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;synchronize&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="k"&gt;until&lt;/span&gt; &lt;span class="n"&gt;permit_acquired?&lt;/span&gt;
        &lt;span class="vi"&gt;@condition_variable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="vi"&gt;@mutex&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;def&lt;/span&gt; &lt;span class="nf"&gt;permit_acquired?&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@used_permits&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="vi"&gt;@max_permits&lt;/span&gt;
      &lt;span class="vi"&gt;@used_permits&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;

    &lt;span class="kp"&gt;false&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;release&lt;/span&gt;
    &lt;span class="vi"&gt;@mutex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;synchronize&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="vi"&gt;@used_permits&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vi"&gt;@used_permits&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
    &lt;span class="vi"&gt;@condition_variable&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;signal&lt;/span&gt;

    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="vi"&gt;@max_permits&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="vi"&gt;@used_permits&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; permit(s) available"&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;Rather than polling (sleep-wake-check-sleep), we just sleep (&lt;code&gt;@condition_variable.wait&lt;/code&gt;), and then when another thread is done, they call &lt;code&gt;@condition_variable.signal&lt;/code&gt;, which will wake up &lt;em&gt;the first&lt;/em&gt; waiting thread (so it's fair, yay). It reminds me a bit of events in JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distributed concurrency control
&lt;/h2&gt;

&lt;p&gt;Okay, good diversion; now, back to concurrent scenarios. We've looked at process-local scenarios. The second scenario is &lt;strong&gt;system-local&lt;/strong&gt;, but separate processes. This is, for example, when you have multiple web server processes running on the same machine, and you want to control access to a shared resource.&lt;/p&gt;

&lt;p&gt;Processes don't share memory, so our mutex/semaphore can't live inside any single process. We have to use an external datastore, such as a cache (eg Redis), or a database (PostgreSQL). You could even use a file or what-have-you.&lt;/p&gt;

&lt;p&gt;A third scenario is in a &lt;strong&gt;distributed system&lt;/strong&gt;: the processes are on separate machines. This is an extension of the above case, so the same approaches as above apply, but obviously in a distributed way (the datastore has to live on an external location all the processes can access).&lt;/p&gt;

&lt;p&gt;Okay, so how could we implement mutexes (&lt;em&gt;n&lt;/em&gt; = 1) here?&lt;/p&gt;

&lt;h3&gt;
  
  
  Aside: things to keep in mind
&lt;/h3&gt;

&lt;p&gt;When dealing with locks/mutexes, you want to avoid &lt;em&gt;starvation&lt;/em&gt; (ie, an unruly/crashed client holding on to the lock and blocking everyone else). A common (but imperfect) way to avoid this is to set a &lt;em&gt;lock expiry&lt;/em&gt; (how long a given client can hold a lock). This way, if a client is unable to release the lock (for instance, if they crashed), it will automatically be released after a while.&lt;/p&gt;

&lt;p&gt;You also want to limit &lt;em&gt;contention&lt;/em&gt;, typically by setting a &lt;em&gt;wait timeout&lt;/em&gt; (how long a client should wait for when another client is using the lock). If you don't set a wait timeout, you could end up with other processes hanging forever because a lock is in use. Sometimes that might be desired, but more likely you probably want to quit and try again later. Either way, you should decide on your wait timeout policy for your clients.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mutex with Redis
&lt;/h3&gt;

&lt;p&gt;A common pattern is to set a key in Redis, something like &lt;code&gt;SET some-lock-key some-value NX EX expiry-in-seconds&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;NX&lt;/code&gt; will set the key only if it doesn't already exists. If the key already exists, it means another process has the lock, and you need to retry.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EX&lt;/code&gt; (or &lt;code&gt;PX&lt;/code&gt;) sets a time when the lock expires, so a crashed process doesn't keep on hanging on to the lock&lt;/li&gt;
&lt;li&gt;To release the lock, you can use &lt;code&gt;DEL&lt;/code&gt; to delete the key. (But you shouldn't! See below.)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This works, but has a few flaws:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Processes need to poll Redis until they get the lock, or give up. We already saw why polling is suboptimal.&lt;/li&gt;
&lt;li&gt;Lock expiry is, at best, a guess. You're &lt;em&gt;hoping&lt;/em&gt; all your clients will finish in that time. But if one process somehow doesn't finish in time, the lock would erroneously expire, and you could end up with two concurrent processes (!)&lt;/li&gt;
&lt;li&gt;If the above happens, and the first process tries to release the lock with &lt;code&gt;DEL&lt;/code&gt;, it would delete the lock now held by the second process. The Redis docs have &lt;a href="https://redis.io/docs/manual/patterns/distributed-locks/#:~:text=Correct%20Implementation%20with%20a%20Single%20Instance"&gt;details&lt;/a&gt; on how to correctly delete a lock.&lt;/li&gt;
&lt;li&gt;In a distributed Redis cluster, a lock could be acquired multiple times. Distributed locking is probably a topic for another day, though. Patterns like &lt;a href="https://redis.io/docs/manual/patterns/distributed-locks/"&gt;Redlock&lt;/a&gt; are suggested (but also &lt;a href="http://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html"&gt;criticized&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Mutex with PostgreSQL
&lt;/h3&gt;

&lt;p&gt;For SQL, I like &lt;a href="https://shiroyasha.io/advisory-locks-and-how-to-use-them.html"&gt;Postgres' advisory locks&lt;/a&gt;. Running &lt;code&gt;SELECT pg_advisory_lock(123)&lt;/code&gt; will give this client a lock called &lt;code&gt;123&lt;/code&gt;, and all other clients who run that same statement will have to wait until the first client releases the lock.&lt;/p&gt;

&lt;p&gt;With Postgres' advisory locks, you don't need a lock expiry, since the lock is bound to the session or transaction. If the client crashes, PG will release the lock. Wait timeouts aren't directly supported, but you can achieve this by using &lt;a href="https://postgresqlco.nf/doc/en/param/lock_timeout/"&gt;the &lt;code&gt;lock_timeout&lt;/code&gt; setting&lt;/a&gt; (combined with transaction-level locks if you don't want global wait timeouts):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;BEGIN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- Start transaction&lt;/span&gt;
&lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="k"&gt;LOCAL&lt;/span&gt; &lt;span class="n"&gt;lock_timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'10s'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- Error if no lock acquired after 10s&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pg_advisory_xact_lock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;123&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;-- Get a transaction-level lock&lt;/span&gt;

&lt;span class="c1"&gt;-- Execute the rest of your code&lt;/span&gt;

&lt;span class="k"&gt;COMMIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;-- End transaction; lock is released automatically&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;MySQL also has advisory locks (although they are not transaction-bound) and wait timeouts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;GET_LOCK&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'my_lock'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;-- Error if no lock acquired after 10s&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;RELEASE_LOCK&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'my_lock'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I love Redis, but I think I prefer SQL databases for mutexes. Since the Redis API does not expose any concept of a lock, we try to emulate it with the &lt;code&gt;SET NX&lt;/code&gt; pattern or more complicated algorithms, which is why we have to do the "lock expiry" dance. PostgreSQL, on the other hand, has locks as a first-class function, which means it can provide better guarantees, such as the fact that locks will always be released when the session ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Semaphore with Redis
&lt;/h2&gt;

&lt;p&gt;How about semaphores (&lt;em&gt;n&lt;/em&gt; &amp;gt; 1)? At first I was thinking of something using &lt;a href="https://redis.io/docs/interact/transactions/"&gt;Redis transactions&lt;/a&gt; (&lt;code&gt;MULTI&lt;/code&gt;) and &lt;code&gt;WATCH&lt;/code&gt;, something like this (not valid code):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;WATCH&lt;/span&gt; &lt;span class="n"&gt;semaphore&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;permits&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;used&lt;/span&gt;
&lt;span class="n"&gt;max&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;GET&lt;/span&gt; &lt;span class="n"&gt;semaphore&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;permits&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;max&lt;/span&gt;
&lt;span class="n"&gt;used&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;GET&lt;/span&gt; &lt;span class="n"&gt;semaphore&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;permits&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;used&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;used&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;max&lt;/span&gt; &lt;span class="c1"&gt;# No permits available&lt;/span&gt;

&lt;span class="no"&gt;MULTI&lt;/span&gt;
&lt;span class="no"&gt;INCR&lt;/span&gt; &lt;span class="n"&gt;semaphore&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;permits&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;used&lt;/span&gt;
&lt;span class="no"&gt;EXEC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;WATCH&lt;/code&gt; will fail the transaction if the &lt;code&gt;semaphore-permits-used&lt;/code&gt; is modified by another client, so this serves as a mutex for the permits. But this implementation seems pretty complex to me; it involves us switching between our app code and Redis multiple times (or making this a Redis script). i haven't had the chance to try it yet, though.&lt;/p&gt;

&lt;p&gt;Turns out, you can implement a semaphore in Redis quite simply with blocking list operations (akin to what we did with &lt;code&gt;Thread::Queue&lt;/code&gt; in Ruby):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First, put &lt;em&gt;n&lt;/em&gt; items in a list in Redis (say &lt;code&gt;semaphore-available-permits&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;To acquire a permit, call &lt;code&gt;BLPOP semaphore-available-permits&lt;/code&gt;. This will pop one item from the list. If there's none available, it will block until some other client pushes one. You can also specify a wait timeout: &lt;code&gt;BLPOP semaphore-available-permits &amp;lt;wait-timeout&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;To release a permit, call &lt;code&gt;RPUSH semaphore-available-permits&lt;/code&gt;. If there are clients waiting for a permit, the longest waiting client will automatically get the newly released permit (so it's a fair semaphore).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's still a bit ugly, because that first step is crucial (otherwise clients would wait forever). The best approach there is to either have each client check if the semaphore has been initialized (e.g. by checking if a certain key exists), and initialize it themselves if not; alternatively, you could have an explicit initialization step that creates the permits when your app starts up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Semaphore with Postgres
&lt;/h2&gt;

&lt;p&gt;Unfortunately, advisory locks don't help here. We need a good ol' database table to keep track of our semaphores.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;global_semaphores&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;semaphore_name&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;max_permits&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;used_permits&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To kick things off, we create the semaphore with capacity &lt;em&gt;n&lt;/em&gt; = 4:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;global_semaphores&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;"my_semaphore"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Checking out and releasing a permit is straightforward: update the &lt;code&gt;used_permits&lt;/code&gt; count. But, to ensure exclusive access, we must use a transaction:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;global_semaphores&lt;/span&gt; 
  &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;used_permits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;used_permits&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;semaphore_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"my_semaphore"&lt;/span&gt;
  &lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;used_permits&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;max_permits&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;UPDATE&lt;/code&gt; statement will automatically lock the matching row (our semaphore) until it finishes executing, so no transaction needed (unless we want to specify a local timeout). All we need to do is check if the row was updated; if so, we have our permit.&lt;/p&gt;

&lt;p&gt;To release the permit, it's the reverse:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="n"&gt;global_semaphores&lt;/span&gt; 
  &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;used_permits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;used_permits&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;semaphore_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;"my_semaphore"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One downside here is that there's no easy way to block while waiting for a new permit to be available. We'll have to rely on polling.&lt;/p&gt;

&lt;p&gt;I find it quite interesting the differences in approach between Redis and PG here: an explicit list of permit items (Redis) vs a counter (Postgres). I think you could use either approach in both, but it would be more complicated. (I actually had an initial implementation in Postgres that used multiple rows and transactional isolation, but it was def more complex.) Postgres' transactional guarantees make it easier to work with a single counter (= a single row), while Redis' list data structure and blocking options make that approach straightforward.&lt;/p&gt;

&lt;h1&gt;
  
  
  Putting it all together: concurrent rate limiter
&lt;/h1&gt;

&lt;p&gt;We're almost there! Actually, we're there. A concurrent rate limiter is essentially a semaphore. The max number of permits = the max number of concurrent tasks.&lt;/p&gt;

&lt;p&gt;However, the Sidekiq version also includes lock expiry, so I spent some time thinking about it. My conclusion: there's no "nice" way to do it. The permit expiry approach I could think of (or find in the wild) was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;When a client gets a permit successfully, it must record that permit and its acquisition time (in a hash in Redis, or a row in a table in Postgres)&lt;/li&gt;
&lt;li&gt;When the client releases the permit, it can then delete the hash entry or row&lt;/li&gt;
&lt;li&gt;You either have an external process that regularly checks for permits that are too old and force-releases them, or have a newly-connecting client do that check themselves.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I think that's it for that exploration. It was quite interesting racking my brain about semaphores and guarantees. My current conclusions are that I'd prefer to use Postgres for mutexes and Redis for semaphores. It was also a revelation that semaphores don't provide any guarantee of expiry, so you must program carefully around that.&lt;/p&gt;

</description>
      <category>concurrency</category>
      <category>ruby</category>
      <category>datastores</category>
    </item>
    <item>
      <title>High and low cardinality</title>
      <dc:creator>Shalvah</dc:creator>
      <pubDate>Sun, 13 Aug 2023 12:36:14 +0000</pubDate>
      <link>https://dev.to/shalvah/high-and-low-cardinality-52e6</link>
      <guid>https://dev.to/shalvah/high-and-low-cardinality-52e6</guid>
      <description>&lt;p&gt;&lt;em&gt;Let's talk about cardinality. Can we talk about cardinality please, Mac? I've been dying to talk about cardinality with you all day.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fpepe-silvia.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.swish.ink%2Fcaf2243d-94c4-49d1-9204-11e4c63a6b5f%2Fmedia%2Fpepe-silvia.png" title="pepe-silvia.png"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In maths, cardinality is the number of elements in a set. &lt;code&gt;A = {2, 4, 5, 8}&lt;/code&gt; -&amp;gt; cardinality of A is 4. It's quite similar in software engineering—cardinality is a rough idea of how many distinct values are in a set.&lt;/p&gt;

&lt;p&gt;For example, timestamp fields (such as &lt;code&gt;created_at&lt;/code&gt;) are often &lt;em&gt;high-cardinality&lt;/em&gt;, because they will likely have many different values. By contrast, a field like &lt;code&gt;day_of_week&lt;/code&gt; is &lt;em&gt;low-cardinality&lt;/em&gt;, because it can only have 7 different values. An enum field like &lt;code&gt;status&lt;/code&gt; is also probably low-cardinality (&lt;code&gt;active&lt;/code&gt;, &lt;code&gt;inactive&lt;/code&gt;, and maybe a few others). Boolean fields/flags have very low cardinality—just two values. User-specific data, like user IDs or names, are high-cardinality.&lt;/p&gt;

&lt;p&gt;Note: there's &lt;a href="https://en.wikipedia.org/wiki/Cardinality_(data_modeling)" rel="noopener noreferrer"&gt;another common usage of cardinality in software&lt;/a&gt;, which refers to relationships in database entities, but that's not what I'm talking about here.&lt;/p&gt;

&lt;p&gt;Why does cardinality matter? It has effects on data storage and access patterns.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data access
&lt;/h2&gt;

&lt;p&gt;A good example of where cardinality affects access is relational database indexes.&lt;/p&gt;

&lt;p&gt;You might have heard that you should add an index for any fields you want to query on. So, supposing you have a &lt;code&gt;users&lt;/code&gt; table, you might add an index on &lt;code&gt;created_at&lt;/code&gt; and &lt;code&gt;status&lt;/code&gt;, so your queries can be faster.&lt;/p&gt;

&lt;p&gt;Here's the thing—for most simple queries, the database only uses one index[&lt;em&gt;note 1&lt;/em&gt;]. So in a query like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt; 
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'active'&lt;/span&gt; 
&lt;span class="k"&gt;AND&lt;/span&gt; &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="s1"&gt;'2023-08-01 00:00:00'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the database has to decide which index it will use, and it's fairly likely it will pick the one on created_at.&lt;/p&gt;

&lt;p&gt;Why's that? Because the database engine maintains statistics about the tables, and it may have noticed that the &lt;code&gt;status&lt;/code&gt; column usually only has a few different values, while &lt;code&gt;created_at&lt;/code&gt; has a wide variety. This means that if it uses the index on &lt;code&gt;created_at&lt;/code&gt;, it could filter out a good portion of the rows in the table, and then scan each row to check if it matches the status.&lt;/p&gt;

&lt;p&gt;Imagine we have 25,000 users. Most (say 17,000) are active. If the database used the &lt;code&gt;status&lt;/code&gt; index, it would have 17,000 rows that it needs to then loop through and check the creation timestamp. But if it used the &lt;code&gt;created_at&lt;/code&gt; index and a time range, it could end up with maybe 2,000 users created in the last month. That's a lot fewer rows to check.&lt;/p&gt;

&lt;p&gt;Of course, this doesn't always work as I've described. The engine doesn't know your application like you do. A different query, a different time range, different data patterns (such as most users from a certain period being inactive), or outdated intenral engine statistics might make the engine's chosen index perform worse, or might make it to pick an entirely different algorithm.&lt;/p&gt;

&lt;p&gt;An important point: &lt;strong&gt;cardinality is relative&lt;/strong&gt;! Or more accurately, &lt;em&gt;selectivity&lt;/em&gt; is. Selectivity is cardinality (distinct values in a set) relative to the overall size of the set. The &lt;code&gt;status&lt;/code&gt; column might have only 6 distinct values, but if there are only 12 rows, that's a good selectivity ratio, as there's a good chance we could filter out half of the rows with one query.&lt;/p&gt;

&lt;p&gt;Database indexing is an involved topic, and I won't delve further here. I'm a big fan of &lt;a href="https://planetscale.com/blog/why-isnt-mysql-using-my-index" rel="noopener noreferrer"&gt;this article&lt;/a&gt;, which talks more about cardinality, selectivity, and many different reasons your index might not be used.&lt;/p&gt;

&lt;h2&gt;
  
  
  Data storage
&lt;/h2&gt;

&lt;p&gt;It's related to access, but more specifically: if we know that a set has a small range of values, we can store it more efficiently (which could mean we can also query it more efficiently). This is what &lt;a href="https://clickhouse.com/docs/en/sql-reference/data-types/lowcardinality" rel="noopener noreferrer"&gt;ClickHouse's &lt;code&gt;LowCardinality&lt;/code&gt; column type&lt;/a&gt; does.&lt;/p&gt;

&lt;p&gt;For instance, suppose we have a &lt;code&gt;status&lt;/code&gt; column as a &lt;code&gt;String&lt;/code&gt;. If we insert three rows with status of "active", we'll be storing a 6-byte string 3 times. But if &lt;code&gt;status&lt;/code&gt; is designated as &lt;code&gt;LowCardinality&lt;/code&gt;, the database can avoid this by storing &lt;code&gt;active&lt;/code&gt; once, and assigning it a small integer ID (like what we did in &lt;a href="https://blog.shalvah.me/posts/packing-and-unpacking-bytes" rel="noopener noreferrer"&gt;byte packing&lt;/a&gt;). Then every new row gets that ID stored, meaning we'd only store 1 byte per row.&lt;/p&gt;

&lt;p&gt;In a database like ClickHouse, used for storing huge amounts of data, this is quite valuable. &lt;a href="https://www.tinybird.co/clickhouse/knowledge-base/low-cardinality" rel="noopener noreferrer"&gt;Here's&lt;/a&gt; a good demonstration of the space savings, and &lt;a href="https://altinity.com/blog/2019/3/27/low-cardinality" rel="noopener noreferrer"&gt;here's&lt;/a&gt; an example where it improves query speed.&lt;/p&gt;

&lt;p&gt;As an end user, it's also helpful to know when a tool you use expects a value to be low- or high-cardinality, even when they don't use those terms.&lt;/p&gt;

&lt;p&gt;An example: in Datadog, metrics can have tags; for a metric such as "number of payments processed", you could attach a tag like &lt;code&gt;status&lt;/code&gt;, with values such as &lt;code&gt;status:successful&lt;/code&gt; and &lt;code&gt;status:failed&lt;/code&gt;. However, &lt;a href="https://docs.datadoghq.com/account_management/billing/custom_metrics/" rel="noopener noreferrer"&gt;you're charged for every metric + tag combination&lt;/a&gt;, so you pay for every new value of &lt;code&gt;status&lt;/code&gt; that you send. If you introduce a &lt;code&gt;status:pending&lt;/code&gt;, you'll pay (literally) for that!&lt;/p&gt;

&lt;p&gt;This means your tags should not have unbounded cardinality. A tag like &lt;code&gt;payment_amount&lt;/code&gt; is asking for trouble. Instead, if you really need to capture values like that, you could put the payment amount in buckets (0 - 100, 100 - 500, etc).&lt;/p&gt;

&lt;h2&gt;
  
  
  Notes
&lt;/h2&gt;

&lt;p&gt;&lt;a href=""&gt;1.&lt;/a&gt; In theory, it could use more than one index (and it does sometimes), but because of the way indices work, that would be more complicated and potentially slower. But for more complex queries, like JOINs, multiple indexes help.&lt;/p&gt;

</description>
      <category>engineeringconcepts</category>
      <category>data</category>
    </item>
  </channel>
</rss>
