<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Paul Lam. Founding engineer for data-driven enterprise startups.</title><link href="https://www.quantisan.com/" rel="alternate"/><link href="https://www.quantisan.com/feeds/atom.xml" rel="self"/><id>https://www.quantisan.com/</id><updated>2023-05-11T00:00:00-04:00</updated><entry><title>Simplifying Step Functions and Stepwise: Lessons Learned and a New Approach</title><link href="https://www.quantisan.com/simplifying-step-functions-and-stepwise-lessons-learned-and-a-new-approach/" rel="alternate"/><published>2023-05-11T00:00:00-04:00</published><updated>2023-05-11T00:00:00-04:00</updated><author><name>Paul Lam</name></author><id>tag:www.quantisan.com,2023-05-11:/simplifying-step-functions-and-stepwise-lessons-learned-and-a-new-approach/</id><summary type="html">&lt;p&gt;At Motiva, we use AWS Step Functions to manage all of our workflows. To make
this simpler, we developed &lt;a href="https://github.com/Motiva-AI/stepwise"&gt;Stepwise&lt;/a&gt;,
an open-source Clojure library that helps us coordinate various tasks like
business processes, data pipelines, and even our machine learning workflows. By
using Step Functions, we can effortlessly &lt;a href="/took-me-4-years-to-realize-wed-been-orchestrating-workflows/"&gt;handle complex …&lt;/a&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;At Motiva, we use AWS Step Functions to manage all of our workflows. To make
this simpler, we developed &lt;a href="https://github.com/Motiva-AI/stepwise"&gt;Stepwise&lt;/a&gt;,
an open-source Clojure library that helps us coordinate various tasks like
business processes, data pipelines, and even our machine learning workflows. By
using Step Functions, we can effortlessly &lt;a href="/took-me-4-years-to-realize-wed-been-orchestrating-workflows/"&gt;handle complex event-driven
processes and monitor our operations&lt;/a&gt;. Although we have been using Step
Functions and Stepwise in production for a couple years, we identified some
areas where we can improve the developer experience. This post will share some
bottlenecks we discovered and propose a new and improved version of a Step
Functions library, but with Clojure all the way.&lt;/p&gt;
&lt;p&gt;What do we mean Clojure all the way? We have been designing a new interface to
define Step Functions state machines, which we will illustrate with an example
of a pizza-making workflow. To create this state machine on AWS Step Functions,
we would use the following code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;def &lt;/span&gt;&lt;span class="nv"&gt;pizza-making-state-machine&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sfn/-&amp;gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;request&lt;/span&gt;
&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sfn/parallel&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;make-dough&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;                         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;make-sauce&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;                         &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sfn/map&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:iterate-over&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:ingredients&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;prepare-ingredients&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;put-ingredients-on-dough&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;bake&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sfn/wait&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:minutes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sfn/choice&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;comp not &lt;/span&gt;&lt;span class="nv"&gt;is-pizza-acceptable?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;                       &lt;/span&gt;&lt;span class="c1"&gt;;; branch off to this fn if condition is true&lt;/span&gt;
&lt;span class="w"&gt;                       &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sfn/fail&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;serve&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sfn/ensure-state-machine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;client&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:pauls-pizza-making-machine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;pizza-making-state-machine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This looks like normal Clojure code, but it is actually a workflow. This is
because AWS Step Functions keeps the state of your workflow at all times. So,
if your server, processes, or any of your workers go down during any workflow
execution, your execution will continue where it left off once your system
comes back online.&lt;/p&gt;
&lt;p&gt;&lt;img alt="Step Functions diagram for pizza-making-state-machine" src="https://www.quantisan.com/images/2023/pizza-making-sfn.svg"&gt;&lt;/p&gt;
&lt;p&gt;We can define the workers for each step of the workflow in Clojure as follows:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;defn &lt;/span&gt;&lt;span class="nv"&gt;make-dough&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;;; SFN error handling configuration as metadata&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:retry&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="ss"&gt;:error-equals&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:sfn.timeout&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="ss"&gt;:interval-seconds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="ss"&gt;:max-attempts&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;}]}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;coll&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;coll&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;defn &lt;/span&gt;&lt;span class="nv"&gt;put-ingredients-on-dough&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;coll&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;coll&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;defn &lt;/span&gt;&lt;span class="nv"&gt;bake&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;coll&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;coll&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;defn &lt;/span&gt;&lt;span class="nv"&gt;make-sauce&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;coll&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;coll&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sfn/def-choice-predicate-fn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;is-pizza-acceptable?&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;m&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;These functions define the steps in the pizza-making workflow. We can choose
where to run these workers at runtime using the following code:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sfn/run-here&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;client&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;make-dough&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:concurrency&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="nv"&gt;put-ingredients-on-dough&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:concurrency&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="nv"&gt;bake&lt;/span&gt;&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:concurrency&lt;/span&gt;&lt;span class="w"&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="nf"&gt;sfn/run-on-lambda&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;client&lt;/span&gt;
&lt;span class="w"&gt;                   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;make-sauce&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:timeout&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;
&lt;span class="w"&gt;                                &lt;/span&gt;&lt;span class="ss"&gt;:memory-size&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;
&lt;span class="w"&gt;                                &lt;/span&gt;&lt;span class="ss"&gt;:max-concurrency&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;}})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Since the workers are defined as Clojure functions, we can choose to run them
in containers or serverless functions at runtime.&lt;/p&gt;
&lt;p&gt;Say, what if &lt;code&gt;make-dough&lt;/code&gt; turns out to be an infrequent but bursty process that
would be more suitable to run on serverless. But &lt;code&gt;make-sauce&lt;/code&gt; takes too long
before Lambda times out. We can switch the two like so:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;sfn/run-here&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;client&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;make-sauce&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:concurrency&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="nv"&gt;put-ingredients-on-dough&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:concurrency&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="nv"&gt;bake&lt;/span&gt;&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:concurrency&lt;/span&gt;&lt;span class="w"&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="nf"&gt;sfn/run-on-lambda&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;client&lt;/span&gt;
&lt;span class="w"&gt;                   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;make-dough&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:timeout&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="w"&gt;                                &lt;/span&gt;&lt;span class="ss"&gt;:memory-size&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;
&lt;span class="w"&gt;                                &lt;/span&gt;&lt;span class="ss"&gt;:max-concurrency&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;}})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;At Motiva, we have state machines to manage email delivery, data integration,
and machine learning decisions, among others. However, we only have four
developers on our team, and we want to improve our speed in delivering quality
products with operational excellence. As demand grows and customers ask for
more, we're seeing that our speed to orchestrate new business workflows as
state machines is crucial to our competitiveness.&lt;/p&gt;
&lt;p&gt;To achieve this goal, we plan to simplify our development process by using only
one tool, Clojure, instead of using multiple tools like Amazon States Language
and Terraform. By doing so, we can focus on delivering value to our customers.&lt;/p&gt;
&lt;p&gt;So, what do you think? We're still in the design phase of this new library. If
this is something that's of interest to you, please &lt;a href="mailto:paul+blog_mailto@quantisan.com?subject=On stepwise.next blog post"&gt;get in touch with
me&lt;/a&gt;.&lt;/p&gt;</content><category term="computing"/></entry><entry><title>Moving from Lambda to SageMaker for inference</title><link href="https://www.quantisan.com/moving-from-lambda-to-sagemaker-for-inference/" rel="alternate"/><published>2023-04-07T00:00:00-04:00</published><updated>2023-04-07T00:00:00-04:00</updated><author><name>Paul Lam</name></author><id>tag:www.quantisan.com,2023-04-07:/moving-from-lambda-to-sagemaker-for-inference/</id><summary type="html">&lt;p&gt;A few years ago, we moved our machine learning inference endpoints to serverless in order to reduce operations upkeep. At that time, AWS Lambda seemed like an obvious choice, as many of our Clojure services were already running on Lambda, and we had a deployment process in place. We used …&lt;/p&gt;</summary><content type="html">&lt;p&gt;A few years ago, we moved our machine learning inference endpoints to serverless in order to reduce operations upkeep. At that time, AWS Lambda seemed like an obvious choice, as many of our Clojure services were already running on Lambda, and we had a deployment process in place. We used our continuous integration platform to package our applications, upload them to S3, and then deployed resources with Terraform.&lt;/p&gt;
&lt;p&gt;However, we faced a new challenge as our machine learning models grew beyond the 250 MB Lambda artifact limit. To resolve this, we needed to update our deployment pipeline to either pack our models as container images on Lambda (which has a more generous 10 GB limit) or find a new solution.&lt;/p&gt;
&lt;p&gt;Enter SageMaker Serverless Inference. It offers all the benefits of Lambda with additional conveniences tailored for ML model deployments. One drawback is cost: according to our calculations, 1000 requests on a 2GB memory instance each running for 1 second cost 3.3 cents on Lambda, but 4 cents on SageMaker Serverless Inference. It's a 21% increase, but for a small company like us, the absolute amount is negligible compared to developer time saved.&lt;/p&gt;
&lt;p&gt;In this blog post, I'll guide you through the steps of creating and using a SageMaker Serverless Inference endpoint, which we found to be a seamless experience. We're continuously impressed with the ecosystem developing around the MLOps community.&lt;/p&gt;
&lt;h2&gt;Creating a SageMaker endpoint&lt;/h2&gt;
&lt;p&gt;We decided to try SageMaker Serverless Inference and re-deployed one of our smallest machine learning models on SageMaker using the instructions from this &lt;a href="https://github.com/huggingface/notebooks/blob/main/sagemaker/19_serverless_inference/sagemaker-notebook.ipynb"&gt;Hugging Face notebook&lt;/a&gt;. &lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;create an IAM role with &lt;code&gt;AmazonSageMakerFullAccess&lt;/code&gt; policy on your AWS account (&lt;a href="https://docs.aws.amazon.com/sagemaker/latest/dg/sagemaker-roles.html"&gt;ref&lt;/a&gt;). Let's call it &lt;code&gt;my_sagemaker_execution_role&lt;/code&gt; in this example.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;create a S3 bucket for SageMaker to upload data, models, and logs:&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;sagemaker&lt;/span&gt;

&lt;span class="n"&gt;sess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sagemaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;sagemaker_session_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sagemaker_session_bucket&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;sess&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# set to default bucket if a bucket name is not given&lt;/span&gt;
    &lt;span class="n"&gt;sagemaker_session_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_bucket&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;ensure execution IAM role and default S3 bucket:&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sagemaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_execution_role&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;iam&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;iam&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;role&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;iam&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_role&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RoleName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;my_sagemaker_execution_role&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Role&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Arn&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;sess&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sagemaker&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_bucket&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;sagemaker_session_bucket&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;sagemaker role arn: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;sagemaker bucket: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_bucket&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;sagemaker session region: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;sess&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;boto_region_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;create a &lt;code&gt;HuggingFaceModel&lt;/code&gt; instance by passing in &lt;code&gt;env&lt;/code&gt; as the environment variables. It is important to note that we need to set the optional &lt;code&gt;HF_API_TOKEN&lt;/code&gt; value because our model is private on Hugging Face. Hence, we must pass an API token to the environment for the container to successfully pull our private model.&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;env&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;HF_MODEL_ID&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;MoritzLaurer/multilingual-MiniLMv2-L6-mnli-xnli&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;HF_TASK&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;zero-shot-classification&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# &amp;quot;HF_API_TOKEN&amp;quot;: HF_API_TOKEN,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;huggingface_model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HuggingFaceModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                      &lt;span class="c1"&gt;# configuration for loading model from Hub&lt;/span&gt;
    &lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;                    &lt;span class="c1"&gt;# iam role with permissions to create an Endpoint&lt;/span&gt;
    &lt;span class="n"&gt;transformers_version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;4.26&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# transformers version used&lt;/span&gt;
    &lt;span class="n"&gt;pytorch_version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;1.13&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;       &lt;span class="c1"&gt;# pytorch version used&lt;/span&gt;
    &lt;span class="n"&gt;py_version&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;py39&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;# python version used&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;deploy!&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;serverless_config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ServerlessInferenceConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;memory_size_in_mb&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2048&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_concurrency&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="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;classifier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;huggingface_model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deploy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;serverless_inference_config&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;serverless_config&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;endpoint_name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;my_demo_sagemaker_endpoint&amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We really liked the fact that this can all be done in the same Python environment as our model development. This saves us from context switching between Python, Docker, and Terraform. However, we are unsure about using the &lt;a href="https://github.com/aws/sagemaker-huggingface-inference-toolkit"&gt;SageMaker Hugging Face Inference Toolkit&lt;/a&gt;, which hides some of the nice features of SageMaker endpoints, such as support for production variants to &lt;a href="https://aws.amazon.com/blogs/machine-learning/a-b-testing-ml-models-in-production-using-amazon-sagemaker/"&gt;perform A/B testing on models&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Requesting an inference&lt;/h2&gt;
&lt;p&gt;Wait for the SageMaker endpoint to be up and running. It takes a few minutes. After this, we can make a request. If you followed through further from the upstream instructions linked above, you can make a request immediately using the object returned from &lt;code&gt;HuggingFaceModel.deploy()&lt;/code&gt;. However, in most cases, requests are made from a different process and at a later time. Therefore, we will not have access to the &lt;code&gt;deploy()&lt;/code&gt; object at that point. Fortunately, this is easily achievable.&lt;/p&gt;
&lt;p&gt;We have a couple of options. We can use the HTTP endpoint directly or leverage &lt;a href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/sagemaker-runtime.html"&gt;SageMakerRuntime&lt;/a&gt;. We opted to use SageMakerRuntime because we're already in Python and do not want to go through the process of writing an &lt;a href="https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-auth-using-authorization-header.html"&gt;AWS authentication header for a HTTP request&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;boto3&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;boto3&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;sagemaker-runtime&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# request body for a zero-shot classifier&lt;/span&gt;
&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;inputs&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;A new model offers an explanation for how the Galilean satellites formed around the solar system’s largest world.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;&amp;quot;parameters&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;&amp;quot;candidate_labels&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;space&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;microbiology&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;robots&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;archeology&amp;quot;&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="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;invoke_endpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;EndpointName&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;my_demo_sagemaker_endpoint&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# replace with your endpoint name&lt;/span&gt;
    &lt;span class="n"&gt;ContentType&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;application/json&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;Body&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Body&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This example request body schema is for a zero-shot classifier. Request bodies for other &lt;code&gt;transformers.pipeline&lt;/code&gt; tasks are &lt;a href="https://github.com/huggingface/notebooks/blob/main/sagemaker/11_deploy_model_from_hf_hub/deploy_transformer_model_from_hf_hub.ipynb"&gt;shown in this Hugging Face notebook&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Lastly, note that SageMaker Serverless endpoint has a concurrent invocation &lt;a href="https://docs.aws.amazon.com/sagemaker/latest/dg/serverless-endpoints.html#serverless-endpoints-how-it-works-concurrency"&gt;limit of 200 invokes per endpoint&lt;/a&gt;, whereas Lambda can handle &lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/gettingstarted-limits.html"&gt;tens of thousands&lt;/a&gt;.&lt;/p&gt;</content><category term="computing"/></entry><entry><title>Paul as a manager</title><link href="https://www.quantisan.com/paul-as-a-manager/" rel="alternate"/><published>2022-01-18T00:00:00-05:00</published><updated>2023-04-23T00:00:00-04:00</updated><author><name>Paul Lam</name></author><id>tag:www.quantisan.com,2022-01-18:/paul-as-a-manager/</id><summary type="html">&lt;p&gt;This is a live cheatsheet to give you an idea of what I offer and what I value
as an engineering manager. I'll refine this post over time as I continue to
grow.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;What you can expect from me&lt;/h2&gt;
&lt;p&gt;My job as a manager is to enable you to do …&lt;/p&gt;</summary><content type="html">&lt;p&gt;This is a live cheatsheet to give you an idea of what I offer and what I value
as an engineering manager. I'll refine this post over time as I continue to
grow.&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;What you can expect from me&lt;/h2&gt;
&lt;p&gt;My job as a manager is to enable you to do your best work so that we can grow
the company and your career together. That means for me to understand your
aspirations and to maximize your personal goals with that of the team's. I will
support you by doing the chores that you don't want to do and push you hard on
the work that you want to do.&lt;/p&gt;
&lt;p&gt;There are many ways to getting things done. I don't expect you to adopt to my
way but I do expect mutual understanding of how we each prefer to work. In
particular, I push the team to be conscious of how they spend their time by
focusing on what matters to the product and our customers. On any given week, I
typically spend my time as follows:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;40% on supporting the team.&lt;/em&gt; This could be anything from collaborating on
  technical designs, code reviews, completing unwanted tickets, or just getting
  to know you.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;25% on development and operations.&lt;/em&gt; I pick up any gaps to keep our systems
  running smoothly.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;25% on engineering management.&lt;/em&gt; I regularly consider these 3 aspects:
  organization, project, and technical. For organization management, that means
  working with the product team to ensure that our engineering strategy has
  alignment within the organization. For project management, ensuring that we
  are achieving our sprint goals. For technical management, keeping an eye on
  our technical debt and prioritizing longer term behind-the-scene work.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;10% on personal growth.&lt;/em&gt; I am focusing on growing the skills that I need to
  be successful as a team lead and improving as a human being.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To enable product and customer mindfulness, it is my job to articulate the
bigger picture to you so that you have the context to fully understand the
problems that you're addressing. For example, I often like to collaborate on a
one-pager problem statement to start a project before doing anything else.&lt;/p&gt;
&lt;p&gt;I am tremendously focused and result-driven. I have an affinity for pragmatic,
minimal effort to get just enough of a job done. I sometimes struggle when
working with others who do not share these traits. Some things that are helpful
for you to know about me:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;I have low ego.&lt;/em&gt; I don't know what I'm doing more often than I realize.
  Sometimes I may inadvertently inconvenience you. When that happens, please
  accept my apology and help me improve.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;I am driven by impact.&lt;/em&gt; Whether it is impact to the team, to customers, to
  the society, or to my family, I am proudest when seeing my effort make
  an impact in other people's lives, no matter how small.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;I value open and transparent communication.&lt;/em&gt; I am a straightforward
  communicator. You can count on me to tell you what's on my mind. I will not
  take it personally if you disagree with me, but make sure that it is done
  openly so that we can discuss it.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;I value a well-rested mind.&lt;/em&gt; Nothing can come between me and my sleep
  (except my kids 😅). When I don't sleep well, I don't perform well and get
  grumpy. I practice meditation, bodyweight training, and eating a pescatarian
  diet to keep my mind and body in shape.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;I am a slow thinker.&lt;/em&gt; I'll often ask to sleep on an idea before getting back
  to you with a response.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;I value understanding a problem deeply over clever solutions.&lt;/em&gt; I don't want
  to get ahead of ourselves and end up solving for the wrong thing.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;I value feedback.&lt;/em&gt; Let me know what I've done well and what I can improve. I
  try my best to give others feedback immediately in context as I find that it
  is the most relevant.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;What I expect from you&lt;/h2&gt;
&lt;p&gt;I trust you with the work; we hire smart people to solve complex problems. I
will give you context and feedback, but you need to become the expert on the
problem and own your solution. I expect you to own the work, to build
relationships with our team, and to come to work with an empathatic attitude.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Support each other.&lt;/em&gt; Support can come in many forms, such as offering help
  on a difficult task, providing constructive feedback, or simply lending a
  listening ear. By supporting each other, we can develop a sense of trust and
  camaraderie.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Build relationships.&lt;/em&gt; We seek people who can develop deep relationships with
  others. You can be introvert, extrovert, or anything else, but the ability
  to build and maintain human relationships is key.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Humility.&lt;/em&gt; There are lots of smart, talented people in the world, and
  there’s usually someone smarter than you. If you can’t accept that, you’re
  going to have a tough time listening to others, developing healthy
  relationships, or understanding others.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Make an impact.&lt;/em&gt; You're hired not just to code but to make a sizable
  business impact. Coding is a means to an end.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Balance.&lt;/em&gt; I like my job but I don't tie my identity to it. If you're
  feeling sick or unwell, take some time off. If it's a nice day outside and
  you'd rather not work, take some time off.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Make decisions.&lt;/em&gt; If you are stuck, figure out what you need to unblock
  yourself. When you make a decision, document your rationality and communicate
  to the rest of the team.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Make the work visible.&lt;/em&gt; Make sure that there is a shared understanding of
  the problem that we are solving, any key deliverables have due dates
  established, and you are communicating the progress of the work. Capture
  feedback from stakeholders on your ticket and close the loop.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Ask questions.&lt;/em&gt; If you don’t know something, ask. If something is unclear,
  ask. Asking questions is the best way for us to come to a shared
  understanding as a team.&lt;/li&gt;
&lt;/ul&gt;</content><category term="startup"/></entry><entry><title>Dealing with On-Call Anxiety at a Startup</title><link href="https://www.quantisan.com/dealing-with-on-call-anxiety-at-a-startup/" rel="alternate"/><published>2021-12-25T00:00:00-05:00</published><updated>2021-12-25T00:00:00-05:00</updated><author><name>Paul Lam</name></author><id>tag:www.quantisan.com,2021-12-25:/dealing-with-on-call-anxiety-at-a-startup/</id><summary type="html">&lt;p&gt;Being on-call 24/7 for 18 months straight was one of the most anxiety-inducing responsibilities that I've ever had. I had to make sure that I always had my phone within reach, my laptop accessible, and my mind ready to fix any critical system failure. I sometimes jolted up in …&lt;/p&gt;</summary><content type="html">&lt;p&gt;Being on-call 24/7 for 18 months straight was one of the most anxiety-inducing responsibilities that I've ever had. I had to make sure that I always had my phone within reach, my laptop accessible, and my mind ready to fix any critical system failure. I sometimes jolted up in the middle of the night, believing there was a page on my phone when there wasn't. It wasn't until months later that I realized I had a problem, when my partner accused me of being irritable lately and it dawned on me that she was right.&lt;/p&gt;
&lt;p&gt;If there's one piece of advice that I would give to fellow software developers hesitant about performing on-calls, it is this; &lt;strong&gt;don't do it!&lt;/strong&gt; Don't take a job that requires it if you're feeling anxious about on-calls. We're blessed to be in a hot IT labour market now, so I'm sure that you have plenty of options available. However, if you're proceeding with being on-call (there are some good reasons to) and want to minimize the drain, continue reading.&lt;/p&gt;
&lt;h2&gt;What is it?&lt;/h2&gt;
&lt;h3&gt;On-Call duty&lt;/h3&gt;
&lt;p&gt;I define on-call as being on pager duty for a period of time, expected to respond to urgent system issues. Typically, developers would be on rotation for 7 consecutive days of on-call duty out of every 4-6 weeks. On-call is a common practice with software teams that practice DevOps such that "you build it, you run it" is the expectation. In such teams, on-call duty is part of the development process, as opposed to a traditional separation of development team versus operations team.&lt;/p&gt;
&lt;h3&gt;Anxiety&lt;/h3&gt;
&lt;p&gt;The weird thing about my anxiety is that I find myself less anxious when there are more alerts happening regularly. One of the worst times was when I was on a family vacation for the first time in a long while, and there hadn't been any alert for weeks prior.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Anxiety is distress or uneasiness of mind caused by a fear of danger or misfortune. &lt;a href="https://simple.wikipedia.org/wiki/Anxiety"&gt;Wikipedia&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;According to the American Psychological Association, &lt;a href="https://www.apa.org/topics/stress/anxiety-difference"&gt;anxiety differs from stress&lt;/a&gt; in that anxiety is defined by a persistent worry that won't go away, whereas stress is typically caused by an external trigger. I am anxious about the prospect of our system going down. But once it's down, I get stressed about debugging the failed system while our customers are pounding on our support team. In fact, I gave &lt;a href="https://youtu.be/r_lW1YRycIk"&gt;a talk about the mental stress of debugging production applications&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Why on-call?&lt;/h2&gt;
&lt;p&gt;Motiva A.I. orchestrates digital marketing campaigns for our enterprise clients. Imagine a scenario where if you sign up for a newsletter at Verizon for Business (one of our customers), you'd expect to receive an email from them. Well, not if Motiva is down. Establishing an on-call practice ensures that we can immediately respond to any service interruptions.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;"I can't wait to spend my weekend waiting for a system outage!", said nobody.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Keep in mind that on-call is a last line of defense to keep things running. If high-availability is a business requirement, then that consideration should be an integral part of the product development process right from the start.&lt;/p&gt;
&lt;p&gt;For the individual software developer, on-call can be a helpful practice to hone your debugging and software engineering skills. Bugs happen when an unexpected event pushes your known system into an unknown state. Debugging in such an environment can be a helpful process to push your understanding of your systems and your tools. Moreover, I personally adhere to a pain-driven development mentality. Developers that feel the pain caused by the system they built will build better systems.&lt;/p&gt;
&lt;h2&gt;Dealing with on-call anxiety&lt;/h2&gt;
&lt;h3&gt;As a developer&lt;/h3&gt;
&lt;h4&gt;Seek professional help&lt;/h4&gt;
&lt;p&gt;If you're feeling anxious about your on-call duty, my first advice is to seek a workplace therapist. Second to that, I recommend working through this &lt;a href="https://www.goodreads.com/book/show/11875970-the-anxiety-and-worry-workbook"&gt;Anxiety &amp;amp; Worry Workbook by Clark and Beck&lt;/a&gt;.&lt;/p&gt;
&lt;h4&gt;Accept failures&lt;/h4&gt;
&lt;p&gt;What ultimately helped me mentally is accepting the fact that the worst that can happen if I miss an alarm or two isn't so bad afterall. We might lose a customer or two, which is pretty bad for an enterprise startup with only a handful of customers. But what's worse is getting burnt out and not being able to function anymore. Growing a startup is a marathon, and I shouldn't lose the race over any single bump. Having said that, I understand that the ability to step back is easier said than done. That's why I recommend seeking a therapist or working through that anxiety workbook to ground yourself first. It took me months of actively working on easing my mind before finding my peace.&lt;/p&gt;
&lt;h4&gt;Personal tips&lt;/h4&gt;
&lt;p&gt;While I worked on my mental health, here are some small adjustments that I made to improve my well-being.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;changed my pager tone to something more relaxing&lt;/li&gt;
&lt;li&gt;took it easy when I got paged, resolved it when I could and did not fret about jumping onto my laptop immediately&lt;/li&gt;
&lt;li&gt;blocked out certain hours in our on-call scheduling system during expected slower weeknights so that I could rest easy. Getting woken up in the middle of the night was my biggest anxiety. This gave me peace of mind.&lt;/li&gt;
&lt;li&gt;this wasn't an option for me, but I wish I could have opted out of on-call duties for a couple months when I was going crazy&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Operational tips&lt;/h4&gt;
&lt;p&gt;In terms of operation, it would suck to keep getting paged for the same issue. Make sure that any incident is followed by a retrospective with actions to mitigate the problem in the future. We write up a one-page incident report for each incident, highlighting who it affected, why it happened, and suggestions for future prevention with actual tickets scheduled to be worked on. This give visibility to the product team around delivery expectation.&lt;/p&gt;
&lt;p&gt;A second operational tip I find useful is that during an incident, don't try to do too much. Focus on getting the system back up, or at least the mission critical parts, even if they will be in a limping state. Leave the fixing for the team during work hours. For example, a couple times I would just pause our affected workers before spending any more time debugging. It's easier to explain to our customers that the system stopped working than to explain that it did the wrong thing. This is a debugging tip but it helped with my anxiety because I lowered the bar for what needs to be done substantially, i.e. from fixing to pausing the system gracefully.&lt;/p&gt;
&lt;h3&gt;As a manager&lt;/h3&gt;
&lt;h4&gt;Be wary of your bus factor for mission critical components&lt;/h4&gt;
&lt;p&gt;As a serial technical co-founder, I often find myself taking on-call responsibility for months at a time, simply because there's no one else available. What is different this time is that we had been around for 4 years already when this long on-call stretch started unexpectedly. It happened because our only other senior backend developer left us in mid-2020.&lt;/p&gt;
&lt;p&gt;People come and go. It was my fault for letting myself be stuck in this situation. We have other developers on the team, but they've been working on more interesting and valuable parts of our product. I've let myself be left behind as the only one responsible for our legacy, but still mission critical, components.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;the more mission critical a component is, the more eyes should be on it&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We categorize our application workflows as i) mission critical, ii) essential, or iii) everything else. Only a couple mission critical workflows on our system are allowed to raise on-call alarms outside of work hours. This reduces support fatigue and focuses our attention on what matters.&lt;/p&gt;
&lt;h4&gt;Distributed workforce&lt;/h4&gt;
&lt;p&gt;Motiva has been a remote-first company from the start. One thing we did well early on is to hire across time zones for our engineering team. My previous senior engineer was on the other side of the world from me (not deliberately to such an extreme; suitable partners are just hard to find). That worked wonders for our on-call and support needs. However, that obviously comes with its challenges as we had to rely heavily on asynchronous collaboration methods for actual development work.&lt;/p&gt;
&lt;h4&gt;Cover each other&lt;/h4&gt;
&lt;p&gt;I left myself behind in my case. But having lived through this trauma, I wouldn't put this on anyone else on my team. In addition to sharing knowledge across critical components, nobody should be on-call alone. I make sure that we have a clear escalation procedure in place so that whoever is on-call feels supported.&lt;/p&gt;
&lt;p&gt;In reality for a startup, there's only so many people that can play musical chair in our team of ten. That's why we built semi-automated diagnostic tools for non-developers on our team to help out. The more issues that our customer support team can resolve, the fewer tickets will need to be escalated to our devs.&lt;/p&gt;
&lt;p&gt;On the other end of the escalation ladder, I make sure that our team knows that I'm always available to whoever is on-call. That's the unfortunate responsibility of being the tech lead at a small startup. Luckily, my team only had to call me once outside of my on-call schedule this year.&lt;/p&gt;
&lt;h4&gt;Set clear expectations&lt;/h4&gt;
&lt;p&gt;Just like any team process, we make sure that a new engineering team member taking on-call duty knows that they are not expected to fix the world, knows that it's ok to take it easy, and knows that they have our support. If anything, err on the side of escalating early for their first few incidents so that we can pair on the incident with them.&lt;/p&gt;
&lt;p&gt;Early on in this post, I mentioned that I wouldn't recommend taking on a job requiring on-call. If you don't think that on-call is for you, then don't do it. However, this is an unavoidable responsiblity if you want to work in a small startup. Something mission critical will eventually fail, and somebody needs to fix it. For such times, I'd rather that we have a clear on-call schedule with clear expectations than scramble to fight the fire.&lt;/p&gt;
&lt;h4&gt;Build robust and maintainable systems, but we don't have time!&lt;/h4&gt;
&lt;p&gt;The lasting remedy for my on-call anxiety was building confidence in our systems so that I know we'll be fine if I'm unavailable. We dug ourselves out of that technical debt. For two quarters in 2021, we directed all engineering resources to &lt;a href="/took-me-4-years-to-realize-wed-been-orchestrating-workflows"&gt;rebuilding the most vulnerable parts of our system as high-availability workflows&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Developers that feel the pain caused by the system they built will build better systems.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Hold on a second! Did I mention that I got stuck with this endless on-call stint because one of our two backend developers left? Between scaling our system to handle the 400% increase in system load this year, hiring for a replacement, building new features, reviewing code, mentoring, ... how did we find time to address this rotting technical debt?&lt;/p&gt;
&lt;p&gt;Engineering management in a startup setting is still something that I'm learning even after 10 years. In this case, I couldn't take it anymore and had to convince the rest of the company that we had to stop everything to focus on addressing some technical debt known for years. The fact that it got to the point where my health was at stake until I prioritized is a failure on my part. Having said that, I learned a lot over these past few years in terms of maintaining an enterprise system and engineering management. I've been heads-down building Motiva for the past 6 years. If you find this post useful, &lt;a href="/blog.html"&gt;take a look at my blog&lt;/a&gt;. I plan to write regularly over 2022 to capture some of my learnings.&lt;/p&gt;</content><category term="startup"/></entry><entry><title>Orchestrating Pizza-Making: A Tutorial for AWS Step Functions with Stepwise</title><link href="https://www.quantisan.com/orchestrating-pizza-making-a-tutorial-for-aws-step-functions-with-stepwise/" rel="alternate"/><published>2021-10-26T00:00:00-04:00</published><updated>2021-10-26T00:00:00-04:00</updated><author><name>Paul Lam</name></author><id>tag:www.quantisan.com,2021-10-26:/orchestrating-pizza-making-a-tutorial-for-aws-step-functions-with-stepwise/</id><summary type="html">&lt;p&gt;In this post, I will step through creating a AWS Step Functions state machine using Stepwise for a hypothetical pizza-making workflow. Before we get to that though, in case you're wondering why would you care about AWS Step Functions, or application workflow orchestration in general, I discussed our &lt;a href="/took-me-4-years-to-realize-wed-been-orchestrating-workflows/"&gt;4-year journey …&lt;/a&gt;&lt;/p&gt;</summary><content type="html">&lt;p&gt;In this post, I will step through creating a AWS Step Functions state machine using Stepwise for a hypothetical pizza-making workflow. Before we get to that though, in case you're wondering why would you care about AWS Step Functions, or application workflow orchestration in general, I discussed our &lt;a href="/took-me-4-years-to-realize-wed-been-orchestrating-workflows/"&gt;4-year journey towards an message-based, orchestrated architecture&lt;/a&gt; previously.&lt;/p&gt;
&lt;p&gt;Let's make some pizzas! Suppose these are the 4 steps to making a tasty Pizza Margherita:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;make dough&lt;/li&gt;
&lt;li&gt;make sauce&lt;/li&gt;
&lt;li&gt;put ingredients on dough&lt;/li&gt;
&lt;li&gt;bake&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And suppose you built some robots to perform each of the step. How would you orchestrate your robots to work together? Well, one option is to use an application workflow orchestration engine. We will be using AWS Step Functions via Stepwise.&lt;/p&gt;
&lt;h2&gt;Why Stepwise?&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/Motiva-AI/stepwise"&gt;Stepwise is our open source library to manage Step Functions using Clojure&lt;/a&gt;. Why would you use Stepwise instead of an infrastructure as code (IoC) tool, like CDK or Terraform, to manage Step Functions?&lt;/p&gt;
&lt;p&gt;If your workers are written in Clojure, then using Stepwise would help keep your state machine definitions, which is essentially business logic, and application code in one place. Thus, making your development and maintenance experience more seamless. And as you will see in this post, Stepwise has a few features that will make your time working with Step Functions easier too.&lt;/p&gt;
&lt;h2&gt;Creating a state machine&lt;/h2&gt;
&lt;p&gt;Let's start easy and create a simple Step Functions (SFN) state machine. Launch a REPL from your local &lt;a href="https://github.com/Motiva-AI/stepwise"&gt;Stepwise repository&lt;/a&gt;,&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;paul@demo:~/stepwise$&lt;span class="w"&gt; &lt;/span&gt;lein&lt;span class="w"&gt; &lt;/span&gt;repl

...

stepwise.dev-repl&lt;span class="o"&gt;=&lt;/span&gt;&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Paste the following code onto your REPL to create your first state machine on SFN. This assume that your environment is already setup with &lt;a href="https://aws.amazon.com/cli/"&gt;awscli&lt;/a&gt; and that your account has permission to manage SFN.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;stepwise.core&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;stepwise&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stepwise/ensure-state-machine&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c1"&gt;;; name of your state machine&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:start-at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:make-dough&lt;/span&gt;

&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="ss"&gt;:states&lt;/span&gt;
&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:make-dough&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:type&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:task&lt;/span&gt;
&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="ss"&gt;:resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza/make-dough&lt;/span&gt;
&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="ss"&gt;:next&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:make-sauce&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="ss"&gt;:make-sauce&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:type&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:task&lt;/span&gt;
&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="ss"&gt;:resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza/make-sauce&lt;/span&gt;
&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="ss"&gt;:next&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:put-ingredients-on-dough&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="ss"&gt;:put-ingredients-on-dough&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:type&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:task&lt;/span&gt;
&lt;span class="w"&gt;                               &lt;/span&gt;&lt;span class="ss"&gt;:resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza/put-ingredients-on-dough&lt;/span&gt;
&lt;span class="w"&gt;                               &lt;/span&gt;&lt;span class="ss"&gt;:next&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:bake&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="ss"&gt;:bake&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:type&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:task&lt;/span&gt;
&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="ss"&gt;:resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza/bake&lt;/span&gt;
&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="ss"&gt;:end&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}}})&lt;/span&gt;

&lt;span class="c1"&gt;; output&lt;/span&gt;
&lt;span class="c1"&gt;; &amp;quot;arn:aws:states:us-west-2:111111111111:stateMachine:make-pizza&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We called &lt;code&gt;ensure-state-machine&lt;/code&gt; to create a state machine named &lt;code&gt;:make-pizza&lt;/code&gt;, specifying 4 states. The states are connected serially with the &lt;code&gt;:next&lt;/code&gt; keyword. For information on the important concepts of SFN, AWS provides &lt;a href="https://docs.aws.amazon.com/step-functions/latest/dg/how-step-functions-works.html"&gt;helpful documentation here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://www.quantisan.com/images/2021/sfn-make-pizza-1.png" alt="make pizza step functions, first version" width="200px" style="float: right" /&gt;&lt;/p&gt;
&lt;p&gt;To see what you created on AWS, navigate to your SFN dashboard on AWS and find your &lt;code&gt;make-pizza&lt;/code&gt; state machine. The diagram below (from my SFN dashboard) shows the corresponding layout of your &lt;code&gt;make-pizza&lt;/code&gt; state machine.&lt;/p&gt;
&lt;p&gt;This state machine doesn't do anything yet. We still need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;assign workers to each of the step&lt;/li&gt;
&lt;li&gt;start an execution&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Assign workers to the steps&lt;/h2&gt;
&lt;p&gt;This is where the magic of Stepwise comes into play. If you're using an IoC tool to manage Step Functions, then you'd have to either create a Step Functions Activity or a AWS Lambda to run your application logic. But with Stepwise, we simply use the &lt;code&gt;stepwise/start-workers!&lt;/code&gt; function to subscribe your Clojure functions to the SFN state machine.&lt;/p&gt;
&lt;p&gt;Note in the code block below that the key names match the resource names defined in the state machine. Stepwise will automatically create the Step Functions Activity and run your workers in a &lt;code&gt;core.async&lt;/code&gt; background process to respond to your Step Functions jobs.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;def &lt;/span&gt;&lt;span class="nv"&gt;workers&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stepwise/start-workers!&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="c1"&gt;;; key names must match one of your state machine resources&lt;/span&gt;
&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza/make-dough&lt;/span&gt;
&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;println &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;making dough...&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza/make-sauce&lt;/span&gt;
&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;println &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;making sauce...&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza/put-ingredients-on-dough&lt;/span&gt;
&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;println &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;putting on ingredients...&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza/bake&lt;/span&gt;
&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;println &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;baking...&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="ss"&gt;:done&lt;/span&gt;&lt;span class="p"&gt;)}))&lt;/span&gt;

&lt;span class="c1"&gt;; output&lt;/span&gt;
&lt;span class="c1"&gt;; #&amp;#39;stepwise.dev-repl/workers&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2&gt;Run an execution&lt;/h2&gt;
&lt;p&gt;WARNING: there might be &lt;a href="https://aws.amazon.com/step-functions/pricing/"&gt;some AWS cost incurring&lt;/a&gt; to follow through with this tutorial from this point forward.&lt;/p&gt;
&lt;p&gt;We have our pizza-making state machine defined. We have our pizza-making workers ready. Let's run this thing once on AWS.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stepwise/start-execution!!&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:input&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{}})&lt;/span&gt;

&lt;span class="c1"&gt;; output&lt;/span&gt;
&lt;span class="c1"&gt;;{:arn &amp;quot;arn:aws:states:us-west-2:111111111111:execution:make-pizza:242630d3-1b1c-4a76-b3c0-1bac266c29b2&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;; :input {}&lt;/span&gt;
&lt;span class="c1"&gt;; :name &amp;quot;242630d3-1b1c-4a76-b3c0-1bac266c29b2&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;; :output &amp;quot;done&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;; :start-date #inst &amp;quot;2021-10-25T01:28:07.873-00:00&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;; :state-machine-arn &amp;quot;arn:aws:states:us-west-2:111111111111:stateMachine:make-pizza&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;; :status &amp;quot;SUCCEEDED&amp;quot;&lt;/span&gt;
&lt;span class="c1"&gt;; :stop-date #inst &amp;quot;2021-10-25T01:28:08.807-00:00&amp;quot;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;For Stepwise API, &lt;code&gt;*-!!&lt;/code&gt; suffix to a function name denotes blocking calls. &lt;code&gt;*-!&lt;/code&gt; denotes non-blocking calls. Similar to &lt;code&gt;core.async&lt;/code&gt; convention. We're using the blocking version of &lt;code&gt;start-execution&lt;/code&gt; here. There is a corresponding non-blocking &lt;code&gt;start-execution!&lt;/code&gt; available.&lt;/p&gt;
&lt;p&gt;You might have noticed that we passed in an empty map as input. For sake of simplicity, we're ignoring input and output entirely in this tutorial. Everybody is getting the same Pizza Margherita.&lt;/p&gt;
&lt;h2&gt;Operational features&lt;/h2&gt;
&lt;p&gt;As discussed in my previous post, one of the main benefits of &lt;a href="/took-me-4-years-to-realize-wed-been-orchestrating-workflows/"&gt;using an application workflow orchestration engine instead of rolling your own&lt;/a&gt; are the provided operational features. I'll show a couple examples that I find the most useful. Execution history and error handling.&lt;/p&gt;
&lt;h3&gt;Execution history&lt;/h3&gt;
&lt;p&gt;Step Functions keep track of every one of your executions. One of the ways to access that logs is to use the AWS Console. On the &lt;code&gt;make-pizza&lt;/code&gt; state machine view, you can see your list of execution history.&lt;/p&gt;
&lt;p&gt;&lt;img alt="AWS Console execution history" src="https://www.quantisan.com/images/2021/aws-sfn-console-execution-history.png"&gt;&lt;/p&gt;
&lt;p&gt;Not only do you get a high-level list of executions, you have access to detailed history of each of your execution with input and output information for each step within the state machine, as well as information of each state transitions.&lt;/p&gt;
&lt;h3&gt;Handling errors&lt;/h3&gt;
&lt;p&gt;Our robots can be prone to failure. What happens if one of them fail? Let's say the dough-making robot fails some of the time. One of the ways to handle that is to retry the task upon failure. Let's add a retry logic to our state machine definition.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:make-dough&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:type&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:task&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="ss"&gt;:resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza/make-dough&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="ss"&gt;:retry&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="ss"&gt;:error-equals&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:States.TaskFailed&lt;/span&gt;
&lt;span class="w"&gt;                       &lt;/span&gt;&lt;span class="ss"&gt;:interval-seconds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="w"&gt;                       &lt;/span&gt;&lt;span class="ss"&gt;:max-attempts&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
&lt;span class="w"&gt;              &lt;/span&gt;&lt;span class="ss"&gt;:next&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:make-sauce&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We've configured &lt;code&gt;:make-pizza/make-dough&lt;/code&gt; worker to retry a second attempt after 30 seconds if a task fail.&lt;/p&gt;
&lt;h2&gt;State machine control flow&lt;/h2&gt;
&lt;p&gt;Guess what? Your Pizza Margheritas are in high demand. We want to save some time in your workflow by making the dough and sauce simultaneously. We can do that with a Parallel state provided by SFN.&lt;/p&gt;
&lt;p&gt;Putting all of these together, here is our final state machine definition with re-try for &lt;code&gt;make-dough&lt;/code&gt; worker and simultaneous runs for &lt;code&gt;make-dough&lt;/code&gt; and &lt;code&gt;make-sauce&lt;/code&gt; workers.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stepwise/ensure-state-machine&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:start-at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:make-base&lt;/span&gt;

&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="ss"&gt;:states&lt;/span&gt;
&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:make-base&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:type&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:parallel&lt;/span&gt;
&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:branches&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="ss"&gt;:start-at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:make-dough&lt;/span&gt;
&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="ss"&gt;:states&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:make-dough&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:type&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:task&lt;/span&gt;
&lt;span class="w"&gt;                             &lt;/span&gt;&lt;span class="ss"&gt;:resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza/make-dough&lt;/span&gt;
&lt;span class="w"&gt;                             &lt;/span&gt;&lt;span class="ss"&gt;:retry&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="ss"&gt;:error-equals&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:States.TaskFailed&lt;/span&gt;
&lt;span class="w"&gt;                                         &lt;/span&gt;&lt;span class="ss"&gt;:interval-seconds&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="w"&gt;                                         &lt;/span&gt;&lt;span class="ss"&gt;:max-attempts&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;
&lt;span class="w"&gt;                             &lt;/span&gt;&lt;span class="ss"&gt;:end&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}}}&lt;/span&gt;

&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:start-at&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:make-sauce&lt;/span&gt;
&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="ss"&gt;:states&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:make-sauce&lt;/span&gt;
&lt;span class="w"&gt;                            &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:type&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:task&lt;/span&gt;
&lt;span class="w"&gt;                             &lt;/span&gt;&lt;span class="ss"&gt;:resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza/make-sauce&lt;/span&gt;
&lt;span class="w"&gt;                             &lt;/span&gt;&lt;span class="ss"&gt;:end&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}}}]&lt;/span&gt;
&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:next&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:put-ingredients-on-dough&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="ss"&gt;:put-ingredients-on-dough&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:type&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:task&lt;/span&gt;
&lt;span class="w"&gt;                               &lt;/span&gt;&lt;span class="ss"&gt;:resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza/put-ingredients-on-dough&lt;/span&gt;
&lt;span class="w"&gt;                               &lt;/span&gt;&lt;span class="ss"&gt;:next&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:bake&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="ss"&gt;:bake&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:type&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:task&lt;/span&gt;
&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="ss"&gt;:resource&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza/bake&lt;/span&gt;
&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="ss"&gt;:end&lt;/span&gt;&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nv"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}}})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src="https://www.quantisan.com/images/2021/sfn-make-pizza-2.png" alt="make pizza step functions, second version with control flow" width="250px" style="float: right" /&gt;&lt;/p&gt;
&lt;p&gt;This definition is getting a bit involved. The flow diagram representation is easier to understand what's happening.&lt;/p&gt;
&lt;h2&gt;Worker concurrency&lt;/h2&gt;
&lt;p&gt;Let's scale this workflow further. You built more robots to make pizzas. Let's increase the number of workers for this state machine to make use of the increased capacity. You can define worker concurrency with Stepwise when you call &lt;code&gt;start-workers!&lt;/code&gt; by passing in a second argument map containing a &lt;code&gt;task-concurrency&lt;/code&gt; key like so.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;def &lt;/span&gt;&lt;span class="nv"&gt;workers&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;stepwise/start-workers!&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:make-pizza/make-dough&lt;/span&gt;
&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;println &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;making dough...&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza/make-sauce&lt;/span&gt;
&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;println &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;making sauce...&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza/put-ingredients-on-dough&lt;/span&gt;
&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;println &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;putting on ingredients...&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza/bake&lt;/span&gt;
&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;fn &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;_&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;println &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;baking...&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="ss"&gt;:done&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:task-concurrency&lt;/span&gt;
&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="ss"&gt;:make-pizza/make-dough&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza/make-sauce&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza/put-ingredients-on-dough&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="ss"&gt;:make-pizza/bake&lt;/span&gt;&lt;span class="w"&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;Suppose your ensemble of robots can make 2 doughs, 2 sauces, and bake 4 pizzas at a time. We've set the worker concurrency to reflect that capacity. So now, any simultaneous executions happening will try to fill up those workers before queueing starts.&lt;/p&gt;
&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;In this tutorial, we've set up a &lt;code&gt;make-pizza&lt;/code&gt; state machine on AWS Step Functions using Stepwise. We demonstrated the core API of Stepwise: &lt;code&gt;ensure-state-machine&lt;/code&gt;, &lt;code&gt;start-workers!&lt;/code&gt;, and &lt;code&gt;start-execution!!&lt;/code&gt;. We also ran through some features of Step Functions: handling errors and parallel flow. As well as a couple features of Stepwise such as seamless state machine and workers development, and worker concurrency.&lt;/p&gt;
&lt;p&gt;If you want to find out more about Step Functions or Stepwise, check out the &lt;a href="https://aws.amazon.com/step-functions/"&gt;AWS Step Functions web site&lt;/a&gt; or &lt;a href="https://github.com/Motiva-AI/stepwise"&gt;Stepwise repository&lt;/a&gt;.&lt;/p&gt;</content><category term="computing"/></entry><entry><title>Took me 4 years to realize we'd been orchestrating workflows</title><link href="https://www.quantisan.com/took-me-4-years-to-realize-wed-been-orchestrating-workflows/" rel="alternate"/><published>2021-10-10T00:00:00-04:00</published><updated>2021-10-13T00:00:00-04:00</updated><author><name>Paul Lam</name></author><id>tag:www.quantisan.com,2021-10-10:/took-me-4-years-to-realize-wed-been-orchestrating-workflows/</id><summary type="html">&lt;p&gt;It's been more than 5 years since we founded Motiva. What we do fundamentally has been the same; helping companies send the most useful messages to the right people at the most convenient time. In this post, I'd like to discuss workflow orchestration and why I wish I'd known about …&lt;/p&gt;</summary><content type="html">&lt;p&gt;It's been more than 5 years since we founded Motiva. What we do fundamentally has been the same; helping companies send the most useful messages to the right people at the most convenient time. In this post, I'd like to discuss workflow orchestration and why I wish I'd known about it earlier.&lt;/p&gt;
&lt;p&gt;Here's a screenshot of one of our workflow execution histories (an execution = a specific workflow run). Not only is there status information to each step, but we also have a record of the input and output for every step. This granular tracking is valuable for diagnosis and the high-level visibility makes designing complex workflows managerable. We process hundreds of executions per hour and each execution handles up to a couple millions of small decisions by interacting with internal and external systems.&lt;/p&gt;
&lt;p&gt;&lt;img alt="SFN sample execution view" src="https://www.quantisan.com/images/2021/sfn_sample_execution.png"&gt;&lt;/p&gt;
&lt;p&gt;Let's run through the evolution of our Process Batch workflow from the beginning to illustrate how we got to this point, using a sample of the typical kind of job we regularly perform at Motiva.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The task is to send certain emails to particular email addresses (contacts).&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We aren't MailChimp though. One of our product's selling points is that our machine learning can determine the &lt;em&gt;most useful*&lt;/em&gt; messages for the right people. Thus, identifying which particular emails to send to whom and at what time is the real work of this Process Batch workflow.&lt;/p&gt;
&lt;p&gt;We query our machine-learning engine to map the "most likely useful" asset to each target. This defines what needs to be done for each batch.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;From the list of email addresses, we need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;know when to send to each contact&lt;/li&gt;
&lt;li&gt;know which email variation to send to each contact&lt;/li&gt;
&lt;li&gt;send out all the emails accordingly&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;p&gt;Typically, this list of email addresses are contacts that either signed up for a particular newsletter or registered for a particular product. Thus, they are contacts with a history of engaging with our clients; providing a history of data for our machine-learning models to optimize on... but that's another story.&lt;/p&gt;
&lt;p&gt;This Process Batch workflow comprises 3 activities:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;scheduler -- enables you to do something at a specific time&lt;/li&gt;
&lt;li&gt;asset picker -- chooses the email to be sent for each individual&lt;/li&gt;
&lt;li&gt;sending out the emails&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;Quick and dirty first version&lt;/h2&gt;
&lt;p&gt;We built our first system in a few weeks as a service-oriented architecture. The system includes an email Campaign Orchestrator service, a Machine-Learning service, and an Email-Sender service, which all communicate via HTTP. I added a cron job onto our server to ping the Campaign Orchestrator service every few minutes. On receiving this heartbeat ping, the Campaign Orchestrator service would query our database table of schedules and create batches ready to be sent.&lt;/p&gt;
&lt;p&gt;Here's a sequence diagram of the interactions between the services for this Process Batch workflow.&lt;/p&gt;
&lt;p&gt;&lt;img alt="sequence diagram" src="https://www.quantisan.com/images/2021/2021-10-01-workflow-orchestration-phase-1.png"&gt;&lt;/p&gt;
&lt;p&gt;This 2016 version didn't last long though. The biggest problem was that since actions were performed in an impromptu manner, we had no observability when things weren't working for any reason. To diagnose which branch of logic a workflow took, I had to match our logs with database records to step through logical forks one by one. It was a slow and tedious chore. We had many basic reliability issues in our early days. I still recall having to debug a production problem after a few drinks around Christmas time while my CEO and one of our clients was waiting on the line -- &lt;a href="https://clojurenorth.com/paul-lam.html"&gt;a stressful experience that I gave a talk on last year&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;Materialize batches for observability&lt;/h2&gt;
&lt;p&gt;Naturally, the improvement needed was to materialize all the impromptu events as database records ahead of time and determine everything beforehand. It meant, for example, that instead of doing a query of the system at each time interval, working out what needs to be done and processing the batch on the spot, we do all that at the beginning of a campaign once and for all. What this translates to in terms of easier diagnostic is that we can query our database for what decision was made at any point of interest in a workflow. It's a deterministic system that goes straight for the win.&lt;/p&gt;
&lt;p&gt;&lt;img alt="sequence diagram" src="https://www.quantisan.com/images/2021/2021-10-01-workflow-orchestration-phase-2.png"&gt;&lt;/p&gt;
&lt;p&gt;There are two major changes to this second version.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;we moved the scheduler out of the Process Batch workflow to run only once per campaign&lt;/li&gt;
&lt;li&gt;all campaign batches are determined and materialized in advance&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In this scenario, our Process Batch workflow has split into two workflows. The first part is the Activation workflow, which happens once and only once when the user finishes configuring a campaign. During this Activation workflow, the Campaign Orchestrator assign send-time schedules to every contacts; for example, Contact A should be emailed at 9am and Contact B should be emailed at 3pm.&lt;/p&gt;
&lt;p&gt;Our Process Batch v2 workflow is still regularly triggered by a cron job. But instead of needing to determine which batches to send impromptu, we simply query our database for these pre-scheduled batches that are ready to be sent.&lt;/p&gt;
&lt;p&gt;This second version lasted a few more months. But as our product was getting more use from our customers, we needed to scale the workflow. Particularly as some batches could take many minutes to process, whereas others would take only seconds. We needed a way to ensure that all batches get processed in reasonable time.&lt;/p&gt;
&lt;h2&gt;Message-based architecture for scaling&lt;/h2&gt;
&lt;p&gt;We chose to add a queue to our Process Batch workflow in order to choreograph batch worker allocation. Each Process Batch request was created as an independent message and put into the Process This Batch queue. When we needed to process a batch, we sent a Process Batch message with the required data to the queue. To respond to these messages in the queue, we had N number of deployed workers in the Campaign Orchestrator service subscribing to the queue.&lt;/p&gt;
&lt;p&gt;That's a lot of words. Perhaps it's easier with a diagram. Note the addition of the Process This Batch queue.&lt;/p&gt;
&lt;p&gt;&lt;img alt="sequence diagram" src="https://www.quantisan.com/images/2021/2021-10-01-workflow-orchestration-phase-3.png"&gt;&lt;/p&gt;
&lt;h4&gt;Scaling&lt;/h4&gt;
&lt;p&gt;Scaling was simply a matter of deploying more workers to increase the N value. Luckily, we kept our components and services decoupled, so scaling was easy. Additional benefits from placing our Process Batch workflow in a queue were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;failure handling&lt;/li&gt;
&lt;li&gt;high-level observability&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Failure handling&lt;/h4&gt;
&lt;p&gt;In the previous v2 version of this workflow, we needed to explicitly scan our database for failed batches in order to recover them. With this queue-based v3 workflow, the corresponding message for a failed batch was simply not acknowledged and the message would automatically be put back into the queue to be processed again. The big caveat here is that batches need to be idempotent so that processing the same batch more than once doesn't cause any unexpected behaviour.&lt;/p&gt;
&lt;h4&gt;Observability&lt;/h4&gt;
&lt;p&gt;With Process Batch requests going through an actual queue within our infrastructure (as opposed to an in-process queue), we achieved high-level monitoring. We configured various alarms in our queue metrics (i.e., CloudWatch alarms on AWS SQS metrics) such as "age of oldest message in the queue in a 5-min period", "number of messages sent in a period", and "number of messages received in a period". Such alarms provided an overview of the health of our Process Batch queue, and by extension, the health of our Process Batch workflow. These metrics replaced a few database queries that we used to run to get similar data.&lt;/p&gt;
&lt;p&gt;Meanwhile, as the demand for our product grew, we scaled our other workflows by using this setup and created numerous queues in the process. Over the following couple of years, we moved to an entirely message-based architecture. Below is our extended Process Batch v3.1 workflow where the intra-system calls -- both to the Machine-Learner and the Email Sender -- were communicated through their own respective queues.&lt;/p&gt;
&lt;p&gt;&lt;img alt="sequence diagram" src="https://www.quantisan.com/images/2021/2021-10-01-workflow-orchestration-phase-4.png"&gt;&lt;/p&gt;
&lt;p&gt;The database journal writes for each worker constitute a small but significant addition to the above graph. What's shown here is a simplistic illustration. This database action could mean writing actual database journal records to log job status, or more often, the transactional reversion of database writes if a job fails. These actions are important because we spent a lot of time accounting for steps failing in different parts of a workflow. Keeping track of each step facilities easier diagnostics.&lt;/p&gt;
&lt;p&gt;A more accurate view of our Process Batch v3.1 workflow looks like this:&lt;/p&gt;
&lt;p&gt;&lt;img alt="decision diagram" src="https://www.quantisan.com/images/2021/2021-10-01-workflow-orchestration-phase-4a-decision-flow.png"&gt;&lt;/p&gt;
&lt;p&gt;Using this messaged-based architecture, we handled a growth of 400% serving a handful of Fortune 500 companies, supported by a team of only 2 backend engineers. Even so, this version still wasn't good enough. Our product was still experiencing unexpected issues every few weeks.&lt;/p&gt;
&lt;p&gt;If only we could have finer-grained control of our workflows to account for even more state transition cases, we thought. But as you can see, even this simplistic decision diagram above is already a bit confusing even after omitting a few details including re-try limits and parallelization. Our increasing reliance on managing states between asynchronous queue workers was getting tedious and hard to manage. We were violating the 'don't repeat yourself' principle with our boilerplate operational application code and system infrastructure.&lt;/p&gt;
&lt;p&gt;The Send Emails validation step in the above decision-flow diagram, for example, creates a decision path in our Process Batch workflow. If all emails were successfully sent, we proceed normally, but if any of the emails failed to send, we break off to an alternate path to re-try only the unsent emails. I coded this path with 2 separate workers over 2 separate queues; one paired worker and queue for the happy path, and a second pair for the failed path. Every extra decision in our workflow therefore became yet another worker and queue pair. With this naive implemenation, our need to impose finer-grained control of our workflows was becoming unmanageable.&lt;/p&gt;
&lt;h2&gt;Application Workflow Orchestration is a solved problem&lt;/h2&gt;
&lt;p&gt;What we needed was an application workflow orchestration engine. All those distinct queues we created to cover every decision path and the database journals used to keep track of step statuses are boilerplate logic for solving a known problem. Take a look at &lt;a href="https://aws.amazon.com/step-functions"&gt;AWS Step Functions&lt;/a&gt; for an example of a workflow orchestration engine as a service. It's also what we ended up using for production.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;AWS Step Functions is a low-code visual workflow service used to orchestrate AWS services, automate business processes, and build serverless applications. Workflows manage failures, retries, parallelization, service integrations, and observability so that developers can focus on higher-value business logic.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;These feature -- manage failures, retries, ... and observability -- match the exact operational requirements that I've been building up to in this blog post and in our actual system.&lt;/p&gt;
&lt;p&gt;It's worth pointing out that there are &lt;a href="http://meirwah.github.io/awesome-workflow-engines/"&gt;plenty of other workflow orchestration engines to choose from&lt;/a&gt;. AWS Step Functions just happens to fit our particular use cases.&lt;/p&gt;
&lt;h4&gt;Failure handling as decision paths&lt;/h4&gt;
&lt;p&gt;Below is a screenshot from the AWS Step Functions UI on one of our workflow definitions. Defining a workflow is simply a matter of writing a JSON definition (in our case, we use &lt;a href="https://github.com/Motiva-AI/stepwise"&gt;Stepwise, an open-source Clojure library on AWS Step Functions&lt;/a&gt;) to specify the state transition logic. All the boilerplate operational aspects, including creating implicit queues and tracking execution history (see next section), are taken care of for you. In this diagram alone, I would have needed to create 7 queues and 8 sets of status records for this workflow. I can't stress enough how the freedom from needing to explicitly create an endless number of queues and journal entries is a huge time saver!&lt;/p&gt;
&lt;p&gt;&lt;img alt="SFN export workflow" src="https://www.quantisan.com/images/2021/sfn_export_workflow_diagram.png"&gt;&lt;/p&gt;
&lt;p&gt;Along with a JSON workflow definition to create this state machine, you have to assign workers to each of the steps. We simply re-used our existing workers because our system had alredy been implemented as a message-based architecture.&lt;/p&gt;
&lt;p&gt;Once you have your workflow definition and workers assigned, Step Functions will automatically route output from one worker as input to the next according to the logic in your workflow definition. Routing options such as timeouts, re-tries, and route by output values are all available.&lt;/p&gt;
&lt;h4&gt;Monitoring 2.0: History for each execution&lt;/h4&gt;
&lt;p&gt;High-level CloudWatch metrics such as step failure counts over time are provided. These metrics are equivalent to the queue metrics mentioned previously. Moreover, Step Functions also provide a complete step-by-step history for each individual execution! This is valuable as it enables non-developer on the team to inspect particular executions for customer support.&lt;/p&gt;
&lt;p&gt;I hope this has been a useful summary of our application business workflow management journey. In the next post, we will &lt;a href="/orchestrating-pizza-making-a-tutorial-for-aws-step-functions-with-stepwise/"&gt;dive into our Stepwise library to illustrate how to set up and execute an AWS Step Functions workflow&lt;/a&gt;.&lt;/p&gt;</content><category term="computing"/></entry><entry><title>We are hiring a ClojureScript developer in Africa</title><link href="https://www.quantisan.com/we-are-hiring-a-clojurescript-developer-in-africa/" rel="alternate"/><published>2017-05-29T00:00:00-04:00</published><updated>2017-05-29T00:00:00-04:00</updated><author><name>Paul Lam</name></author><id>tag:www.quantisan.com,2017-05-29:/we-are-hiring-a-clojurescript-developer-in-africa/</id><summary type="html">&lt;p&gt;The InGrower Mobile App has been put to the test in the hands of a few smallholder
chicken farmers in Mozambique since January 2016. We have learnt what's needed
and is proceeding to the next stage of development. That's why we're looking to
hire a ClojureScript developer to build the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;The InGrower Mobile App has been put to the test in the hands of a few smallholder
chicken farmers in Mozambique since January 2016. We have learnt what's needed
and is proceeding to the next stage of development. That's why we're looking to
hire a ClojureScript developer to build the next version of our mobile app.&lt;/p&gt;
&lt;p&gt;One of my longer term goals is to grow a distributed technical team
in East Africa that can tackle various social impact challenges in the region.
This first hire will play an important role in setting the direction for the team.&lt;/p&gt;
&lt;p&gt;&lt;a href="http://bit.ly/ingrower_clojurescript"&gt;Link to job description&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;InGrower Mobile&lt;/strong&gt; is a side business that I've been working on since September
2015. We are an early stage social enterprise in the I.C.T. for Development
(smallholder farming) space. InGrower Mobile came about from my co-founder's
experience operating an agri-business incubator in Mozambique since 2010. We
admit unemployed individuals, then provide training and support to help them start
their own agriculture business to make money. For InGrower Mobile, our product
is a mobile application that takes what we learned from the past 7 years
running our agri-business incubator and applying them online to make our
training and support available for a wider audience across East Africa.&lt;/p&gt;</content><category term="startup"/><category term="clojure"/></entry><entry><title>A Call to Scandinavian Investigative Journalists</title><link href="https://www.quantisan.com/a-call-to-scandinavian-investigative-journalists/" rel="alternate"/><published>2016-11-15T10:00:00-05:00</published><updated>2016-11-15T10:00:00-05:00</updated><author><name>Paul Lam</name></author><id>tag:www.quantisan.com,2016-11-15:/a-call-to-scandinavian-investigative-journalists/</id><summary type="html">&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt; Are you a journalist interested in understanding online threats to your
operations as a journalist and protecting the network of people that have given
you their trust -- colleagues, informants, collaborators, partners, and social
connections? I would love to talk to you.&lt;/p&gt;
&lt;p&gt;With both Brexit and Trump this year …&lt;/p&gt;</summary><content type="html">&lt;p&gt;&lt;strong&gt;tl;dr&lt;/strong&gt; Are you a journalist interested in understanding online threats to your
operations as a journalist and protecting the network of people that have given
you their trust -- colleagues, informants, collaborators, partners, and social
connections? I would love to talk to you.&lt;/p&gt;
&lt;p&gt;With both Brexit and Trump this year, for better or for worse, it's clear that
extreme actions are gaining popularity with the people. This saying comes to
mind, "most of the evil in this world is done by people with good intentions."
Things are most likely going to be fine. But in the 1% chance that shit is
going to hit the fan, maintaining freedom of speech is going to be critical.
We've seen from history (as recently as Tunisia, Gaddafi) that freedom of
information is one of the first to go when human rights are trampled.&lt;/p&gt;
&lt;p&gt;My colleagues in Silicon Valley is co-hosting a digital security workshop with
Bloomberg in the US. I am looking to start something similar in Scandinavia. At
the moment, I'm looking to interview investigative journalists on what
questions or concerns you might have regarding online threats to the work that
you do. The goal of the interview is for me to listen and identify topics to
cover at the planned encryption and operational security free seminar for
investigative journalists in 2017.&lt;/p&gt;
&lt;p&gt;The planned seminar is structured to help investigative journalists understand
the threats that you, as a journalist, face online, and how to design effective
responses to those threats. At the conclusion, participants will have an
understanding of what a threat model is, how to design a reasonable security
posture, best practice behaviours, and, to a lesser extent, what tools are
available.&lt;/p&gt;
&lt;p&gt;If you know any reporter or journalist that might be interested to
have a chat with me, please put me in touch with them. You can find my contacts
on the left sidebar.&lt;/p&gt;</content><category term="journal"/></entry><entry><title>Facebook-Loving Farmers of Mozambique</title><link href="https://www.quantisan.com/facebook-loving-farmers-of-mozambique/" rel="alternate"/><published>2016-03-10T00:00:00-05:00</published><updated>2016-03-10T00:00:00-05:00</updated><author><name>Paul Lam</name></author><id>tag:www.quantisan.com,2016-03-10:/facebook-loving-farmers-of-mozambique/</id><summary type="html">&lt;p&gt;We run out of drinking water one afternoon at our house on the 300-hectare
farm. The region's power grid has been down for over a day. Our water tanks
have run dry. It is 38 degrees Celsius, and the African summer sun is high in
the sky. This happened on …&lt;/p&gt;</summary><content type="html">&lt;p&gt;We run out of drinking water one afternoon at our house on the 300-hectare
farm. The region's power grid has been down for over a day. Our water tanks
have run dry. It is 38 degrees Celsius, and the African summer sun is high in
the sky. This happened on the first week of my month-long stay with our
smallholder farmers in Moamba, Mozambique. It also sets the tone for what to
expect. This is Africa. Nothing is easy.&lt;/p&gt;
&lt;p&gt;&lt;img alt="picture of Moamba landscape" src="https://www.quantisan.com/images/2016/moamba_landscape.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Rewinding back five weeks, our first cohort of smallholder farmers were
finishing trial runs of our product, InGrower App. We got lots of feedback.
More inputs here. Move the box there. The usual pointing out the obvious. &lt;em&gt;Users
can tell you what's wrong, but they cannot tell you what to build.&lt;/em&gt; As we're an
early stage startup that is still finding a product-market fit, we desperately
need to figure out the latter. It is then that I see the only way to do this is
for me to be in the same room as our users. And that is how I found myself
living with our users on a chicken farm in Moamba.&lt;/p&gt;
&lt;p&gt;One of the first questions that I asked my current co-founder when he pitched
the InGrower idea to me was –  do farmers in Mozambique use smartphones? I am
now able to answer my own question. While living there, power and water often
go out (though not usually  at the same time), but the 3G mobile internet is
always on. So what do you do when there is no power or water, and it's too hot
to do anything? You browse Facebook on your $30 Android smartphone, like
everybody else.&lt;/p&gt;
&lt;p&gt;&lt;img alt="picture of Lurdes using phone" src="https://www.quantisan.com/images/2016/lurdes_on_phone.jpg"&gt;&lt;/p&gt;
&lt;p&gt;Of our first 14 trial users, two are heavy users of our product. These power
users have been regular Facebook users for more than three years. They are used
to navigating a mobile user interface, and understand the benefits of having
data online. These are the users that we want as our initial champions for the
app.&lt;/p&gt;
&lt;p&gt;The farm is 10 km away from Moamba village. We often roar into the village
sitting on the back of a tractor.&lt;/p&gt;
&lt;p&gt;&lt;img alt="riding tractor" src="https://www.quantisan.com/images/2016/riding_tractor.jpg"&gt;&lt;/p&gt;
&lt;p&gt;This is Moamba market:&lt;/p&gt;
&lt;p&gt;&lt;img alt="market" src="https://www.quantisan.com/images/2016/moamba_market.jpg"&gt;&lt;/p&gt;
&lt;p&gt;This is my bed during my stay at the farm:&lt;/p&gt;
&lt;p&gt;&lt;img alt="bed" src="https://www.quantisan.com/images/2016/moamba_bed.jpg"&gt;&lt;/p&gt;
&lt;p&gt;I specifically asked the manager before arriving to help me set up a mosquito
net. Malaria is a real danger here, especially at the height of summer in
January, when I was present. But what do you know? There were few mosquitoes in
the house. Instead, I had giant bugs of all shapes and forms visiting me every
night. Thank god I had the net!&lt;/p&gt;
&lt;p&gt;&lt;img alt="moth" src="https://www.quantisan.com/images/2016/moth.jpg"&gt;&lt;/p&gt;
&lt;p&gt;I ate with the staff and farmers at the house. We all chipped in with US$20
per person each month. That was the budget for a month of groceries for them. Average
monthly salary is US$100 per month in the country. A piece of bread cost about
$0.15 and a whole chicken was $4 at the market. This was our typical lunch at
the house:&lt;/p&gt;
&lt;p&gt;&lt;img alt="Lunch" src="https://www.quantisan.com/images/2016/two_piece_lunch.jpg"&gt;&lt;/p&gt;
&lt;p&gt;There was a limit of two small pieces of meat per person for every meal when
there was anything to eat at all. There were a couple of days when I had only a
piece of bread and some crackers all day. I don’t know how the other guys who
actually had to do physical labour managed to work on an empty stomach.&lt;/p&gt;
&lt;p&gt;Sometimes, some of the guys went fishing or picked coconuts, and then our
housekeeper would make something special.&lt;/p&gt;
&lt;p&gt;&lt;img alt="coconut" src="https://www.quantisan.com/images/2016/coconut_fish.jpg"&gt;&lt;/p&gt;
&lt;p&gt;At other times, I snuck out to the village and had a shameless feast. All of
this grilled sausage and food is $6 and enough to serve four people:&lt;/p&gt;
&lt;p&gt;&lt;img alt="grilled sausage" src="https://www.quantisan.com/images/2016/grilled_sausage.jpg"&gt;&lt;/p&gt;
&lt;p&gt;A typical day goes like this for the chicken farmers of Moamba. They come in
from the village at 7am. Tend to their chicken house. Breakfast at 10am. Back
to their chicken house. Lunch at 2pm. By then, the sun and summer heat is in
full blast. They chill in the staff house until 5pm, then head home back to the
village.&lt;/p&gt;
&lt;p&gt;&lt;img alt="chicken house" src="https://www.quantisan.com/images/2016/chicken_house.jpg"&gt;&lt;/p&gt;
&lt;p&gt;I came to Moamba to do product development. In addition to all the learning and
understanding that I received, the experience itself has been very special.
Hanging out in the afternoon with the merry gang, and with the cool breeze
blowing against your face. Riding into the village on a tractor. Brushing my
teeth every evening out in the open, under the milky way. Nothing is easy here.
But there's a certain romance in expecting the unexpected in daily life in
Mozambique.&lt;/p&gt;
&lt;p&gt;P.S. the title of this post is a play on &lt;a href="http://www.theatlantic.com/technology/archive/2016/01/the-facebook-loving-farmers-of-myanmar/424812/"&gt;Craig Mod’s article&lt;/a&gt;.&lt;/p&gt;</content><category term="startup"/></entry><entry><title>Immersing with Users in Mozambique</title><link href="https://www.quantisan.com/immersing-with-users-in-mozambique/" rel="alternate"/><published>2016-01-14T00:00:00-05:00</published><updated>2016-01-14T00:00:00-05:00</updated><author><name>Paul Lam</name></author><id>tag:www.quantisan.com,2016-01-14:/immersing-with-users-in-mozambique/</id><summary type="html">&lt;p&gt;I've been living on a farm in Mozambique for the past week to get to know our
users for &lt;a href="https://ingrower.com"&gt;InGrower&lt;/a&gt;. My cofounder has been working here
for 5 years. But still, the fact that I'm a product lead whom knows almost
nothing about the lives of our users outside the …&lt;/p&gt;</summary><content type="html">&lt;p&gt;I've been living on a farm in Mozambique for the past week to get to know our
users for &lt;a href="https://ingrower.com"&gt;InGrower&lt;/a&gt;. My cofounder has been working here
for 5 years. But still, the fact that I'm a product lead whom knows almost
nothing about the lives of our users outside the context of our product trouble
me. Soon after starting our alpha trial last month, I informed our team that
I'm going over for user research.&lt;/p&gt;
&lt;p&gt;Immersion is one of several ways to get to know your users. According to IDEO,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The Inspiration phase is dedicated to hearing the voices and understanding
the lives of the people you’re designing for. The best route to gaining that
understanding is to talk to them in person, where they live, work, and lead
their lives. Once you’re in-context, there are lots of ways to observe the
people you’re designing for. Spend a day shadowing them, have them walk you
through how they make decisions, play  fly on the wall and observe them as
they cook, socialize, visit the doctor—whatever is relevant to your
challenge.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The insight here is that a product doesn't operate in a vacuum. To build a
product that our users would love, I need to understand who they are, what
motivates them, what keeps them up at night. So that we deliver a product built
for them for their lives in Mozambique and not what I assume to be while living
in Boston or Copenhagen.&lt;/p&gt;
&lt;p&gt;One of the many things that I learned here is that our users have a high
tolerance for bad user interface. If they want something, they'll make it
work. The big question for us then is creating something of tangible value to
them. Having shadowed and spoken to a handful of local smallholding farmers, I
think I have a better sense of what that actually means.&lt;/p&gt;
&lt;p&gt;&lt;img alt="makeshift kitchen" src="https://www.quantisan.com/images/2016/makeshift_kitchen.jpg"&gt;&lt;/p&gt;
&lt;p&gt;This is our makeshift kitchen that my housemates assembled. Everything here are
hacked together in one way or another.&lt;/p&gt;</content><category term="startup"/></entry></feed>