<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Katarzyna Kozłowska</title>
    <description>The latest articles on DEV Community by Katarzyna Kozłowska (@katarzyna_kozowska).</description>
    <link>https://dev.to/katarzyna_kozowska</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4093859%2F82df5c46-fc92-4ac2-bad5-f07fddb6aa24.jpg</url>
      <title>DEV Community: Katarzyna Kozłowska</title>
      <link>https://dev.to/katarzyna_kozowska</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/katarzyna_kozowska"/>
    <language>en</language>
    <item>
      <title>Sealed Monad: Linear Business Flows as Algebraic Data Types</title>
      <dc:creator>Katarzyna Kozłowska</dc:creator>
      <pubDate>Tue, 13 Oct 2026 08:39:37 +0000</pubDate>
      <link>https://dev.to/katarzyna_kozowska/sealed-monad-linear-business-flows-as-algebraic-data-types-4430</link>
      <guid>https://dev.to/katarzyna_kozowska/sealed-monad-linear-business-flows-as-algebraic-data-types-4430</guid>
      <description>&lt;p&gt;Every team that builds a Scala backend long enough makes the same structural mistake: they use nested &lt;code&gt;flatMap&lt;/code&gt; and &lt;code&gt;match&lt;/code&gt; to express multi-step business logic. The types are right. The code does what it's supposed to do. And then requirements change, someone adds a step, and the next developer has to trace a decision tree through nested match arms to understand what the function actually does. At that point, the maintainability cost is already sunk. You're just paying it in review time and bugs.&lt;/p&gt;

&lt;p&gt;This article covers two tools that solve the structural problem together: ADTs for expressing typed business outcomes, and &lt;a href="https://github.com/theiterators/sealed-monad" rel="noopener noreferrer"&gt;sealed-monad&lt;/a&gt; for composing them linearly.&lt;/p&gt;

&lt;h2&gt;
  
  
  ADTs for business outcomes
&lt;/h2&gt;

&lt;p&gt;A sealed trait is the right type for business outcomes. Not exceptions, not raw booleans, not strings. Here's what a login result looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;trait&lt;/span&gt; &lt;span class="nc"&gt;LoginResult&lt;/span&gt;
&lt;span class="k"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;LoginResult&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LoggedIn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;LoginResult&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="k"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;InvalidCredentials&lt;/span&gt;     &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;LoginResult&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="k"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;Deleted&lt;/span&gt;                &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;LoginResult&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="k"&gt;object&lt;/span&gt; &lt;span class="nc"&gt;ProviderAuthFailed&lt;/span&gt;     &lt;span class="k"&gt;extends&lt;/span&gt; &lt;span class="nc"&gt;LoginResult&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sealed&lt;/code&gt; modifier means the compiler knows the complete list of cases. No subclass can be added outside the file. Add &lt;code&gt;case object AccountLocked extends LoginResult&lt;/code&gt;, recompile, and the compiler flags every pattern match on &lt;code&gt;LoginResult&lt;/code&gt; that doesn't handle it.&lt;/p&gt;

&lt;p&gt;The alternatives don't give you this. Exceptions are invisible to the type system: the signature &lt;code&gt;def login(...): Future[String]&lt;/code&gt; tells the caller nothing about what can go wrong. Raw booleans lose the reason: you know the login failed, not why. String errors can't be distinguished by the compiler: &lt;code&gt;Left("invalid credentials")&lt;/code&gt; and &lt;code&gt;Left("account deleted")&lt;/code&gt; are the same type.&lt;/p&gt;

&lt;h2&gt;
  
  
  The composition problem
&lt;/h2&gt;

&lt;p&gt;Defining the outcome type is the easy part. The problem is chaining multiple operations, each returning a typed outcome, into a single flow. Consider a login with four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find the user by email. If none exists: &lt;code&gt;InvalidCredentials&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Check the user isn't archived. If they are: &lt;code&gt;Deleted&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Find the auth method. If none: &lt;code&gt;ProviderAuthFailed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Verify the credentials. If they fail: &lt;code&gt;InvalidCredentials&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In standard Scala, this is what you write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Future&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt;
  &lt;span class="nf"&gt;findUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="py"&gt;flatMap&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nv"&gt;Future&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;successful&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;InvalidCredentials&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;archived&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nv"&gt;Future&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;successful&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Deleted&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt;
      &lt;span class="nf"&gt;findAuthMethod&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;Provider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;EmailPass&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="py"&gt;flatMap&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;None&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nv"&gt;Future&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;successful&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;ProviderAuthFailed&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;am&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;checkAuthMethod&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;am&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nv"&gt;Future&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;successful&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;InvalidCredentials&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nc"&gt;Some&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;_&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nv"&gt;Future&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;successful&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;LoggedIn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;issueTokenFor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;)))&lt;/span&gt;
      &lt;span class="o"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The types are correct and the logic is correct. Look at the structure anyway:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The happy path is buried in the innermost match arm.&lt;/li&gt;
&lt;li&gt;Error cases are scattered across the nesting rather than declared next to the step they belong to.&lt;/li&gt;
&lt;li&gt;Adding a step means adding another nesting level, not another line.&lt;/li&gt;
&lt;li&gt;A reviewer has to mentally trace the execution tree to verify the logic matches the requirement.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Four steps already take 6 match arms across 2 nested blocks.&lt;/p&gt;

&lt;p&gt;We ran into this at Iterators while building a backend for a client with unusually dense business rules. Login was fine. Order creation was manageable. But the cancellation flow had seven distinct failure modes, each conditional on the previous steps, and the nested version had become something nobody wanted to modify. Adding a step for fraud detection before the payment charge required reading the full function, finding the right nesting level, and not breaking the cases already there. It took longer than it should have, and the resulting review was not clean. The logic was right. The structure was wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  sealed-monad
&lt;/h2&gt;

&lt;p&gt;sealed-monad is an open-source Scala library built at Iterators. It wraps the composition pattern in a monadic abstraction that makes for-comprehensions work with ADT-based business flows. The same login logic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pl.iterators.sealedmonad.syntax._&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Future&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;user&lt;/span&gt;       &lt;span class="k"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="nf"&gt;findUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;valueOr&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;InvalidCredentials&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;ensure&lt;/span&gt;&lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="nv"&gt;_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;archived&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Deleted&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;authMethod&lt;/span&gt; &lt;span class="k"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="nf"&gt;findAuthMethod&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;user&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;id&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;Provider&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;EmailPass&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;valueOr&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;ProviderAuthFailed&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
                    &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;ensure&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;checkAuthMethod&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;InvalidCredentials&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nv"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;LoggedIn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;issueTokenFor&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;))).&lt;/span&gt;&lt;span class="py"&gt;run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it top to bottom:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find the user. If none: &lt;code&gt;InvalidCredentials&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Ensure not archived. If archived: &lt;code&gt;Deleted&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Find auth method. If none: &lt;code&gt;ProviderAuthFailed&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Ensure credentials pass. If not: &lt;code&gt;InvalidCredentials&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If all succeed: &lt;code&gt;LoggedIn&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The happy path is the &lt;code&gt;yield&lt;/code&gt;. Each failure case is an inline annotation on the step it belongs to. The structure of the code is the structure of the requirement.&lt;/p&gt;

&lt;h3&gt;
  
  
  The two operators
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;valueOr(fallback)&lt;/code&gt; converts an &lt;code&gt;Option&lt;/code&gt; or &lt;code&gt;Either&lt;/code&gt; result. If the step produces &lt;code&gt;None&lt;/code&gt; or &lt;code&gt;Left&lt;/code&gt;, the chain short-circuits with the fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Option: short-circuit on None&lt;/span&gt;
&lt;span class="nf"&gt;findUser&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="py"&gt;valueOr&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;InvalidCredentials&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;ensure(predicate, fallback)&lt;/code&gt; checks a condition on a successful result. If the predicate fails, the chain short-circuits with the fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Short-circuit if the user is archived&lt;/span&gt;
&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;ensure&lt;/span&gt;&lt;span class="o"&gt;(!&lt;/span&gt;&lt;span class="nv"&gt;_&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;archived&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;Deleted&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both work inside a single for-comprehension because the &lt;code&gt;Sealed&lt;/code&gt; wrapper is a monad over the effect type &lt;code&gt;F&lt;/code&gt;. The &lt;code&gt;.run&lt;/code&gt; at the end collapses the chain back into &lt;code&gt;F[LoginResult]&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the compiler checks
&lt;/h3&gt;

&lt;p&gt;Because &lt;code&gt;LoginResult&lt;/code&gt; is a sealed trait, the compiler knows every branch. If a call site doesn't handle all cases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="o"&gt;).&lt;/span&gt;&lt;span class="py"&gt;map&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nv"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;LoggedIn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Ok&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="nv"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;InvalidCredentials&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;Unauthorized&lt;/span&gt;
  &lt;span class="c1"&gt;// Warning: match may not be exhaustive.&lt;/span&gt;
  &lt;span class="c1"&gt;// It would fail on: Deleted, ProviderAuthFailed&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compiler surfaces the gap before the code reaches production. Add a new failure mode to the business logic, and the compiler flags every call site that doesn't handle it. By default that is a warning, so enable fatal warnings in your build if you want a missing case to fail the compile. With exception-based error handling, adding a new failure case compiles cleanly and the surprise arrives at runtime.&lt;/p&gt;

&lt;h3&gt;
  
  
  Effect systems
&lt;/h3&gt;

&lt;p&gt;sealed-monad works with any effect type that has a &lt;code&gt;cats.Monad&lt;/code&gt; instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Future&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Future&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nv"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;LoggedIn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="py"&gt;run&lt;/span&gt;

&lt;span class="c1"&gt;// cats-effect IO&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;IO&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nv"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;LoggedIn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="py"&gt;run&lt;/span&gt;

&lt;span class="c1"&gt;// Monix Task&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;login&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="k"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Task&lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="kt"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="k"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt; &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nv"&gt;LoginResult&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="py"&gt;LoggedIn&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;)).&lt;/span&gt;&lt;span class="py"&gt;run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Migrating from &lt;code&gt;Future&lt;/code&gt; to &lt;code&gt;IO&lt;/code&gt; means changing the effect type of the functions you compose. The business logic in the for-comprehension stays identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it fits and where it doesn't
&lt;/h2&gt;

&lt;p&gt;sealed-monad is for multi-step business flows where each step can fail with a typed domain error. That's the scope.&lt;/p&gt;

&lt;p&gt;It is not a general-purpose error handling library. Infrastructure errors, database failures and network timeouts do not belong in your business ADT. A database timeout is not a &lt;code&gt;LoginResult&lt;/code&gt; case. It's an exception, and it should propagate as one. The sealed trait should contain only the outcomes the calling code needs to handle and route.&lt;/p&gt;

&lt;p&gt;The practical question before reaching for it: does the function implement a business requirement that has multiple named failure modes, each meaningful to the calling code? If yes, sealed-monad is worth using. If the function is infrastructure plumbing with one failure mode (it worked or it didn't), standard error handling is correct.&lt;/p&gt;

&lt;p&gt;One more thing about the compile-time check: it only holds if you treat it seriously. If you add a catch-all case to your pattern matches to make the compiler stop warning, you've opted out of the guarantee.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="c1"&gt;// build.sbt&lt;/span&gt;
&lt;span class="n"&gt;libraryDependencies&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="s"&gt;"pl.iterators"&lt;/span&gt; &lt;span class="o"&gt;%%&lt;/span&gt; &lt;span class="s"&gt;"sealed-monad"&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="s"&gt;"2.0.1"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight scala"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nn"&gt;pl.iterators.sealedmonad.syntax._&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scala 2.13 and Scala 3, cats-core as the only dependency. Apache 2.0 license.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/theiterators/sealed-monad" rel="noopener noreferrer"&gt;https://github.com/theiterators/sealed-monad&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://theiterators.github.io/sealed-monad/" rel="noopener noreferrer"&gt;https://theiterators.github.io/sealed-monad/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The longer version of this article, with an order-processing flow and a testing section, is on the &lt;a href="https://www.iteratorshq.com/blog/sealed-monad-linear-business-flows-as-algebraic-data-types/" rel="noopener noreferrer"&gt;Iterators blog&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;sealed-monad is maintained by Iterators.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>scala</category>
      <category>api</category>
    </item>
    <item>
      <title>Baklava: Generate API Documentation and Type-Safe Clients from Scala Routing Tests</title>
      <dc:creator>Katarzyna Kozłowska</dc:creator>
      <pubDate>Tue, 25 Aug 2026 09:49:56 +0000</pubDate>
      <link>https://dev.to/katarzyna_kozowska/baklava-generate-api-documentation-and-type-safe-clients-from-scala-routing-tests-3eif</link>
      <guid>https://dev.to/katarzyna_kozowska/baklava-generate-api-documentation-and-type-safe-clients-from-scala-routing-tests-3eif</guid>
      <description>&lt;p&gt;API documentation has a reliability problem. The code gets updated; the OpenAPI spec gets forgotten. The spec gets updated; the TypeScript client doesn't regenerate. By the time an enterprise client asks for your API contract, the document you hand them describes a system that no longer exists. Baklava, an open-source library by &lt;a href="https://www.iteratorshq.com/" rel="noopener noreferrer"&gt;Iterators&lt;/a&gt;, solves this structurally: documentation is generated from the tests that verify your actual API behaviour, so it cannot drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Documentation drift is the default state of any API that lives long enough. The causes are well-understood: docs and code are maintained separately, documentation updates require extra discipline at every PR, and no automated check catches a route signature change that wasn't reflected in the OpenAPI file.&lt;/p&gt;

&lt;p&gt;The consequence is real. Clients building against a stale spec hit integration errors in production. Internal teams onboarding to a service spend hours reconciling the documented contract with actual behaviour. TypeScript front-ends break when an API response field changes without a corresponding client update. The problem compounds as the API grows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The solution
&lt;/h2&gt;

&lt;p&gt;Baklava integrates into your existing test suite. When routing tests run, baklava observes each request and response, infers the API surface, and generates documentation as a test output, not as a separate build step, not as a manually-maintained file.&lt;/p&gt;

&lt;p&gt;In baklava, the test &lt;em&gt;is&lt;/em&gt; the documentation spec. Instead of a standard assertion block, each route is defined with &lt;code&gt;path()&lt;/code&gt;, &lt;code&gt;supports()&lt;/code&gt;, and &lt;code&gt;onRequest()&lt;/code&gt; scenarios that both verify the API behaviour and describe it for documentation output:&lt;/p&gt;

&lt;p&gt;​`// The test IS the documentation spec&lt;br&gt;
class UserApiSpec extends AnyFunSpec&lt;br&gt;
    with BaklavaPekkoHttp[Unit, Unit, ScalatestAsExecution]&lt;br&gt;
    with BaklavaScalatest[Route, ToEntityMarshaller, FromEntityUnmarshaller] {&lt;/p&gt;

&lt;p&gt;path("/users/{userId}")(&lt;br&gt;
    supports(&lt;br&gt;
      GET,&lt;br&gt;
      pathParameters = p&lt;a href="https://dev.to"&gt;Long&lt;/a&gt;,&lt;br&gt;
      summary = "Get user by ID"&lt;br&gt;
    )(&lt;br&gt;
      onRequest(pathParameters = 1L)&lt;br&gt;
        .respondsWith&lt;a href="https://dev.toOK,%20description%20="&gt;User&lt;/a&gt;&lt;br&gt;
        .assert { ctx =&amp;gt;&lt;br&gt;
          ctx.performRequest(routes).body.id shouldBe 1L&lt;br&gt;
        },&lt;br&gt;
      onRequest(pathParameters = 999L)&lt;br&gt;
        .respondsWith&lt;a href="https://dev.toNotFound,%20description%20="&gt;ErrorResponse&lt;/a&gt;&lt;br&gt;
        .assert { ctx =&amp;gt; ctx.performRequest(routes) }&lt;br&gt;
    )&lt;br&gt;
  )&lt;br&gt;
}&lt;br&gt;
// Running sbt test generates OpenAPI, HTML, and TypeScript, automatically.&lt;br&gt;
// If the test breaks, the docs don't publish.`&lt;/p&gt;

&lt;p&gt;If the test passes, the documentation is accurate, because the test verified the behaviour the documentation describes. If the route changes and the test breaks, the documentation won't publish until the test is fixed. Documentation drift becomes structurally impossible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coverage
&lt;/h2&gt;

&lt;p&gt;Baklava supports the two dominant Scala HTTP frameworks, Pekko HTTP and http4s, and integrates with ScalaTest, Specs2, and MUnit. Seven output formats, each an independent SBT dependency, auto-discovered via reflection:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Simple HTML&lt;/strong&gt;: self-contained, browsable human-readable documentation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OpenAPI&lt;/strong&gt;: OpenAPI 3.0.1 spec with SwaggerUI integration for Pekko HTTP, the standard contract format for enterprise API integrations&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TS-REST&lt;/strong&gt;: TypeScript npm package using ts-rest and Zod for type-safe API contracts&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;oRPC Contract Format&lt;/strong&gt;: TypeScript package with oRPC contracts, Zod validators, and a ready-made client factory&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript Fetch Client&lt;/strong&gt;: plain-TypeScript client using the browser/Node fetch API, no external runtime dependencies&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Postman&lt;/strong&gt;: Postman Collection v2.1, importable into Postman and Insomnia&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sttp client&lt;/strong&gt;: Scala sttp-client4 request builders for every endpoint&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Scala 2.13 and Scala 3, JDK 11+. Baklava also integrates with kebs: if your project already uses kebs for domain type derivation, baklava picks up the schema definitions automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adoption and maintenance
&lt;/h2&gt;

&lt;p&gt;Baklava v1.4.0 was released in May 2026 and is actively maintained by Iterators. Apache 2.0 license.&lt;/p&gt;

&lt;p&gt;The approach, deriving documentation from tests rather than maintaining it separately, is not new in principle, but prior implementations in the Scala ecosystem typically required significant test instrumentation or source-level annotation. Baklava keeps the overhead minimal: mix in a trait, run your existing tests, get documentation as output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;OpenAPI generation tools for Scala generally fall into two camps: annotate source code (tapir, endpoints4s) or maintain separate spec files. Source annotation couples documentation concerns into routing logic and requires adopting the library's routing model. Separate spec files require discipline. Baklava is positioned differently: it uses the test suite as the source of truth, which works with any routing code that produces testable HTTP responses, so no migration of existing routes is required.&lt;/p&gt;

&lt;p&gt;As Łukasz Sowa, Managing Partner at &lt;a href="https://www.iteratorshq.com/" rel="noopener noreferrer"&gt;Iterators&lt;/a&gt;, puts it: "Integration tests are one of the most overlooked types of tests. 'Units pass, I'm ok!' until they aren't and it isn't. The amount of work to get them right used to be the usual excuse; now AI can do the heavy lifting, which removes it. But integration tests are more than a good night's sleep pill. They're the ultimate spec for how the outside world interacts with your system. With baklava, we turn integration tests into integration artifacts, for many kinds of consumers."&lt;/p&gt;

&lt;p&gt;Baklava is open source under the Apache 2.0 license. GitHub: &lt;a href="https://github.com/theiterators/baklava" rel="noopener noreferrer"&gt;https://github.com/theiterators/baklava&lt;/a&gt;&lt;/p&gt;

</description>
      <category>scala</category>
      <category>opensource</category>
      <category>testing</category>
      <category>api</category>
    </item>
  </channel>
</rss>
