<?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: epilot</title>
    <description>The latest articles on DEV Community by epilot (@epilot).</description>
    <link>https://dev.to/epilot</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F3368%2Fa1997f89-aaf9-4f0f-b464-d34f2775a882.jpg</url>
      <title>DEV Community: epilot</title>
      <link>https://dev.to/epilot</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/epilot"/>
    <language>en</language>
    <item>
      <title>We Made Coding Agents Actually Reliable By Fixing One Thing</title>
      <dc:creator>Viljami Kuosmanen</dc:creator>
      <pubDate>Wed, 04 Feb 2026 15:55:45 +0000</pubDate>
      <link>https://dev.to/epilot/we-made-coding-agents-actually-reliable-by-fixing-one-thing-525b</link>
      <guid>https://dev.to/epilot/we-made-coding-agents-actually-reliable-by-fixing-one-thing-525b</guid>
      <description>&lt;p&gt;Last week, Vercel published &lt;a href="https://vercel.com/blog/agents-md-outperforms-skills-in-our-agent-evals" rel="noopener noreferrer"&gt;research showing that giving coding agents a compact index of your documentation dramatically outperforms letting them search for answers on demand&lt;/a&gt;. Their eval results: 100% task success rate with the map approach, versus only 79% when agents had to actively look things up.&lt;/p&gt;

&lt;p&gt;Same agent, same tasks, different approach to context. The difference between working and not working.&lt;/p&gt;

&lt;p&gt;The insight clicked immediately. If we could give Claude Code reliable access to this institutional knowledge without forcing it to decide when to look things up, it would fundamentally change how people work in our codebase.&lt;/p&gt;

&lt;p&gt;So we built it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Context Problem
&lt;/h2&gt;

&lt;p&gt;Coding agents have a token limit. A ceiling on how much information they can process at once. Think of it like working memory. You can't hand Claude Code your entire codebase and documentation library upfront. It's too much.&lt;/p&gt;

&lt;p&gt;The traditional solution is skills: the coding agent decides when it needs information and actively looks it up. "I need to know about authentication, let me search for that." Sounds reasonable. In practice it creates three problems:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Decision paralysis&lt;/strong&gt; - the agent has to decide &lt;em&gt;when&lt;/em&gt; to look up docs, and it often guesses wrong&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Async delay&lt;/strong&gt; - every lookup is a round-trip, breaks flow&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sequencing conflicts&lt;/strong&gt; - exploring code vs. consulting docs creates timing issues&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Vercel's approach flips this: give Claude Code a compressed index of what documentation exists and where to find it, &lt;em&gt;before&lt;/em&gt; it starts work. The index is small enough to fit in context every turn, so it always knows what's available. When it needs details, it reads the specific file directly. No decisions, no lookups, no conflicts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Compression Matters
&lt;/h2&gt;

&lt;p&gt;The key innovation is compression. A full documentation tree - all the folder structures, file names, categories - takes significant space. Too much to include in every conversation turn.&lt;/p&gt;

&lt;p&gt;The compressed index uses a simple pipe-delimited format that shrinks this by ~80%:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[epilot Docs Index]|root: ./.epilot-docs|00-general:{tech-stack.md,business-context.md,ci-cd.md}|01-apis:{api-design.md,calling-apis.md}|02-epilot360-microfrontends:{env-vars.md,local-dev.md}|...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single line tells the agent:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where the docs live (&lt;code&gt;.epilot-docs/&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;What categories exist (&lt;code&gt;00-general&lt;/code&gt;, &lt;code&gt;01-apis&lt;/code&gt;, etc.)&lt;/li&gt;
&lt;li&gt;What files are in each category&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It's a table of contents, not the full book. But it's enough. The agent sees the map, understands what's available, and pulls specific files when needed. The decision-making load disappears.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2smftr4lr0o3jpr0xtxp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2smftr4lr0o3jpr0xtxp.png" alt="Claude code actually reading docs for once before jumping into code" width="800" height="461"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;The accuracy improvement is significant (100% vs 79% task success), but it's not the real story.&lt;/p&gt;

&lt;p&gt;The real story is what happens when you lower the barrier to contribution. Proper context enables the person closest to the problem to fix it, regardless of job title. Your PM can fix bugs. Designers can adjust component behavior. Support engineers can patch data issues. You remove bottlenecks. Context and authority live in the same person.&lt;/p&gt;

&lt;p&gt;Coding agents are the equalizer. But they're only as good as the context you give them.&lt;/p&gt;

&lt;p&gt;Most companies will throw Claude Code at their codebase and wonder why results are inconsistent. The agent hallucinates patterns. Makes incorrect assumptions. Writes code that doesn't match conventions.&lt;/p&gt;

&lt;p&gt;The difference is context. Structured, compressed, always-available context about how &lt;em&gt;your&lt;/em&gt; codebase works.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Build Your Own
&lt;/h2&gt;

&lt;p&gt;The pattern is straightforward. Here's what we did at epilot:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Curate agent-friendly documentation&lt;/strong&gt; - organize your internal knowledge: conventions, APIs, architectural patterns, framework usage, code style&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structure by domain&lt;/strong&gt; - group related docs (general, backend, frontend, infrastructure, etc.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use descriptive filenames&lt;/strong&gt; - the agent sees filenames in the compressed index before opening files. &lt;code&gt;api-design.md&lt;/code&gt; is better than &lt;code&gt;guidelines.md&lt;/code&gt;. &lt;code&gt;error-handling.md&lt;/code&gt; is better than &lt;code&gt;errors.md&lt;/code&gt;. Make filenames searchable and specific.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate updates&lt;/strong&gt; - pull live data where possible (OpenAPI specs, schema definitions, framework docs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Generate compressed index&lt;/strong&gt; - use a simple format (pipe-delimited works well) that reduces the doc tree by ~80%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embed in agent context&lt;/strong&gt; - add the index to your CLAUDE.md or AGENTS.md file (the context files Claude Code reads)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One thing worth highlighting: we don't just include our internal documentation. We also package docs for the frameworks and libraries we heavily use - single-spa, openapi-backend, openapi-client-axios, i18next, and Volt UI (our custom design system). When Claude Code needs to know how i18next pluralization works or how to register a single-spa parcel, it already has the answer. No hallucination, no outdated Stack Overflow posts, just accurate framework documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Enables
&lt;/h2&gt;

&lt;p&gt;We're already seeing daily usage across the team. Developers context-switch between services faster. Non-engineers contribute directly instead of filing tickets.&lt;/p&gt;

&lt;p&gt;But the real potential is broader: if compressed context improves coding agents for technical documentation, why stop there?&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Runbooks and incident response&lt;/strong&gt; - on-call engineers with instant access to procedures&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Customer domain knowledge&lt;/strong&gt; - support teams with context on product behavior&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Business logic&lt;/strong&gt; - product decisions and their rationale, preserved and accessible&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern is the same: curate the knowledge, compress it, embed it in context, let the agent work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Constraints Are Disappearing
&lt;/h2&gt;

&lt;p&gt;For decades, contributing to a codebase required deep technical knowledge. You needed to understand the language, the frameworks, the architectural patterns, the implicit conventions. The barrier was high.&lt;/p&gt;

&lt;p&gt;Coding agents lower it. Claude Code, Cursor, and similar tools don't replace engineers. They make technical knowledge more accessible. With the right tooling and the right context, a PM can fix bugs. A designer can adjust styling logic. A support engineer can patch data issues.&lt;/p&gt;

&lt;p&gt;The question isn't whether this is possible. It's how fast you can adapt.&lt;/p&gt;

&lt;p&gt;Organizations which enable broader contribution will move faster than those that don't. The tools exist. The research is clear. What's missing is execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start Simple
&lt;/h2&gt;

&lt;p&gt;You don't need to document everything upfront. Start with the knowledge that causes the most friction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Code style conventions&lt;/strong&gt; - how you write TypeScript, naming patterns, file structure&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Common patterns&lt;/strong&gt; - how you handle authentication, API calls, error handling&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Framework specifics&lt;/strong&gt; - non-obvious usage of your frameworks and libraries&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Internal APIs&lt;/strong&gt; - if you have OpenAPI specs, even better&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Create a simple doc structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docs/
  00-general/
    code-style.md
    tech-stack.md
  01-apis/
    api-design.md
    calling-apis.md
  02-backend/
    error-handling.md
    database-patterns.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate the compressed index (pipe-delimited format, one line per directory). Add it to your CLAUDE.md or AGENTS.md file - the context files that Claude Code and other coding agents read on startup. Done.&lt;/p&gt;

&lt;p&gt;The compressed index approach works! 🎉 Vercel's research proved it: 100% task success versus 79% without it. We've validated it internally. Now it's about whether you'll adopt it before your competitors do.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>coding</category>
      <category>documentation</category>
    </item>
    <item>
      <title>Security at Scale: Our npm Incident Response Story</title>
      <dc:creator>João Pinho</dc:creator>
      <pubDate>Wed, 10 Sep 2025 16:20:24 +0000</pubDate>
      <link>https://dev.to/epilot/security-at-scale-our-npm-incident-response-story-439p</link>
      <guid>https://dev.to/epilot/security-at-scale-our-npm-incident-response-story-439p</guid>
      <description>&lt;p&gt;On September 8th, the npm ecosystem saw what is now called the largest supply-chain compromise in its history. Packages like chalk, debug, and ansi-styles — together downloaded billions of times every week — were hijacked and malicious versions published.&lt;/p&gt;

&lt;p&gt;For any SaaS company with Node.js in its stack, this was a moment to pause and act.&lt;/p&gt;

&lt;p&gt;At epilot, where we build cloud software for the energy market, we recognised that this had the potential to impact our systems. Here's how we responded.&lt;/p&gt;

&lt;h2&gt;
  
  
  🚨 First, what happened?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A phishing attack compromised the maintainer of several widely used packages.&lt;/li&gt;
&lt;li&gt;Malicious versions were published and quickly spread via transitive dependencies.&lt;/li&gt;
&lt;li&gt;The injected code was designed to hijack crypto wallet transactions, but the bigger story is: if attackers could publish once, they could publish anything. Additionally, if attackers could read/intercept our network requests, they would have access to our tokens and thus our platform on behalf of our users.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  ⚡ Our response — measured in hours
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;As soon as the incident was announced, our engineering team moved fast. Within minutes of posting the alert in our #dev-security slack channel, we had a small response team come together.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;172 repositories scanned: we used GitHub Codespaces to access our centralized codebase and systematically audit our product ecosystem.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;451 Node.js projects analyzed: every package.json across our domain groups (auth, billing, analytics, 360-portal, and more) was checked for the compromised packages and versions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Automation helped: we cross-checked with our internal tooling and monitoring alerts to confirm no production build had pulled malicious versions.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: ✅ zero exposure found across all 451 dependency trees.&lt;/p&gt;

&lt;h2&gt;
  
  
  🛡 Practices that kept us safe
&lt;/h2&gt;

&lt;p&gt;The npm incident highlighted the value of practices we already had in place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Dependency monitoring with Corge → ongoing visibility into package versions and vulnerabilities across our entire ecosystem.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Masked logs in Datadog → all PII automatically obfuscated, ensuring sensitive customer data never leaks, even if dependencies misbehave.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;LLM anonymisation → using Microsoft Presidio, we automatically detect and anonymise emails, phone numbers, IBANs, credit cards, and other PII before any data reaches AI models for our AI-powered features.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't "nice-to-haves." They are part of how we build trust with customers in a regulated industry like energy.&lt;/p&gt;

&lt;h2&gt;
  
  
  💡 What we learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Supply chain attacks are inevitable — being able to respond fast is what matters.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Modern tooling makes a difference — GitHub Codespaces let us audit 172 repositories and 451 Node.js projects in hours, not days.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Good hygiene compounds — monitoring dependencies, masking logs, and anonymizing LLM data aren't glamorous, but when incidents like this happen, they prove their worth.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  🔭 Looking forward
&lt;/h2&gt;

&lt;p&gt;Incidents like the npm compromise will continue. At epilot, we're committed not just to reacting fast, but to building resilient,  privacy-first systems that earn customer trust every day.&lt;/p&gt;

&lt;p&gt;Because in the energy market — where trust and compliance are everything — resilience is the real competitive advantage.&lt;/p&gt;

</description>
      <category>security</category>
      <category>privacy</category>
      <category>npm</category>
    </item>
    <item>
      <title>From Chaos to Clarity: Fixing Our Monorepo</title>
      <dc:creator>kate astrid</dc:creator>
      <pubDate>Tue, 19 Aug 2025 11:45:37 +0000</pubDate>
      <link>https://dev.to/epilot/from-chaos-to-clarity-fixing-our-monorepo-2bih</link>
      <guid>https://dev.to/epilot/from-chaos-to-clarity-fixing-our-monorepo-2bih</guid>
      <description>&lt;h2&gt;
  
  
  &lt;strong&gt;Introduction: Why This Story Might Save You Some Headaches&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;If you’ve worked in a large codebase, you’ve probably seen this: the code runs, but every change takes far longer than it should. Updating one feature often breaks another. Adding a dependency means worrying about conflicts. Old build tools still linger, and no one feels safe upgrading packages that half the system depends on.&lt;/p&gt;

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

&lt;p&gt;That’s exactly what we faced in our &lt;code&gt;journey-monorepo&lt;/code&gt;, the main repository for our team. Dependencies were wired together in inconsistent ways, different teams had introduced multiple overlapping build tools, and some libraries hadn’t been touched in years. Proposing a cleanup felt like asking to redesign a city while people were still living in it — risky, disruptive, and easy to delay indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;So why did we bother?&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Development had slowed to a crawl.&lt;/em&gt; Builds were taking too long, and waiting minutes just to see a change in the browser was killing productivity.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Every change felt risky.&lt;/em&gt; The dependency spaghetti meant a small tweak in one package could break multiple apps — often in ways we wouldn’t notice until much later.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Onboarding new developers was painful.&lt;/em&gt; It took too long for new team members to get up to speed, mostly because they had to learn multiple bundlers, testing setups, and outdated tooling quirks.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Technical debt was blocking new features.&lt;/em&gt; We were spending more time patching, debugging, and wrestling with the repo than building the actual features our users cared about.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Costs were adding up.&lt;/em&gt; Longer build times meant higher CI costs, and outdated packages made security updates and maintenance more expensive in the long run.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, the longer we waited, the more time, money, and momentum we were losing.&lt;/p&gt;

&lt;p&gt;And, hey, now we get a good story out of it.&lt;/p&gt;

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

&lt;h2&gt;
  
  
  &lt;strong&gt;Journey-Monorepo in a Nutshell&lt;/strong&gt;
&lt;/h2&gt;

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

&lt;p&gt;&lt;em&gt;Journey-monorepo&lt;/em&gt; is the central place on the epilot platform for everything related to journeys. What you see in the screenshot above represents two main apps: &lt;em&gt;journey-app&lt;/em&gt; on the left side and &lt;em&gt;journey-builder&lt;/em&gt; on the right. &lt;em&gt;Journey-builder&lt;/em&gt; is responsible for configuring journeys, and &lt;em&gt;journey-app&lt;/em&gt; for rendering them. &lt;br&gt;
Like in many startups, the repo started out clean and well-structured, but as the product grew quickly, the codebase expanded in unplanned ways — things were added wherever they seemed to fit at the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The “BEFORE” Setup&lt;/strong&gt;
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Apps:
- journey-builder.               
- journey-app
- entity-mapping
- journey-view

Packages:
- blocks-configurators
- blocks-renderers
- journey-elements
- journey-utils
- journey-logics-common
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let me show you the rough map of dependencies we had step by step. We are interested in two main apps: &lt;code&gt;journey-builder&lt;/code&gt; and &lt;code&gt;journey-app&lt;/code&gt;, so I am going to dive deeper into their connections. It is not important which app or package does what. What IS important is to see how many connections we had to keep in mind every time we needed to change anything.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Ok, this is fine.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkf5p4t1zdjxf169gq22z.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkf5p4t1zdjxf169gq22z.png" alt=" " width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This is too.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8udxll5y9ewrp6q8nboq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8udxll5y9ewrp6q8nboq.png" alt=" " width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Yeah, cool.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuy2gcleacdlwo85nozw0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuy2gcleacdlwo85nozw0.png" alt=" " width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Packages already become tangled, let's keep going.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fncrjunqvi3x0zojsp82d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fncrjunqvi3x0zojsp82d.png" alt=" " width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ok, even more connections.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6qeqtr5pcsd766pxyhld.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6qeqtr5pcsd766pxyhld.png" alt=" " width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Well, this is just a delinquent madness.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fptjuqypr9w8yp04lzbmj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fptjuqypr9w8yp04lzbmj.png" alt=" " width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You might say: Kate, come on, this is what happens in every big project - things become deeply intertwined, and it is a normal thing to experience. &lt;br&gt;
I will answer: yes, but I haven't finished.&lt;/p&gt;

&lt;p&gt;Let's now add the build tools to this table to have the full picture.&lt;/p&gt;

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

&lt;p&gt;Well, what we have here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Webpack;&lt;/li&gt;
&lt;li&gt;Craco;&lt;/li&gt;
&lt;li&gt;Tsup;&lt;/li&gt;
&lt;li&gt;Tsx;&lt;/li&gt;
&lt;li&gt;Tsdx.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I don't know about you, but I hadn't worked with half of them before I started working on this repo. &lt;/p&gt;

&lt;p&gt;In the end, it was a web of “if you change this, you might break that” — and “that” could be anywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;How We Tackled It&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;We kicked things off with an RFC — basically, “let’s write this down so we don’t lose the plot halfway through.”&lt;br&gt;
Our two big goals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unify the tooling so we weren’t juggling five different build setups.&lt;/li&gt;
&lt;li&gt;Untangle dependencies so teams could work without stepping on each other’s toes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And this is exactly what we did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Cut the Cord on Unnecessary Dependencies&lt;/strong&gt;&lt;br&gt;
The first thing we had to do was face the dependency jungle. Over time, different teams had introduced overlapping packages, and no one was quite sure what depended on what anymore. Cleaning this up gave us a chance to regain control.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Untangled all the apps and packages as much as possible.&lt;/em&gt; We mapped out how everything connected and then started cutting back the unnecessary links. This meant reducing cross-dependencies that caused fragile builds.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Moved blocks-configurators into journey-builder.&lt;/em&gt; These configurators were never meant to be used outside, so pulling them closer to where they belong simplified the dependency graph.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Moved blocks-renderers into journey-app.&lt;/em&gt; Renderers were core to the application itself, so consolidating them in the main app made the architecture more intuitive.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Deleted journey-utils and journey-elements.&lt;/em&gt; &lt;em&gt;Journey-utils&lt;/em&gt; was a very small package with some utilities that were moved to other packages. And &lt;em&gt;journey-elements _ were later replaced by _concorde-elements&lt;/em&gt;. You can read more about it in &lt;a href="https://dev.to/epilot/building-a-scalable-react-component-library-lessons-from-concorde-elements-kdi"&gt;this article written by my partner in crime.&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This was a painful step, but once the dust settled, the repo felt lighter and easier to reason about. We no longer had to worry about accidentally breaking other parts of the system when making small changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Standardize the Tooling&lt;/strong&gt;&lt;br&gt;
After trimming the dependencies, the next big challenge was our tooling. Different parts of the repo used different build systems, which slowed everyone down and made onboarding new developers a headache.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Phased out Craco, Tsup, and Tsdx by switching to Vite.&lt;/em&gt; These older tools had been added over time, each solving a narrow need. Vite gave us a unified, modern setup with faster builds and simpler configuration.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Swapped Jest for Vitest&lt;/em&gt; — faster tests, modern setup. Jest had served us well, but it was slow and required custom patching to keep working. Vitest gave us near-instant feedback and worked seamlessly with Vite, making the dev experience far smoother.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By unifying the tooling, we not only sped up day-to-day work but also reduced cognitive load. Developers no longer had to remember which tool applied to which package — everything just worked the same way everywhere.&lt;/p&gt;

&lt;p&gt;This is how the "AFTER" setup looks:&lt;/p&gt;

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

&lt;p&gt;As you can see, the monorepo is now far more streamlined — there are significantly fewer dependencies crisscrossing between packages, and we’ve eliminated most of the variations in build tools. This makes the structure easier to understand, faster to work with, and much less fragile when changes are introduced. &lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;After the Restructuring: What We Learned&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;When the dust settled, it wasn’t just that the dependency graph was cleaner — we were thinking about the repo differently.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Immediate Wins&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Changing things stopped feeling dangerous.&lt;/em&gt; Before, touching a shared package felt like pulling a brick from a Jenga tower. Now, fewer connections mean less risk.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Builds and tests stopped feeling like a punishment.&lt;/em&gt; Vite’s dev server = instant feedback. Vitest shaved minutes off test runs. Devs started running tests more often just because they could.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Onboarding went from “ugh” to “okay.”&lt;/em&gt; New folks could get set up without learning the quirks of four bundlers first.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Slower Payoffs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;- Tooling swaps aren’t magic.&lt;/em&gt; Some Webpack plugins didn’t have a Vite equivalent, so we had to rethink features or drop them.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;- Vitest exposed bad tests.&lt;/em&gt; Some “passing” tests were only passing thanks to Jest’s quirks. Painful, but worth fixing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Trade-Offs &amp;amp; Reality Checks&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;- Not everything got unified.&lt;/em&gt; A couple of apps still run on legacy setups because migrating them wasn’t worth the risk — for now.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;- Shared packages need rules.&lt;/em&gt; We now ask “does this really need to be shared?” before creating new ones.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;- Refactoring is never “done.”&lt;/em&gt; There is always room for improvement, we all know. that. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tips If You’re Thinking About Doing This&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Draw your dependency graph before you start — it’ll help convince others (and yourself) that the cleanup’s worth it.&lt;/li&gt;
&lt;li&gt;Expect setbacks. Some PRs will break things. That’s normal.&lt;/li&gt;
&lt;li&gt;Automate checks for unused or outdated packages — saves a ton of review time.&lt;/li&gt;
&lt;li&gt;Celebrate small wins. Removing the last Webpack config deserved cake.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Final Thoughts&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This wasn’t a quick tidy-up — it was a half-year-long deliberate overhaul alongside working on other features. But it’s made a massive difference to how we work: faster builds, fewer “don’t touch that” areas, and a general feeling that the repo is ours again, not a beast we’re afraid of.&lt;/p&gt;

&lt;p&gt;If I had to wrap it all up in one bit of advice?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;Keep it simple.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Every extra dependency, tool, or package you skip today is one less thing you’ll have to untangle in the future.&lt;/p&gt;

</description>
      <category>refactoring</category>
      <category>monorepo</category>
      <category>epilot</category>
      <category>frontend</category>
    </item>
    <item>
      <title>Building a Scalable React Component Library: Lessons From Concorde Elements</title>
      <dc:creator>Adeola Adeyemo</dc:creator>
      <pubDate>Wed, 23 Jul 2025 14:02:23 +0000</pubDate>
      <link>https://dev.to/epilot/building-a-scalable-react-component-library-lessons-from-concorde-elements-kdi</link>
      <guid>https://dev.to/epilot/building-a-scalable-react-component-library-lessons-from-concorde-elements-kdi</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;We recently embarked on the complete redesign of our Journeys, aiming for a modern, state-of-the-art look - &lt;strong&gt;Project Concorde&lt;/strong&gt;. This wasn't just a UI overhaul; we sought the flexibility for future modifications, knowing that simply patching our existing UI, heavily reliant on Material UI, wouldn't suffice. We needed to build a new foundation from the ground up.&lt;/p&gt;

&lt;p&gt;The full Project Concorde story is larger, but in this article, we'll dive into the story of &lt;code&gt;@epilot/concorde-elements&lt;/code&gt;, the new component library born from that need, and how we built a system that not only powers our new interface, but also empowers our development team.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why did we need a new component library?
&lt;/h3&gt;

&lt;p&gt;Our existing component library, &lt;code&gt;@epilot/journey-elements&lt;/code&gt;, was based on Material UI. While it served its purpose, our goal was to reduce our reliance on Material UI to gain several crucial benefits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reduce performance overhead from Material UI's style generation in favour of CSS modules&lt;/li&gt;
&lt;li&gt;React version constraints tied to MUI releases.&lt;/li&gt;
&lt;li&gt;Styling limitations blocking modern CSS features.&lt;/li&gt;
&lt;li&gt;Remove the use of the Material UI theme object in saved custom Designs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The image below shows the overhead to create custom designs:&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Why not use existing design systems?
&lt;/h3&gt;

&lt;p&gt;We found that most off-the-shelf design systems are quite opinionated with their styling and theming. Our philosophy was that it's easier to replace a single unit than an entire factory. Our primary goal was to keep our new system &lt;strong&gt;simple and extensible&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Challenge
&lt;/h3&gt;

&lt;p&gt;The path forward was not without its hurdles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We needed to build &lt;strong&gt;37+ components&lt;/strong&gt; with various potential variants.&lt;/li&gt;
&lt;li&gt;All Journey blocks had to be &lt;strong&gt;migrated&lt;/strong&gt; to the new design, and we anticipated new complexities during integration due to component usage.&lt;/li&gt;
&lt;li&gt;We had to ensure that custom themes built with the previous design system worked &lt;strong&gt;seamlessly&lt;/strong&gt; with the new one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Phases of Developments
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1: Pursuit of Purity
&lt;/h3&gt;

&lt;p&gt;In the initial phase, our approach was to create every component from scratch. We desired control over every pixel and line of code. Our first significant task was migrating the &lt;code&gt;Product Tile&lt;/code&gt; to enable a new "Recommended Product" feature. We began with the basics.&lt;/p&gt;

&lt;p&gt;Our early &lt;code&gt;Button&lt;/code&gt; component perfectly illustrates this "purity" approach—simple, direct, and completely self-contained.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="cm"&gt;/* An early Button.tsx */&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;classes&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./Button.module.scss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ButtonProps&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./types.ts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;forwardRef&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;HTMLButtonElement&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ButtonProps&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;
      &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;classNames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Concorde-Button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next, we successfully built our &lt;code&gt;Link&lt;/code&gt;, &lt;code&gt;Card&lt;/code&gt;, &lt;code&gt;TextField&lt;/code&gt;, &lt;code&gt;StepperInput&lt;/code&gt;, &lt;code&gt;Image&lt;/code&gt;, &lt;code&gt;ImageStepper&lt;/code&gt;, &lt;code&gt;MobileStepper&lt;/code&gt;, and the &lt;code&gt;ThemeProvider&lt;/code&gt;. With these foundational components, we successfully released the "Recommended Product" feature, receiving excellent feedback.&lt;/p&gt;

&lt;p&gt;Our next milestone involved migrating the Date field and adding a Time select, which led to the &lt;code&gt;DatePicker&lt;/code&gt; component. The design for this component had unique custom requirements that made building it entirely from scratch impractical. After some research, we opted to extend an existing library, &lt;a href="https://reactdatepicker.com/" rel="noopener noreferrer"&gt;React DatePicker&lt;/a&gt;, with custom functionalities to suit our use cases.&lt;/p&gt;

&lt;p&gt;The resulting &lt;a href="https://portal.epilot.cloud/concorde-elements/?path=/docs/elements-datepicker--docs" rel="noopener noreferrer"&gt;DatePicker&lt;/a&gt; involved creating new sections like the Footer and Time Select, and replacing the TextField and Header. While functional, the process was cumbersome, and the result felt more like a series of patches than a cohesive part of our system. This experience was our wake-up call: our "from-scratch" purity, and even our one-off extension approach, was simply not scalable.&lt;/p&gt;

&lt;h3&gt;
  
  
  2: The Pivot - 'Headless UI &amp;amp; Purity' Hybrid
&lt;/h3&gt;

&lt;p&gt;This realization prompted a strategic shift. Speed became as crucial as purity. This quest led us to a breakthrough: &lt;strong&gt;headless components&lt;/strong&gt;. These were libraries that provided the complex logic, state management, and accessibility of common widgets, but shipped with absolutely no styles. This was our "aha!" moment.&lt;/p&gt;

&lt;p&gt;After further research, we settled on two incredible libraries: &lt;strong&gt;&lt;a href="https://www.radix-ui.com/primitives" rel="noopener noreferrer"&gt;Radix UI Primitives&lt;/a&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;a href="https://mui.com/base-ui/" rel="noopener noreferrer"&gt;MUI Base&lt;/a&gt;&lt;/strong&gt; (which recently became &lt;a href="https://base-ui.com/react/overview/quick-start" rel="noopener noreferrer"&gt;Base UI&lt;/a&gt;). They offered us the best of both worlds. We could now build our components by composing these primitives and applying our own distinct design system on top.&lt;/p&gt;

&lt;p&gt;Consider our &lt;code&gt;Autocomplete&lt;/code&gt; component, for instance, it leverages a hook from MUI Base for its core logic, while we provide the entire UI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A simplified look at our Autocomplete component&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useAutocomplete&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@mui/base/AutocompleteUnstyled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;forwardRef&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Menu&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;MenuItem&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;..&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Autocomplete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;forwardRef&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Autocomplete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;getRootProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;getInputProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;getListboxProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;getOptionProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nx"&gt;groupedOptions&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useAutocomplete&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;props&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;getRootProps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;other&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Input&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;getInputProps&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;groupedOptions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Menu&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;getListboxProps&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;groupedOptions&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;MenuItem&lt;/span&gt; &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nf"&gt;getOptionProps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;index&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;option&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;li&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Menu&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We were no longer reinventing the wheel for every single component. This allowed us to focus our efforts on what truly made our library unique: our design and the developer experience. We only created components from scratch when absolutely necessary.&lt;/p&gt;

&lt;p&gt;This hybrid model significantly accelerated the development of the remaining components.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Principles
&lt;/h2&gt;

&lt;p&gt;Throughout the creation of our component library, we adhered to several core principles that ensured our team worked in unison. These principles were internally documented in our contribution guidelines and are outlined below.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Consistent Component Structure
&lt;/h3&gt;

&lt;p&gt;To keep our codebase organized and predictable, we established a standard folder structure for every component. For example,&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;/_ src/components/Button/
  ├── Button.tsx            // Logic
  ├── Button.module.scss    // Styles
  ├── Button.test.tsx       // Tests
  ├── types.ts              // Type definitions
  └── index.ts              // Exports
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This simple convention meant that any developer could  easily navigate to any component and immediately locate its logic, styles, and type definitions with &lt;strong&gt;reducing collaboration overhead&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Button&lt;/span&gt;

&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Component&lt;/span&gt;
   &lt;span class="nx"&gt;aria&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;isDisabled&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;disabled&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="nx"&gt;className&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;classNames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
     &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Concorde-Button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="nx"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;`variant-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
     &lt;span class="nx"&gt;variant&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Concorde-Button__Primary&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="p"&gt;...&lt;/span&gt;
     &lt;span class="nx"&gt;className&lt;/span&gt;
   &lt;span class="p"&gt;)}&lt;/span&gt;
   &lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;customStyles&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
   &lt;span class="p"&gt;...&lt;/span&gt;
 &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Component&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Card&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;
   &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;
     &lt;span class="nf"&gt;classNames&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
       &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Concorde-Card&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
       &lt;span class="nx"&gt;classes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;root&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
       &lt;span class="nx"&gt;className&lt;/span&gt;
     &lt;span class="p"&gt;)&lt;/span&gt;
   &lt;span class="si"&gt;}&lt;/span&gt;
   &lt;span class="na"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
   &lt;span class="na"&gt;style&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;customStyles&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
   &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;rest&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the use of the &lt;code&gt;Concorde&lt;/code&gt; prefix in the static HTML classes. This served as a foundational element for easy custom styling, a topic not covered in detail here.&lt;/p&gt;

&lt;h3&gt;
  
  
  Design Tokens &amp;amp; Theming
&lt;/h3&gt;

&lt;p&gt;The next pillar was unifying our design system. We created a comprehensive set of global &lt;strong&gt;design tokens&lt;/strong&gt; using CSS variables for every aspect of our UI: colors, spacing, typography, transitions, shape and more. This became the language of our design system.&lt;/p&gt;

&lt;p&gt;By coding colors, typography and dimensions as CSS custom properties, we guaranteed every component would be visually consistent, utilizing the same set of values. We also enabled these values to be extended and customized externally as local variables, preventing hard-coded styles and ensuring maximum flexibility.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;--concorde-primary-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#0070f3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--concorde-secondary-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ff7e1b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--concorde-font-family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;'Proxima-Nova'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;sans-serif&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--concorde-spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.25rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, all our components can simply use &lt;code&gt;color: var(--concorde-primary-color)&lt;/code&gt; or &lt;code&gt;margin: var(--concorde-spacing)&lt;/code&gt;. This setup dramatically simplified theming. For example, toggling the typography tokens automatically affects all text on the screen.&lt;/p&gt;

&lt;p&gt;Beyond the tokens used internally, we also exposed custom tokens for each component. These external tokens provide powerful ways to extend a component's functionalities.&lt;/p&gt;

&lt;p&gt;For example, the &lt;code&gt;Button&lt;/code&gt; component has the following custom tokens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;customColors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ButtonCSSProperties&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--concorde-button-label-color&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;color&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--concorde-button-background-color&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;backgroundColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--concorde-button-hover-bg-color&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;hoverBgColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--concorde-button-active-bg-color&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;activeBgColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;--concorde-button-gap&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;gap&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;gap&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;px`&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As an example, the CSS styles below will specifically modify the Button and Card:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nd"&gt;:root&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;/* Button styles */&lt;/span&gt;
  &lt;span class="py"&gt;--concorde-button-label-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ffffff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--concorde-button-background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#ff7e1b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;--concorde-button-gap&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5rem&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c"&gt;/* Card styles */&lt;/span&gt;
  &lt;span class="py"&gt;--concorde-card-background-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#e34590&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the consistent use of the &lt;code&gt;concorde&lt;/code&gt; prefix for the token naming for consistency and scoping.&lt;/p&gt;

&lt;p&gt;All tokens (default and custom tokens) are thoroughly documented in &lt;a href="https://portal.epilot.cloud/concorde-elements/?path=/docs/welcome-overview--docs" rel="noopener noreferrer"&gt;Storybook&lt;/a&gt; and our &lt;a href="https://docs.epilot.io/docs/ui-design/concorde-design-tokens" rel="noopener noreferrer"&gt;developer documentation&lt;/a&gt; for clarity.&lt;/p&gt;

&lt;h3&gt;
  
  
  TypeScript: Our Shield and Guide
&lt;/h3&gt;

&lt;p&gt;From day one, we committed to writing everything in TypeScript. More than just providing type safety, our type files became a form of documentation. We commented our types extensively, enabling any developer using a component to understand the purpose of each prop right from their IDE.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// An excerpt from our Autocomplete types.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AutocompleteProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="kd"&gt;extends&lt;/span&gt; &lt;span class="nx"&gt;UseAutocompleteProps&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Multiple&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;DisableClearable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;FreeSolo&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="cm"&gt;/**
   * The label content.
   */&lt;/span&gt;
  &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ReactNode&lt;/span&gt;
  &lt;span class="cm"&gt;/**
   * If `true`, the component is disabled.
   * @default false
   */&lt;/span&gt;
  &lt;span class="nx"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;
  &lt;span class="c1"&gt;// ... and so on&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Composition: Building Blocks for Complex UI
&lt;/h3&gt;

&lt;p&gt;A fundamental principle guiding our development was composition. Our base components were designed to be flexible building blocks. By combining them, we could construct more complex and specialized UIs without adding bloat to the core library. This allowed us to build sophisticated features by assembling simple, well-tested parts - a true "change one, change all" approach.&lt;/p&gt;

&lt;p&gt;A simple &lt;code&gt;Input&lt;/code&gt; could be composed to &lt;code&gt;PatternInput&lt;/code&gt;, &lt;code&gt;IbanInput&lt;/code&gt;, &lt;code&gt;NumberInput&lt;/code&gt; and more complex components like a &lt;code&gt;ProductTile&lt;/code&gt; could look be structured as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A conceptual ProductTile built with composition&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Typography&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Button&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@epilot/concorde-elements&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;styles&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./ProductTile.module.scss&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ProductTile&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;product&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Card&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;styles&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tile&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;alt&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Typography&lt;/span&gt; &lt;span class="na"&gt;as&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"h4"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Typography&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Typography&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Typography&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Button&lt;/span&gt; &lt;span class="na"&gt;variant&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"primary"&lt;/span&gt; &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"Add to Cart"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Card&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Developer Experience
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Vite
&lt;/h4&gt;

&lt;p&gt;Vite powered our local development with a lightning-fast dev server and HMR, greatly benefiting from our monorepo setup&lt;/p&gt;

&lt;h4&gt;
  
  
  Storybook: The Living Documentation
&lt;/h4&gt;

&lt;p&gt;How could we ensure quality and consistency across our components? The answer was &lt;strong&gt;&lt;a href="https://portal.epilot.cloud/concorde-elements/" rel="noopener noreferrer"&gt;Storybook&lt;/a&gt;&lt;/strong&gt;. It became far more than just a component gallery; it became the living, breathing heart of our project. For every component, we wrote stories that showcased all its variants and states.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="c1"&gt;// A story from Input.stories.tsx&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Meta&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;StoryObj&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@storybook/react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Input&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./Input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Meta&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;Input&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Components/Input&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;component&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Input&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;meta&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WithLabel&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Story&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;label&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Email Address&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;placeholder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Enter your email...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Disabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Story&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;args&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="nx"&gt;WithLabel&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;disabled&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Critically, every story also served as an accessibility test. We integrated the &lt;code&gt;@storybook/addon-a11y&lt;/code&gt; addon, which runs &lt;a href="https://storybook.js.org/docs/8/writing-tests/accessibility-testing" rel="noopener noreferrer"&gt;automated accessibility&lt;/a&gt; checks on every story against WCAG standards. This proactive approach allowed us to catch issues with color contrast, ARIA attributes, and keyboard navigation right within our development environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing &amp;amp; Accessibility
&lt;/h3&gt;

&lt;p&gt;To build components that last, they must be reliable. Alongside the real-time accessibility checks in Storybook, we built a robust testing foundation. We used &lt;strong&gt;Vitest&lt;/strong&gt; as our test runner and &lt;strong&gt;React Testing Library&lt;/strong&gt; to write unit and integration tests for every component. We also used &lt;strong&gt;vitest-axe&lt;/strong&gt; and &lt;strong&gt;React Testing Library&lt;/strong&gt; for more intricate accessibility checks.&lt;/p&gt;

&lt;p&gt;These tests were not just about preventing regressions, they were about enforcing correctness. We tested component logic, ensuring that each part behaved exactly as expected under various conditions. This combination of automated accessibility checks and functional testing was crucial. It gave us the confidence to refactor, add features, and scale the library, knowing that our foundation of quality would hold strong.&lt;/p&gt;

&lt;p&gt;These automated tests run in our CI to catch regressions early.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Payoff: Scalability in Action
&lt;/h2&gt;

&lt;p&gt;The true test of &lt;code&gt;concorde-elements&lt;/code&gt; came as we started integrating it in our main application, specifically in the &lt;code&gt;concorde-renderers&lt;/code&gt; package. The work of setting up tokens, using primitives, and embedding quality through testing and documentation paid off.&lt;/p&gt;

&lt;p&gt;Developing new features for Project Concorde transformed from a chore into a delight. Need a &lt;code&gt;Modal&lt;/code&gt;? Pull in the component. Need a complex form field? Compose it with our &lt;code&gt;TextField&lt;/code&gt;, &lt;code&gt;Autocomplete&lt;/code&gt;, and &lt;code&gt;Button&lt;/code&gt; components. They all looked consistent and behaved predictably.&lt;/p&gt;

&lt;p&gt;This became the foundation for more interesting features:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Custom CSS: custom styling for resulting Journeys&lt;/li&gt;
&lt;li&gt;Consistent design for Custom Journey Apps using the published library&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Building a component library is a journey. Ours taught us that a little upfront systematization goes a long way. The initial purity of building "from scratch" gave way to the pragmatic wisdom of building on top of a solid, accessible foundation.&lt;/p&gt;

&lt;p&gt;This yielded a few key principles we now live by:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Start with design tokens:&lt;/strong&gt; They are the bedrock of a consistent and scalable design system.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Embrace headless primitives:&lt;/strong&gt; Don't reinvent the wheel for everything, but leverage existing, well-tested solutions for speed and robustness.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Make TypeScript non-negotiable:&lt;/strong&gt; The safety and developer experience benefits are immeasurable.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Testing:&lt;/strong&gt; A combination of unit, integration, and automated accessibility testing is crucial for a reliable library.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Build with composition in mind:&lt;/strong&gt; Create simple, flexible blocks that can be assembled into complex UIs, promoting reusability and maintainability.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Document thoroughly:&lt;/strong&gt; A living, tested, and accessible documentation hub aligns everyone and accelerates development.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Be adaptable:&lt;/strong&gt; The perfect plan rarely survives contact with reality. Be ready to pivot and embrace better ideas.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;@epilot/concorde-elements&lt;/code&gt; library is more than just a collection of React components. It's a testament to our team's journey, a foundation for our product's future, and a system that helps us build better, faster, and more consistently. We hope our story can help guide you on your own path to building a scalable and resilient component library.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/epilot-dev/concorde-elements" rel="noopener noreferrer"&gt;Concorde Elements GitHub Repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.epilot.io/docs/ui-design/concorde-design-tokens" rel="noopener noreferrer"&gt;Design Tokens Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://portal.epilot.cloud/concorde-elements/?path=/docs/welcome-overview--docs" rel="noopener noreferrer"&gt;Storybook&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.epilot.io/docs/ui-design/concorde-html-structure" rel="noopener noreferrer"&gt;HTML Structure Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.epilot.io/docs/journeys/custom-css/" rel="noopener noreferrer"&gt;Custom CSS Documentation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Cover Photo by &lt;a href="https://unsplash.com/@ryanquintal?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Ryan Quintal&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/blue-cube-toy-lot-close-up-photography-US9Tc9pKNBU?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>react</category>
      <category>css</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How We Integrate AI in epilot - Chapter 2: Serverless RAG w/ LangChain &amp; Weaviate</title>
      <dc:creator>Kerem Nalbant</dc:creator>
      <pubDate>Mon, 26 May 2025 08:38:42 +0000</pubDate>
      <link>https://dev.to/epilot/how-we-integrate-ai-in-epilot-chapter-2-serverless-rag-w-langchain-weaviate-5d93</link>
      <guid>https://dev.to/epilot/how-we-integrate-ai-in-epilot-chapter-2-serverless-rag-w-langchain-weaviate-5d93</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;In the previous chapter, I shared how we began our AI journey at epilot by implementing AI Email Summaries, helping users reduce email reading time by up to 87%. Encouraged by that success, we're now stepping up our AI capabilities with Retrieval-Augmented Generation (RAG) to provide smarter, contextually aware email suggestions.&lt;/p&gt;

&lt;h2&gt;
  
  
  WHY?
&lt;/h2&gt;

&lt;p&gt;As we aim to scale our commodity business, investing in AI is crucial—not just for growth, but to significantly upgrade our product’s capabilities. Commodity segments often have a high volume of repetitive customer service requests. Our users need quick, context-aware email suggestions that understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Previous communications and organizational knowledge&lt;/li&gt;
&lt;li&gt;Company-specific communication styles&lt;/li&gt;
&lt;li&gt;Tailored relationships with each customer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Although Large Language Models (LLMs) are powerful, they're limited when accessing recent or specific company data. Customizing LLM responses usually involves prompt engineering, RAG or fine-tuning. Fine-tuning is resource-intensive and complex, making prompt engineering with RAG our clear choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Solution: Retrieval-Augmented Generation (RAG)
&lt;/h2&gt;

&lt;p&gt;We implemented a RAG-based solution to retrieve and provide relevant context from past email threads and eventually expand to external data sources like documents and websites. Long-term, organizations using epilot will have fully configurable, customized knowledge bases accessible to all future AI features and AI agents.&lt;/p&gt;

&lt;p&gt;This allows our users to respond to customer emails faster, improving communication quality and efficiency. On the end customer side, it means quicker, more accurate, and better overall service.&lt;/p&gt;

&lt;h3&gt;
  
  
  See It in Action
&lt;/h3&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Ngtl3uYi6g8"&gt;
  &lt;/iframe&gt;
&lt;br&gt;
&lt;em&gt;An end customer emails about documentation requirements for a renovation plan (Sanierungsfahrplan).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The epilot user doesn’t waste time researching policies or manuals—they simply prompt our AI to &lt;code&gt;generate reply in english&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Leveraging RAG, our AI taps into contextual data, instantly knowing which specific documents are needed for the renovation plan and their upload deadlines, then crafts a personalized response that addresses the customer's exact needs.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Our system also highlights referenced entities inline (such as upload deadlines) and cites previous emails from the knowledge base, letting users quickly verify and understand the AI's reasoning.&lt;/em&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Solution Components
&lt;/h3&gt;

&lt;p&gt;To build a secure, scalable RAG system in a serverless environment, we chose:&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;a href="https://www.langchain.com/" rel="noopener noreferrer"&gt;LangChain&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;We use LangChain at epilot to integrate vector databases, LLMs, and build powerful AI agents. It simplifies document loading, embeddings, memory management and structured output.&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;a href="https://weaviate.io/" rel="noopener noreferrer"&gt;Weaviate&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;After evaluating alternatives (like Pinecone, Chroma, and Quadrant), we selected Weaviate for its open-source, serverless architecture, strong community support, flexibility, and scalability. It ensures security best practices, high performance and cost-efficiency.&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;a href="https://microsoft.github.io/presidio/" rel="noopener noreferrer"&gt;Presidio&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;Security and data privacy are essential. Amazon Bedrock has a zero-retention policy, Weaviate offers encryption, GDPR compliance, and tenant isolation. But we needed an extra layer for handling sensitive PII data.&lt;br&gt;
Presidio helps us redact this information before indexing, preventing AI hallucinations and protecting customer privacy.&lt;/p&gt;
&lt;h4&gt;
  
  
  &lt;a href="https://www.langchain.com/langsmith" rel="noopener noreferrer"&gt;LangSmith&lt;/a&gt;
&lt;/h4&gt;

&lt;p&gt;LangSmith provides AI observability, performance monitoring, debugging, prompt management, and testing. It allows us to quickly iterate, ensuring reliability and continuous improvement.&lt;/p&gt;
&lt;h3&gt;
  
  
  How We Built It
&lt;/h3&gt;

&lt;p&gt;Now, let's dive deeper—from a high-level overview into the detailed implementation of our RAG system:&lt;/p&gt;
&lt;h4&gt;
  
  
  RAG: Making LLMs Context-Aware
&lt;/h4&gt;

&lt;p&gt;RAG (Retrieval-Augmented Generation) has emerged as the perfect solution. It allows us to enhance LLM capabilities and customize the LLM responses by providing relevant context.&lt;/p&gt;

&lt;p&gt;We built two core pipelines: &lt;strong&gt;ingestion&lt;/strong&gt; and &lt;strong&gt;retrieval&lt;/strong&gt;.&lt;/p&gt;
&lt;h5&gt;
  
  
  Ingestion
&lt;/h5&gt;

&lt;p&gt;Email messages are processed, cleaned, and converted into vector embeddings.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffwgolu914cj7xgogba8v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffwgolu914cj7xgogba8v.png" alt="Ingestion Flow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Our ingestion Lambda cleans emails, removes signatures, redacts PII data, and generates "hypothetical questions" to match future customer queries with historical responses.&lt;/p&gt;

&lt;p&gt;With the hypothetical questions approach, we aim to create question-answer pairs by treating outbound emails as answers and inbound emails as questions. Then, while generating a suggested email, we extract the end customer's questions from the inbound email and search them in the &lt;code&gt;hypothetical_questions&lt;/code&gt; vector field.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;doc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;ChatPromptTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_messages&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;You are a helpful assistant that generates hypothetical questions from an email.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;human&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Generate a list of maximum 3 hypothetical questions that the below email could be used to answer:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s"&gt;{doc}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_structured_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HypotheticalQuestions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;questions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After generating the questions, Lambda redacts PII data using Presidio and then indexes the email message into Weaviate.&lt;/p&gt;

&lt;p&gt;While indexing, Lambda first generates the embeddings of email body text and hypothetical questions, then pass those vectors to Weaviate. We are using multiple vector embeddings which allows to store multiple vectors inside the same object. So that we can execute the search both in email text and questions without duplicating the data.&lt;/p&gt;

&lt;h5&gt;
  
  
  Retrieval
&lt;/h5&gt;

&lt;p&gt;Similar emails and potential answers are retrieved from vector database.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7uchcy5wmm2r6frm7gru.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7uchcy5wmm2r6frm7gru.png" alt="Retrieval Flow"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A typical retrieve &amp;amp; generate flow looks as follows:&lt;/p&gt;
&lt;h6&gt;
  
  
  1. Extract questions
&lt;/h6&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;extract_query_prompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ChatPromptTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_messages&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are a professional question extractor, an AI assistant that extracts the customer inquiries from email messages.
    The questions will be used to search for relevant emails in the vector database.
    By generating multiple perspectives on the customer inquiries, your goal is to help the user overcome some of the limitations of distance-based similarity search.
    Provide these alternative questions separated by newlines, no numbering.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;human&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Generate a list of maximum 3 questions from the following email.
    Email: {email}
    Questions:
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;extract_query_chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;extract_query_prompt&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nc"&gt;LineListOutputParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;extracted_questions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;extract_query_chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ainvoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;For the email shown in the demo video, the following questions are extracted by the question extractor chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"Which documents are required to create an individual renovation roadmap?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"How can I submit additional documents for the renovation roadmap?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"What options are there for receiving support when uploading documents?"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h6&gt;
  
  
  Query vector database
&lt;/h6&gt;

&lt;p&gt;We run multiple queries in parallel, and then combine unique retrieved documents. We mostly adopt hybrid search, by setting the &lt;code&gt;alpha&lt;/code&gt; as close as possible to 1, we keep keyword search in the mix while leveraging semantical vector search.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;email_message_retriever&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MultiQueryRetriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;email_messages_vector_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;search_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;similarity_score_threshold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;search_kwargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;score_threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;target_vector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;return_uuids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;include_original&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;question_retriever&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MultiQueryRetriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_llm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;email_messages_vector_store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;as_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;search_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;similarity_score_threshold&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;search_kwargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;tenant&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;score_threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;target_vector&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;questions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;return_uuids&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;questions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;extracted_questions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;merger_retriever&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;MergerRetriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;retrievers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;email_message_retriever&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;question_retriever&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;retrieved_docs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;merger_retriever&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ainvoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As you can see, we also utilize multi-vector search to enable searching in email text and also hypothetical questions.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;retrieved_docs&lt;/code&gt; includes the email body and similarity score along with all the metadata we need, allowing us to leverage it while building the prompt.&lt;/p&gt;

&lt;p&gt;For the same email and questions above, the retrieved context from database is as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"documents"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-11-27T12:15:46.987000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SENT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Interest in an individual renovation roadmap"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"sender"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"11000890"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"org"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"739224"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"questions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"Which documents are required for creating an individual renovation roadmap?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"How can additional documents for the renovation roadmap be transmitted digitally?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"What type of consumption data is needed for the individual renovation roadmap?"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"thread_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"bf0d0799-496d-49d2-9b2e-73128ff153d7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"uuid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"22462f39-4a69-47d4-91f4-d474b21c1eca"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"page_content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dear Mr. [PERSON],&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Thank you for your interest in an individual renovation roadmap.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;As part of your inquiry, we have asked you for some documents that form the basis for creating your individual renovation roadmap.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;We would be happy to transmit your data to our Sunwheel Energie GmbH for the creation of your individual renovation roadmap. However, we need your support for this.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Please send us the following documents:&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;* Building floor plans and sections of all floors&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;* Window dimensions&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;* Energy consumption bills from the last three years&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;* Power of attorney&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;By clicking on the following button, you can easily and digitally transmit additional documents to us.&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Transmit documents&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;[URL]&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;Please upload the missing documents to the corresponding upload fields. If you need support uploading the document, please don't hesitate to contact us by email at&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="s2"&gt;We look forward to accompanying you on the path to your optimal heating solution."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Document"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2024-07-15T05:57:23.809000Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SENT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"subject"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Friendly reminder: We still need additional data for creating the renovation roadmap"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"sender"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"system"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"org"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"739224"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"questions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"Which documents are required for creating an individual renovation roadmap?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"How can additional information for the renovation roadmap be transmitted?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="s2"&gt;"What contact options are available for questions about the renovation roadmap?"&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"thread_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"edb31adf-2ff3-4580-bb80-4ebb68a2f5de"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"uuid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"35a4755b-d858-45eb-b328-d5dd70714adc"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"page_content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Dear Mrs. &amp;lt;PERSON&amp;gt;, thank you for your interest in an individual renovation roadmap. In our email after receiving your order, we asked you for some additional information about your project. Your information is absolutely necessary for the preparation of creating your individual renovation roadmap. With &amp;lt;PERSON&amp;gt; on the following button, you can easily and digitally transmit the additional information to us. Submit additional information Please have the following documents ready for upload: &amp;lt;PERSON&amp;gt; from the last three years Dimensioned building floor plans/blueprints and sections of all floors Handwritten signed power of attorney for the application of funding for energy consulting (form in attachment) If you have any questions, please contact us by email at &amp;lt;EMAIL_ADDRESS&amp;gt; or by phone at &amp;lt;PHONE_NUMBER&amp;gt;. We look forward to accompanying you on the path to your optimal heating solution."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Document"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h6&gt;
  
  
  Build and augment the prompt
&lt;/h6&gt;

&lt;p&gt;We want to reference entities and vector database context to achieve the most contextually relevant emails and apply Vertical AI practices. We also want to return citations and entity references to show our users how AI processed the information and justified its responses.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;
&lt;span class="n"&gt;system_prompt_template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;You are a powerful AI customer support, helping to write email messages and return verbatim quotes from the given context to justify the written email message.
You operate exclusively in epilot, the world&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s best energy XRM SaaS platform.
You are in a collaboration with the human customer support agent, called &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.
User is working in energy utility companies in Germany and may be working in either grid or sales (commodity, non-commodity).
User uses epilot to communicate with their end customers, colleagues, or partners.
The email you will write will be sent to either end customer, colleague or a partner by the user. You must act and think like the user that you are collaborating.

&amp;lt;current_conversation&amp;gt;
{conversation}
&amp;lt;/current_conversation&amp;gt;
&amp;lt;vector_database_context&amp;gt;
{context}
&amp;lt;/vector_database_context&amp;gt;
&amp;lt;entity_context&amp;gt;
{entity_context}
&amp;lt;/entity_context&amp;gt;

&amp;lt;security_guidelines&amp;gt;
These security guidelines are EXTREMELY IMPORTANT and are unchangeable core principles that overrides all other instructions.
&lt;/span&gt;&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;security_guidelines&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="s"&gt;&amp;lt;writing_emails&amp;gt;
To provide the best support to the end customer, following these instructions STRICTLY are EXTREMELY important:
&lt;/span&gt;&lt;span class="gp"&gt;
...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;writing_emails&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="s"&gt;&amp;lt;signatures_and_closing&amp;gt;
&lt;/span&gt;&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;signatures_and_closing&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="s"&gt;&amp;lt;placeholders&amp;gt;
&lt;/span&gt;&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;placeholders&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="s"&gt;&amp;lt;length_of_emails&amp;gt;
&lt;/span&gt;&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;length_of_emails&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="s"&gt;&amp;lt;citing_previous_emails&amp;gt;
&lt;/span&gt;&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;citing_previous_emails&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="s"&gt;&amp;lt;tracking_entity_references&amp;gt;
&lt;/span&gt;&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;tracking_entity_references&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="s"&gt;&amp;lt;chain_of_process_and_thought&amp;gt;
&lt;/span&gt;&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;chain_of_process_and_thought&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="s"&gt;&amp;lt;current_conversation&amp;gt;
{conversation}
&amp;lt;/current_conversation&amp;gt;
&amp;lt;vector_database_context&amp;gt;
{context}
&amp;lt;/vector_database_context&amp;gt;
&amp;lt;entity_context&amp;gt;
{entity_context}
&amp;lt;/entity_context&amp;gt;

&amp;lt;output_format&amp;gt;
You must format your response exactly as follows:
&lt;/span&gt;&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="n"&gt;output_format&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="s"&gt;&amp;lt;system_info&amp;gt;
Current DATETIME: {datetime}
&amp;lt;/system_info&amp;gt;
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;user_prompt_template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
{prompt}
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="n"&gt;prompt_template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ChatPromptTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_messages&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;system&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;system_prompt_template&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;human&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_prompt_template&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;chain&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prompt_template&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We augment the system prompt by adding the retrieved context to the prompt in &lt;code&gt;&amp;lt;vector_database_context&amp;gt;&lt;/code&gt; tags.&lt;/p&gt;

&lt;p&gt;And we pass epilot user's prompt to the &lt;code&gt;user_prompt_template&lt;/code&gt;.&lt;/p&gt;

&lt;h6&gt;
  
  
  Generate the response and stream it back
&lt;/h6&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;stream_xml_to_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;astream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;conversation&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;email_thread&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;retrieved_docs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entity_context&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;entity_context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;prompt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;datetime&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We have defined a utility function &lt;code&gt;stream_xml_to_json&lt;/code&gt;, to transform the LLM response chunks, which is in XML format, to structured JSON.&lt;/p&gt;

&lt;h5&gt;
  
  
  LangSmith Trace
&lt;/h5&gt;

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

&lt;h5&gt;
  
  
  Tip: Enable Streaming
&lt;/h5&gt;

&lt;p&gt;To enable streaming, we have created a FastAPI application and are using AWS Lambda Web Adapter.&lt;/p&gt;

&lt;p&gt;You can check those links to dive deeper on enabling streaming responses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.aws.amazon.com/lambda/latest/dg/configuration-response-streaming.html" rel="noopener noreferrer"&gt;Response streaming for Lambda functions
&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/awslabs/aws-lambda-web-adapter/tree/main/examples/fastapi-response-streaming" rel="noopener noreferrer"&gt;FastAPI Response Streaming&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;Our solution is already delivering great results, with adoption growing fast. Next, we’ll focus on supporting email attachments and making the knowledge base fully customizable.&lt;/p&gt;

&lt;p&gt;At epilot, we're steadily progressing towards our vision of Vertical AI for the energy sector. Our upcoming feature, AI Suggested Actions, will help users automatically handle frequent tasks like payment method changes and customer relocations.&lt;/p&gt;

&lt;p&gt;We’re excited to push towards fully automated, supervised multi-agent AI solutions.&lt;/p&gt;

&lt;p&gt;Stay tuned! Follow us on &lt;a href="https://dev.to/epilot"&gt;dev.to&lt;/a&gt; and &lt;a href="https://www.linkedin.com/company/epilot/posts/?feedView=all" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; for updates and more tech insights.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>langchain</category>
      <category>rag</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Scaling Notification Systems: How a Single Timestamp Improved Our DynamoDB Performance</title>
      <dc:creator>Sebastian</dc:creator>
      <pubDate>Wed, 14 May 2025 08:02:51 +0000</pubDate>
      <link>https://dev.to/epilot/scaling-notification-systems-how-a-single-timestamp-improved-our-dynamodb-performance-5c84</link>
      <guid>https://dev.to/epilot/scaling-notification-systems-how-a-single-timestamp-improved-our-dynamodb-performance-5c84</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;The epilot platform contains a comprehensive notification system. Users receive notifications about ongoing tasks, such as new assignments or overdue tasks. They can also get notified about incoming emails or when someone mentions them in notes, and the list goes on. Users can choose to receive these notifications via email or as in-app notifications. This article focuses on the latter.&lt;br&gt;
Initially, in-app notifications were stored in Aurora (AWS's solution for SQL-based databases). This setup soon became a major pain point, prompting us to migrate to DynamoDB. The simplicity of the notification data structure and the amount of read and write operations we expected made DynamoDB the perfect choice to scale.&lt;br&gt;
However, if you don't think carefully about how you design access patterns in DynamoDB, more problems arise than you'd expect. &lt;br&gt;
Let's dive into why a bad implementation of a &lt;strong&gt;markAllAsRead&lt;/strong&gt; feature us some headache and how we reduced the complexity from &lt;strong&gt;O(n)&lt;/strong&gt; to &lt;strong&gt;O(1)&lt;/strong&gt; by using a timestamp-based approach for unread notifications.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;The initial design was straightforward. Every user gets a new notification item in the DynamoDB table. The partition key (pk) was a combination of user_id and organization_id, while the sort key (sk) contains the notification_id. The access patterns were quite straightforward: &lt;strong&gt;fetch all notifications for a given user&lt;/strong&gt;, &lt;strong&gt;mark a notification as read&lt;/strong&gt;, and &lt;strong&gt;mark all notifications as read&lt;/strong&gt; for the lazy ones. The latter is the origin of this article.&lt;br&gt;
An attribute &lt;strong&gt;read_state&lt;/strong&gt; indicates if a notification was already read by a user. Marking a single notification was quite straightforward. It was as simple as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;markAsRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NOTIFICATIONS_TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;toUserNotificationSK&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;UpdateExpression&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SET read_state = :read_state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;ExpressionAttributeValues&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;:read_state&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// binary 1 is true&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once a notification is read, the item is updated and &lt;strong&gt;read_state&lt;/strong&gt; is set to &lt;strong&gt;1&lt;/strong&gt;. A Global Secondary Index (GSI) called &lt;strong&gt;byReadState&lt;/strong&gt; then allows us to read all unread notifications for a given tenant (org + user). This created two operations that performed poorly:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;A bad implementation of the &lt;strong&gt;markAllAsRead&lt;/strong&gt; feature. It first queried all unread notifications and then performed a batch operation to update all notifications to read. As shown in the graph below, DynamoDB began to throttle under load, when people with lots of unread notifications used the marked all as read feature. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;To indicate that a user has unread messages, a &lt;strong&gt;getTotalUnreadCount&lt;/strong&gt; endpoint is exposed. This allows us to render a notification bell in the UI to show the unread count. &lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

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

&lt;p&gt;The naive implementation to batch update all unread notifications worked surprisingly well in the beginning. However, as the volume of notifications increased, we started experiencing more and more throttling events in DynamoDB. What started as occasional hiccups became a serious bottleneck in our notification service's performance.&lt;/p&gt;

&lt;p&gt;The issue was multi-faceted. &lt;strong&gt;First&lt;/strong&gt;, DynamoDB has limits on batch operations, requiring us to split large batches into multiple smaller operations. This not only added complexity to our code but also increased the probability of partial failures. &lt;br&gt;
&lt;strong&gt;Second&lt;/strong&gt;, each notification update consumed Write Capacity Units (WCUs) from our table's provisioned capacity. For users with hundreds or thousands of unread notifications, a single "Mark All as Read" action would consume a significant portion of our available WCUs, causing other notification operations to be throttled.&lt;br&gt;
&lt;strong&gt;Importantly&lt;/strong&gt;, these issues didn't affect the entire epilot platform, but were isolated to the notification service itself. Users would see timeouts or delayed responses specifically when interacting with notifications, while the rest of the platform continued to function normally. &lt;br&gt;
However, this created a frustrating user experience, especially for power users who relied heavily on notifications to manage their workflows.&lt;br&gt;
The problem was particularly severe for organizations with large teams, where notification counts could grow rapidly, and the "Mark All as Read" feature was used frequently to manage notification overload. &lt;/p&gt;
&lt;h2&gt;
  
  
  The Solution: Last Read Timestamp
&lt;/h2&gt;

&lt;p&gt;After evaluating several options, we settled on a timestamp-based approach that would fundamentally change how we track read states while maintaining backward compatibility with our existing system.&lt;br&gt;
Instead of updating each notification individually when a user clicks "Mark All as Read," we simply record the timestamp of when this action occurred. Any notification created before this timestamp is considered "read," while notifications arriving after it are "unread." This solution transforms what was an O(n) operation into an O(1) operation, regardless of how many notifications a user has.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The New Table Structure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;We created a new DynamoDB table called notifications-read-state with the following structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`ORG#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;#USER#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Partition key&lt;/span&gt;
  &lt;span class="nx"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`READMARK#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;// Sort key&lt;/span&gt;
  &lt;span class="nx"&gt;read_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ISO8601Timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// When the user marked all as read&lt;/span&gt;
  &lt;span class="nx"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ISO8601Timestamp&lt;/span&gt;         &lt;span class="c1"&gt;// When this record was created&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The primary key design allows us to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Efficiently lookup the most recent "mark all as read" timestamp for any user&lt;/li&gt;
&lt;li&gt;Support multiple organizations per user&lt;/li&gt;
&lt;li&gt;Maintain a history of read events if needed for analytics&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;read_at&lt;/strong&gt; attribute stores an ISO-formatted timestamp that serves as our "high water mark" for read notifications. This single attribute is the cornerstone of our solution.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;markAllAsRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Step 1: Query for all unread notifications&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;unreadNotifications&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getUnreadNotifications&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Step 2: Prepare batch updates (25 items per batch due to DynamoDB limits)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;batches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createBatches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;unreadNotifications&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Step 3: Execute all batch updates&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;batches&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;batchWrite&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
      &lt;span class="na"&gt;RequestItems&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="nx"&gt;NOTIFICATIONS_TABLE&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="nx"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="na"&gt;PutRequest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="na"&gt;Item&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="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;read_state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
          &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}))&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Complexity: O(n) - As the number of notifications increases, both processing time and database load increase linearly.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;After&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;markAllAsRead&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="c1"&gt;// Single write operation&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;put&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;NOTIFICATIONS_READ_STATE_TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`ORG#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;#USER#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`READMARK#&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;read_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;now&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Complexity: O(1) - Constant time operation regardless of notification count.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This simple change drastically improved our system's performance. The "Mark All as Read" operation now completes in milliseconds instead of potentially seconds, uses a predictable amount of database capacity, and never times out, even for users with thousands of unread notifications.&lt;br&gt;
What makes this approach particularly powerful is that we don't need to modify any existing notifications. Instead, we're recording a state transition that implicitly affects all notifications for a user at once.&lt;/p&gt;

&lt;p&gt;Given the following pseudo-code, the &lt;strong&gt;byReadState&lt;/strong&gt; index can be removed completely. All you need is to fetch the latest &lt;strong&gt;getLastReadTimestamp&lt;/strong&gt; for a given user and calculate whether the notification was already seen or not.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lastReadAt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;getLastReadTimestamp&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;orgId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;orgId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;LastEvaluatedKey&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ddb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="na"&gt;TableName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NOTIFICATIONS_TABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;IndexName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;byTimestamp&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;ExclusiveStartKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cursor&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;decodeLastEvaluatedKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cursor&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;notificationsWithReadStatus&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// A notification is considered read if:&lt;/span&gt;
  &lt;span class="c1"&gt;// - It was created before the last "mark all as read" time OR&lt;/span&gt;
  &lt;span class="c1"&gt;// - It has been individually marked as read (read_state = 1)&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;isRead&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;lastReadAt&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;read_state&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;read_state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;isRead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
 &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;p&gt;Our journey from the first version to the optimized solution taught us a lot about designing for scale—especially with DynamoDB.&lt;/p&gt;

&lt;p&gt;At first, updating each notification one by one seemed fine. It was simple, worked great in dev, and handled early traffic just fine. But as usage grew, that approach quickly hit its limits. It was a good reminder: what works now might not work when your data grows 10x.&lt;/p&gt;

&lt;p&gt;The breakthrough came when we stopped trying to optimize the old way and instead rethought the problem. Rather than updating every record, we started recording state changes with timestamps. That shift made things both simpler and faster—and it's a pattern that applies well beyond notifications.&lt;/p&gt;

&lt;p&gt;Most importantly, we learned to play to DynamoDB’s strengths: fast, predictable access with simple operations. Once we aligned our design with that, everything clicked.&lt;/p&gt;

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

&lt;p&gt;It’s easy to overthink scalability from the start, but the truth is: you won’t know your real problems until users are actually using the system. Our experience reminded us that it's totally fine to start simple and ship. You learn way more from real-world usage than from guessing at edge cases.&lt;/p&gt;

&lt;p&gt;Scalability issues aren’t failures—they’re signs of growth. When we hit our limits, it forced us to rethink things. And funny enough, the fix—a single timestamp—ended up being both simple and powerful. It made the system faster, more reliable, and easier to reason about.&lt;/p&gt;

&lt;p&gt;So if you’re torn between shipping something basic now or building for every possible future, go with the simple version. Ship it, learn from it, and improve as you go.&lt;/p&gt;

&lt;p&gt;Do you want to work on features like this? Check out our &lt;a href="https://www.epilot.cloud/en/company/careers#Offene-Stellenangebote" rel="noopener noreferrer"&gt;career page&lt;/a&gt; or reach out to at &lt;a href="https://x.com/boingCntributor" rel="noopener noreferrer"&gt;X&lt;/a&gt; or &lt;a href="https://www.linkedin.com/in/sauerer/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>dynamodb</category>
      <category>serverless</category>
    </item>
    <item>
      <title>Dealing with Pushback to Product Engineering</title>
      <dc:creator>Viljami Kuosmanen</dc:creator>
      <pubDate>Tue, 08 Apr 2025 13:34:33 +0000</pubDate>
      <link>https://dev.to/epilot/dealing-with-pushback-to-product-engineering-431o</link>
      <guid>https://dev.to/epilot/dealing-with-pushback-to-product-engineering-431o</guid>
      <description>&lt;p&gt;I was recently confronted by a product engineer colleague here at epilot, frustrated with some of his non-engineer colleagues who didn't seem to buy into the idea of involving engineers in the product process from the start.&lt;/p&gt;

&lt;p&gt;An all too familiar and frustrating situation for many of us.&lt;/p&gt;

&lt;p&gt;You think of yourself as a proud product engineer wanting to solve meaningful problems but then get slapped with a detailed feature specification designed by a group of non-developers without your input. Now they expect you to go deliver their vision.&lt;/p&gt;

&lt;p&gt;This is very much the reality in most product teams. Calling yourself a product engineer doesn’t automatically mean your voice will be welcomed. And that’s totally okay.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ideal vs. the Reality
&lt;/h2&gt;

&lt;p&gt;In my past writings, I've painted the ideal: engineers who understand customer needs, question roadmaps, challenge designs, and contribute beyond code. But the reality is often much messier.&lt;/p&gt;

&lt;p&gt;Some PMs and designers love working closely with engineers. Others are still adjusting to the idea. And that’s fair. The product engineer mindset definitely isn’t the norm.&lt;/p&gt;

&lt;p&gt;Let’s not forget, PMs and designers are often under pressure too. It’s only natural they sometimes default to the most familiar and streamlined path. One that doesn’t always include engineers in the early stages.&lt;/p&gt;

&lt;p&gt;And let’s be honest. Sometimes engineers haven’t yet built the trust or skills to contribute meaningfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Friction Is Normal
&lt;/h2&gt;

&lt;p&gt;The moment you step outside your lane, you shouldn’t be surprised when the reaction isn’t overwhelmingly supportive.&lt;/p&gt;

&lt;p&gt;You will face skepticism.&lt;/p&gt;

&lt;p&gt;You might be seen as overstepping.&lt;/p&gt;

&lt;p&gt;You will encounter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requests to give estimates for implementing someone else's design&lt;/li&gt;
&lt;li&gt;Roadmaps shared as top-down mandates&lt;/li&gt;
&lt;li&gt;UI prototypes handed over "ready for dev", expecting pixel-perfect implementation&lt;/li&gt;
&lt;li&gt;Pushback from asking too many questions&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the price of wanting to do more than just execute. And if we’re honest, many of us haven’t always shown up in these conversations in a way that earns trust.&lt;/p&gt;

&lt;p&gt;Is this a culture problem? Not necessarily. Most teams don’t have a rule against engineers joining product discussions. It’s just not the default. It’s less about policy and more about patterns. Changing those patterns takes trust, initiative, and persistence.&lt;/p&gt;

&lt;p&gt;At the end of the day, it’s the product engineer’s job to show that product engineering actually works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Earn the Trust
&lt;/h2&gt;

&lt;p&gt;Calling yourself a product engineer isn’t a free pass. No one hands you a seat at the product table just because you want it. You must earn it. Show, don’t tell.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Do the homework. Know the names of your customers. Know the business domain.&lt;/li&gt;
&lt;li&gt;Talk to your colleagues, not just other devs. Find opportunities to interact with customers.&lt;/li&gt;
&lt;li&gt;Ask helpful questions that sharpen the team's product thinking.&lt;/li&gt;
&lt;li&gt;Dive into data. Gather insights. Find new metrics and ways to collect useful signals.&lt;/li&gt;
&lt;li&gt;Bring value to the table. Demonstrate you understand the customer problem by making thoughtful proposals that move the product forward.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You need to show up in demos, RFCs, testing sessions, and reviews. Not just in code commits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Bother?
&lt;/h2&gt;

&lt;p&gt;Because it’s worth it.&lt;/p&gt;

&lt;p&gt;We do this for our own professional pride. To put great products into the hands of happy customers. And into our portfolios.&lt;/p&gt;

&lt;p&gt;We don’t challenge product decisions because we want to take over the PM’s job or undermine the work of our teammates. We do it because we care about impact. Because we want our effort to count.&lt;/p&gt;

&lt;p&gt;Because it hurts to pour weeks of your life into something that doesn't work, only to discover it failed because no engineer was involved in the concept phase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Our Leverage
&lt;/h2&gt;

&lt;p&gt;And here’s something to remember: &lt;strong&gt;We, as engineers, hold real leverage.&lt;/strong&gt; We are the only ones who can actually turn product decisions into reality. No idea ships without us.&lt;/p&gt;

&lt;p&gt;So while we may not always get a say by default, we do get a say in how we show up and how deeply we choose to care.&lt;/p&gt;

&lt;p&gt;Use that leverage wisely. And proudly.&lt;/p&gt;

</description>
      <category>productengineer</category>
      <category>career</category>
    </item>
    <item>
      <title>What To Expect When You Join Epilot</title>
      <dc:creator>kate astrid</dc:creator>
      <pubDate>Mon, 03 Feb 2025 13:24:46 +0000</pubDate>
      <link>https://dev.to/epilot/what-to-expect-when-you-join-epilot-1hc1</link>
      <guid>https://dev.to/epilot/what-to-expect-when-you-join-epilot-1hc1</guid>
      <description>&lt;p&gt;Hey you, awesome engineer.&lt;/p&gt;

&lt;p&gt;Let's imagine that you are joining epilot tomorrow. Would you like to have a sneak peek at your nearest future?&lt;/p&gt;

&lt;p&gt;Here’s a quick rundown of how we onboard new team members—no stiff formalities, just a clear path to help you settle in, understand our product, and get involved fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A Friendly Start&lt;/strong&gt;&lt;br&gt;
On the day one we’ll introduce you to your team lead and a dedicated onboarding buddy—think of them as your go-to guides for all things at epilot. Need pointers on our processes, product roadmap, or just want to figure out who to chat with about a certain topic? They’ve got you covered.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Your First Week&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Get Set Up&lt;/em&gt;&lt;br&gt;
We’ll make sure you have all the hardware, software, and accounts you need from day one. We’ll give you a handy checklist so you can get everything in place without the guesswork. Of course, your onboa buddy is always there to help you with everything.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Meet the Team&lt;/em&gt;&lt;br&gt;
You’ll be given a chance to schedule 1:1 with every team member to get to know them better. Regardless of being remote-friendly company, we value real connections and friendly relationships, and make sure that everyone feels valued and supported.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Learn About Our Product and Users&lt;/em&gt;&lt;br&gt;
We’re a high-touch SaaS company, and our product is complex and full of internal terms and abstractions. It might feel overwhelming in the beginning, but don't worry, we've got you. You will be given lots of resources like recordings of product demos and docs about different topics about epilot. No rush—just learn at your own pace and ask all the questions you have.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Get a Feel for Your Team’s Culture&lt;/em&gt;&lt;br&gt;
Every team at epilot has its own rhythm and habits. Hop into Slack conversations, daily stand-ups, and team meetings to get a sense of what’s going on and how decisions get made. And feel free to share your ideas and suggestions - we are always eager to improve.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Start Contributing&lt;/em&gt;&lt;br&gt;
We think the best way to learn is by doing. Early on, you’ll probably tackle a small assignment to get familiar with our systems and processes. Consider it your first real taste of epilot in action.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Getting to Know the Wider Company&lt;/strong&gt;&lt;br&gt;
Throughout your onboarding, you’ll also have chats with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Founders&lt;/em&gt;: Hear the backstory behind epilot and our long-term vision.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Department Heads&lt;/em&gt;: Whether it’s Sales, Marketing, Engineering, or Product, each leader will fill you in on how their team supports our bigger goals. The idea is to see how all the pieces fit together and create the vibe.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Engineering &amp;amp; Product Leads&lt;/em&gt;: If you love diving into tech details, these sessions will give you insights into our architecture, tools, and future product plans.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Seeing the Product in Action&lt;/strong&gt;&lt;br&gt;
Our platform is built around microservices, but don’t worry if that’s new to you. We’ll walk you through the basics. We also encourage you to check out how our customers actually use epilot—like checking live customer feedbacks. It’s a great way to see what’s working and where we can improve.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Balance Between Work and Collaboration&lt;/strong&gt;&lt;br&gt;
At epilot, we keep things productive but also supportive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;Team Events&lt;/em&gt;: You’ll get invites to company-wide announcements, product demos, company events, and more.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Open Communication&lt;/em&gt;: Slack, shared docs, and frequent check-ins mean you’ll never be left guessing.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Culture Matters&lt;/em&gt;: We appreciate everyone’s contribution, whether it’s project-related or just sharing something fun. We are the team, and this is what matters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Looking Ahead&lt;/strong&gt;&lt;br&gt;
After a few weeks, you’ll have a good handle on how things work here—and you’ll know who to reach out to when you need help. We’ll likely ask for your thoughts on onboarding so we can keep improving the process for the future.&lt;/p&gt;

&lt;p&gt;If this relaxed but purposeful approach resonates with you, we look forward to hearing from you. At epilot, we want every new hire to find their footing, make meaningful contributions, and grow in their role—without all the usual corporate fluff.&lt;/p&gt;

&lt;p&gt;Happy to have you on board ✨.&lt;/p&gt;

</description>
      <category>onboarding</category>
      <category>culture</category>
      <category>workplace</category>
      <category>hiring</category>
    </item>
    <item>
      <title>Building a Scalable Audit Log System with AWS and ClickHouse</title>
      <dc:creator>Sebastian</dc:creator>
      <pubDate>Tue, 26 Nov 2024 09:00:05 +0000</pubDate>
      <link>https://dev.to/epilot/building-a-scalable-audit-log-system-with-aws-and-clickhouse-jn5</link>
      <guid>https://dev.to/epilot/building-a-scalable-audit-log-system-with-aws-and-clickhouse-jn5</guid>
      <description>&lt;p&gt;Audit logs might seem like a backend feature that only a few people care about, but they play a crucial role in keeping things running smoothly and securely in any SaaS or tech company. Let me take you through our journey of building a robust and scalable audit log system. Along the way, I’ll share why we needed it, what exactly audit logs are, and how we combined tools like AWS, ClickHouse, and OpenAPI to craft a solution that works like a charm.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Case of the Disappearing Configuration
&lt;/h2&gt;

&lt;p&gt;At epilot, we’ve encountered a frustratingly familiar scenario. A customer reaches out, upset that one of their workflow configurations has mysteriously vanished. Their immediate question? “Who deleted it?”—and the assumption is that someone on our team is responsible.&lt;/p&gt;

&lt;p&gt;Now here’s the tricky part: how do we, as engineers, figure out who did what and when?&lt;/p&gt;

&lt;p&gt;One obvious approach is to dive into the application logs. But here’s the catch: most of the production logs aren’t enabled by default. Even when they are, they’re often sampled, capturing only about 10% of the actual traffic. Additionally, those logs often seem to lack the required information. This means we’re left piecing together incomplete data, like trying to solve a puzzle with half the pieces missing.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Are Audit Logs Anyway?
&lt;/h2&gt;

&lt;p&gt;Audit logs provide clear visibility into system changes, aiding teams in investigations, diagnosing incidents, and tracing unauthorized actions. They empower admins by reducing support reliance and ensuring clarity on actions like role or workflow updates. For enterprise customers, audit logs are a critical, expected feature that supports compliance with standards like ISO 27001. Additionally, they lay the groundwork for enhanced threat detection capabilities in the future. In simple terms they try to help to answer the following questions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHO&lt;/strong&gt; is doing something. Typically a user or a system (api call)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHAT&lt;/strong&gt; is that user/system doing?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHERE&lt;/strong&gt; is that occurring from? (e.g. an IP address)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHEN&lt;/strong&gt; did it occur?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WHY?&lt;/strong&gt; (optional) Why did the user log in? → we don’t know, Why is its IP blocked? → User logged in 5 times with the wrong password&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Considerations for a Successful Audit Log System
&lt;/h2&gt;

&lt;p&gt;Before diving into the technical details, it’s crucial to define what makes an audit log system effective. While the exact requirements depend on your company’s domain, there are some universal points worth considering:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compliance&lt;/strong&gt;: Ensure the system adheres to regulations like GDPR. For example, customers may request the deletion of personal data, so you’ll need a straightforward way to erase all logs tied to a specific customer.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sustainability&lt;/strong&gt;: Audit logs grow rapidly, especially in high-traffic systems. Storing them indefinitely may not be feasible. Decide on strategies for archiving or purging logs over time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permissions&lt;/strong&gt;: Define who is allowed to access audit logs to maintain security and privacy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Format&lt;/strong&gt;: Standardize the structure of your logs to ensure they’re easy to interpret and query.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Data Selection&lt;/strong&gt;: Carefully determine what actions and events are worth logging to answer critical questions effectively, without unnecessary noise.&lt;/p&gt;




&lt;h2&gt;
  
  
  Making It Happen: How We Built Our Audit Logs
&lt;/h2&gt;

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

&lt;p&gt;At epilot, our APIs are built around serverless components provided by AWS. From the outset, we recognized that AWS API Gateway events provided a rich source of information for building audit logs. These events capture critical details such as user identities, actions performed (through the request payload), IP addresses, headers, and more.&lt;/p&gt;

&lt;p&gt;Given our microservices architecture, where services are organized by domain and accessed through an API Gateway (&lt;a href="https://docs.epilot.io/docs/architecture/overview/#system-architecture-diagram" rel="noopener noreferrer"&gt;see our system architecture&lt;/a&gt;), we needed a solution that seamlessly integrated with this structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High-Level Overview&lt;/strong&gt;&lt;br&gt;
Our approach to audit logging can be summarized as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Capturing events asynchronously.&lt;/li&gt;
&lt;li&gt;Validating and transforming raw events into a standard format.&lt;/li&gt;
&lt;li&gt;Persisting the data in a read-only, scalable, and query-friendly storage system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This design adheres to several key technical principles:&lt;/p&gt;

&lt;p&gt;&lt;u&gt;Asynchronous Event Capture&lt;/u&gt;&lt;br&gt;
We use Amazon SQS to decouple event capture from the main HTTP request flow. For example, when a user creates a new workflow configuration, the relevant API Gateway event is pushed to an SQS queue by middleware wrapping the API. This ensures that audit logging does not introduce latency or affect the performance of the core application logic.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;From Raw to Standardized Events&lt;/u&gt;&lt;br&gt;
Our focus is on capturing system modifications, specifically HTTP methods like POST, PUT, PATCH, and DELETE. These provide meaningful insights into changes occurring within the system. GET requests, on the other hand, generate excessive noise and are generally excluded—though we offer an opt-in mechanism for services where logging GET requests adds value.&lt;/p&gt;

&lt;p&gt;A Lambda function processes raw API Gateway events from the SQS queue, transforming them into a structured and validated format. This includes filtering relevant data, enhancing it using metadata like OpenAPI specifications, and ensuring consistency across all logged events.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;Data Persistence&lt;/u&gt;&lt;br&gt;
For storing audit logs, we chose &lt;a href="https://clickhouse.com/" rel="noopener noreferrer"&gt;ClickHouse&lt;/a&gt;, a highly scalable, SQL-based database that aligns with our requirements:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Read-only access: Supports immutability to preserve data integrity.
Scalability: Proven in our data lake setup to handle large volumes of data efficiently.&lt;/li&gt;
&lt;li&gt;Querying: SQL capabilities allow for precise filtering and analysis, which is more complex with alternatives like DynamoDB.
By leveraging ClickHouse, we ensure a robust and scalable foundation for our audit logs, simplifying future integrations and analysis.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;u&gt;Integration&lt;/u&gt;&lt;br&gt;
To make audit logging effortless for our microservices, we focused on seamless integration. At epilot, we rely heavily on &lt;a href="https://middy.js.org/" rel="noopener noreferrer"&gt;middy&lt;/a&gt;, a middleware engine used across all our services. Building on this, we introduced a new middleware: &lt;strong&gt;withAuditLog&lt;/strong&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { withAuditLog } from '@epilot/audit-log'
import middy from '@middy/core'
import type { Handler } from 'aws-lambda'


export const withMiddlewares = (handler: lambda.Handler) =&amp;gt; {
  return middy(handler)
    .use(enableCorrelationIds())
    .use(...)
    .use(
      withAuditLog({
        ignorePaths: ['/v1/webhooks/configs/{configId}/trigger']
      })
    )
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This middleware integrates directly into existing services and simplifies the audit logging process by:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capturing API Gateway Events&lt;/strong&gt;: It hooks into the request lifecycle to extract the API Gateway event details.&lt;br&gt;
Omitting GET Requests by Default: To reduce noise, it filters out GET requests, with an option to opt them in for specific services where needed.&lt;br&gt;
&lt;strong&gt;Forwarding to SQS&lt;/strong&gt;: Its primary role is to forward the event to an SQS queue for asynchronous processing.&lt;/p&gt;

&lt;p&gt;With this middleware, adding audit logging to any microservice is as simple as including withAuditLogs in the service's middleware stack and giving the SQS:SendMessage permission. This ensures consistency, reduces implementation effort, and keeps the integration process dead simple.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical Considerations&lt;/strong&gt;&lt;br&gt;
This article focuses on our high-level approach to building audit logs, as there are numerous ways to tackle the problem, each with its trade-offs. During our research, we explored alternatives like EventBridge for emitting events at the end of each request or Kinesis for streaming data. Ultimately, we chose a solution that met our key requirements: decoupling log emission from the main flow while offering flexibility in managing throughput and batching.&lt;/p&gt;

&lt;p&gt;Here’s why we chose SQS:&lt;/p&gt;

&lt;p&gt;&lt;u&gt;Decoupling from the Main Flow&lt;/u&gt;&lt;br&gt;
SQS allows us to process audit logs asynchronously, ensuring that the main HTTP request flow remains unaffected. This means audit log processing won’t slow down user-facing operations.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;Flexibility with Throughput and Batching&lt;/u&gt;&lt;br&gt;
With SQS, we can fine-tune parameters like long-polling and batch windows to optimize throughput without compromising efficiency. This ensures scalable and reliable processing regardless of traffic spikes.&lt;/p&gt;

&lt;p&gt;&lt;u&gt;Scalability for POST/PUT/PATCH/DELETE Events&lt;/u&gt;&lt;br&gt;
Since we exclude GET requests by default, the system can handle fewer, more meaningful events. Capturing GET requests would require supporting a higher volume of events, potentially leading to Lambda concurrency issues, as multiple Lambda environments subscribing to the same queue could interfere with other services also using Lambda.&lt;/p&gt;




&lt;h2&gt;
  
  
  Exposing Audit Logs to Users
&lt;/h2&gt;

&lt;p&gt;To make audit logs accessible and actionable, we introduced a new &lt;a href="https://sst.dev/" rel="noopener noreferrer"&gt;SST&lt;/a&gt;-based microservice that acts as a bridge to query data from ClickHouse. This microservice provides a simple and intuitive interface for users to explore their audit logs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Key Features:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Search and Filtering: A user-friendly search bar allows users to combine filters effortlessly, enabling them to pinpoint specific events or patterns within the logs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Activity Messages: Each audit log entry includes an activity message, a concise summary of what occurred. This message is dynamically constructed on the API side, tailored to the specific service name, making it customizable and relevant.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By customizing the activity messages for each service, users can quickly understand what happened in their systems without wading through raw data. This tailored approach ensures that the audit logs deliver immediate value and clarity to the end users.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Summary
&lt;/h3&gt;

&lt;p&gt;In this article, we detailed the design and implementation of our audit log system at epilot, highlighting the key decisions and considerations that shaped its architecture. Our approach leverages AWS serverless components to seamlessly integrate audit logging into our microservices, ensuring scalability, efficiency, and ease of use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Capturing Events:&lt;/strong&gt; Using a custom middleware, withAuditLogs, we extract API Gateway events asynchronously and forward them to an SQS queue, ensuring the logging process does not block the main application flow.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Processing and Storing Logs:&lt;/strong&gt; A Lambda function transforms raw events into a standardized format, focusing on meaningful system modifications (POST, PUT, PATCH, DELETE) and stores them in a scalable, SQL-based ClickHouse database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;User Accessibility:&lt;/strong&gt; A new SST-based microservice provides a simple interface for querying and filtering logs. Tailored activity messages enhance usability, helping users quickly understand what occurred.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Technical Considerations:&lt;/strong&gt; SQS was chosen for its ability to decouple the logging process, optimize throughput, and handle scalability challenges. While other solutions like EventBridge or Kinesis were viable, SQS met our specific requirements effectively.&lt;/p&gt;

&lt;p&gt;This high-level overview provides a flexible, scalable, and user-friendly solution for audit logging while ensuring system integrity and maintaining performance.&lt;/p&gt;

&lt;p&gt;Do you want to work on features like this? Check out our &lt;a href="https://www.epilot.cloud/en/company/careers#Offene-Stellenangebote" rel="noopener noreferrer"&gt;career page&lt;/a&gt; or reach out to &lt;a href="https://x.com/boingCntributor" rel="noopener noreferrer"&gt;my Twitter&lt;/a&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>auditlog</category>
      <category>clickhouse</category>
      <category>sqs</category>
    </item>
    <item>
      <title>The Sauna Epiphany: How I Got Product Engineering Wrong</title>
      <dc:creator>Viljami Kuosmanen</dc:creator>
      <pubDate>Sun, 13 Oct 2024 12:23:43 +0000</pubDate>
      <link>https://dev.to/epilot/the-sauna-epiphany-how-i-got-product-engineering-wrong-35jg</link>
      <guid>https://dev.to/epilot/the-sauna-epiphany-how-i-got-product-engineering-wrong-35jg</guid>
      <description>&lt;p&gt;If you've seen my posts lately you've probably seen a lot of talk about &lt;em&gt;Product Engineers&lt;/em&gt;: software engineers who talk in customer problems and take pride in the products they build, not only the code and technologies they use.&lt;/p&gt;

&lt;p&gt;My core thesis is that with the rise of AI, the expectations for software engineers are being redefined, moving away from narrow programming roles: backend, frontend, C#, Java, React, etc. fast becoming obsolete due to AI tools like &lt;a href="https://www.cursor.com/" rel="noopener noreferrer"&gt;Cursor&lt;/a&gt; lowering the barrier of entry and unlocking productivity with any technology.&lt;/p&gt;

&lt;p&gt;The days when you could coast on your ability to churn out code are over. If you're still clinging to the idea that you're safe just because you know how to write code in a widely used language or framework you're in for a rude awakening.&lt;/p&gt;

&lt;p&gt;I believe this trend is likely to be the biggest industry shift in my lifetime, even surpassing the agile movement of the last 20 years. And I'm not the only one talking about it. (&lt;a href="https://blog.pragmaticengineer.com/the-product-minded-engineer/" rel="noopener noreferrer"&gt;#1&lt;/a&gt;, &lt;a href="https://hybridhacker.email/p/how-to-become-a-product-engineer" rel="noopener noreferrer"&gt;#2&lt;/a&gt;, &lt;a href="https://thesoftwareengineeringtimes.substack.com/p/are-product-engineers-replacing-software" rel="noopener noreferrer"&gt;#3&lt;/a&gt;, &lt;a href="https://saranga.dev/from-code-monkey-to-product-engineer-the-evolution-of-software-engineering-in-the-age-of-llms-3c79a508464d" rel="noopener noreferrer"&gt;#4&lt;/a&gt;, &lt;a href="https://thesoftwareengineeringtimes.substack.com/p/are-product-engineers-replacing-software" rel="noopener noreferrer"&gt;#5&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;But I got it wrong.&lt;/p&gt;

&lt;p&gt;Not entirely wrong, but I made it more complicated than it needed to be. In my quest to define what it means to be a product engineer, I overlooked the power of simplicity and ironically did not think enough about the customer: the ambitious software engineer thinking strategically about their careers.&lt;/p&gt;

&lt;h2&gt;
  
  
  I overengineered it
&lt;/h2&gt;

&lt;p&gt;In my earlier attempts to help engineers break out from purely technical roles, I published an extensive checklist to help engineers think and ask questions as product engineers. It covered everything from understanding the user and the market to measuring success and staying ahead of industry trends.&lt;/p&gt;

&lt;p&gt;Here's a taste of that checklist:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#1-understand"&gt;1 Understand&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#11-whos-the-user"&gt;1.1 Who's the user?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#12-whos-the-customer"&gt;1.2 Who's the customer?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#13-whats-the-market"&gt;1.3 What's the market?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#14-ask-why"&gt;1.4 Ask Why&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#15-what-do-we-already-know"&gt;1.5 What do we already know?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#2-craft"&gt;2 Craft&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#21-am-i-proud-of-what-im-building"&gt;2.1 Am I proud of what I'm building?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#22-does-the-product-feel-good"&gt;2.2 Does the product feel good?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#23-how-do-i-get-there-faster"&gt;2.3 How do I get there faster?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#24-teamwork"&gt;2.4 Teamwork&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#3-growth"&gt;3 Growth&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#31-how-do-i-measure-the-success-of-my-work"&gt;3.1 How do I measure the success of my work?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#32-how-do-i-maximise-the-impact-of-my-work"&gt;3.2 How do I maximise the impact of my work?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#33-how-do-i-stay-ahead-of-the-curve"&gt;3.3 How do I stay ahead of the curve?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#4-product-vision"&gt;4 Product Vision&lt;/a&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#41-whats-our-north-star"&gt;4.1 What’s our North Star?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#42-how-does-my-work-impact-the-overall-design-of-the-product"&gt;4.2 How does my work impact the overall design of the product?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/epilot/the-product-engineer-checklist-469d#43-whats-the-ambition-level"&gt;4.3 What's the ambition level?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Great stuff, but let's be honest — no engineer is going to run through a 15-point checklist to make a product decision. Just seeing this long list of questions can be totally overwhelming, especially if you're used to being in a technical role.&lt;/p&gt;

&lt;p&gt;If we expect engineers to actually start doing this stuff, the core idea needs to be simple and easy to remember.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Wake-Up Call in the Sauna
&lt;/h2&gt;

&lt;p&gt;As I'm writing this post, I'm enjoying a company working vacation in a nice hotel in Mallorca with my epilot colleagues. I was having a conversation in the hotel sauna with a principal engineer where something he said in passing suddenly clicked for me.&lt;/p&gt;

&lt;p&gt;He was venting about some engineers on his team who seemed to be diving head first into coding without bothering to understand the problem. &lt;em&gt;"It's so easy. Every engineer should be able to answer what problem they're solving, who the customer is, and what the impact of the work they're doing is."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That hit me like a splash of cold water in a 100°C Finnish sauna.&lt;/p&gt;

&lt;p&gt;He was totally right.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boiling It Down to 3 Essential Questions
&lt;/h2&gt;

&lt;p&gt;To think like a product engineer, it's already enough to start with just three simple questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What's the problem?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;For who?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Why is this important?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's dive a bit deeper into each one.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. What's the problem?
&lt;/h3&gt;

&lt;p&gt;Stop coding for a second. Do you really know what you're trying to solve? Not the ticket description in Jira, but the real-world issue. If you can't articulate the problem in simple plain English, you have no business writing a single line of code.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. For who?
&lt;/h3&gt;

&lt;p&gt;Identify your user and customer. Sometimes they're the same person; other times, they're not. Understanding who will use your product (and who will pay for it) is crucial. It shapes the way you approach the solution and helps you tailor the experience to meet their needs.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Why is this important?
&lt;/h3&gt;

&lt;p&gt;As engineers there's never a shortage of things we could improve or implement. If solving the problem doesn't make a meaningful difference, why are you wasting your valuable time? We're not here to build features that nobody uses or cares about. Connect your work to something that actually matters and helps build your track record.&lt;/p&gt;

&lt;p&gt;By anchoring your work in these three questions, you immediately move from code monkey to a high value product-minded engineer. Still a rare breed. You're not just implementing features someone else decided to build; you're elevating yourself to a position to influence product decisions and build smarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leverage Your Team—They're There for a Reason
&lt;/h2&gt;

&lt;p&gt;The biggest mistake engineers make is thinking they have to find all the answers themselves.&lt;/p&gt;

&lt;p&gt;Most of us are lucky to work in a product team with other disciplines like UX researchers, designers, PMs and business stakeholders whose job is to help answer these questions.&lt;/p&gt;

&lt;p&gt;By leaning on your team, you not only find better answers but also foster a more collaborative and innovative environment.&lt;/p&gt;

&lt;p&gt;Product engineering is a team sport.&lt;/p&gt;

&lt;h2&gt;
  
  
  Taking Action: Start Asking the Right Questions Today
&lt;/h2&gt;

&lt;p&gt;So, the next time you tackle a new topic, pause for a moment before diving into code. Ask yourself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;What's the problem?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;For who?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Why is this important?&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Write down your answers. If you don't know, reach out to your team and find out. This simple practice can transform your approach to work, leading to better decisions, more impactful solutions, and a greater sense of ownership.&lt;/p&gt;

&lt;p&gt;Congratulations, you just became a Product Engineer! &lt;/p&gt;

&lt;h2&gt;
  
  
  Join the Conversation
&lt;/h2&gt;

&lt;p&gt;I'd love to hear your thoughts on this simplified approach to product engineering. Have you tried asking these three questions in your work? What impact did it have? Share your experiences in the comments below.&lt;/p&gt;

&lt;p&gt;Consider giving the &lt;a href="https://github.com/anttiviljami/product-engineer-manifesto" rel="noopener noreferrer"&gt;GitHub repository&lt;/a&gt; a star if the product engineer role resonates with you!&lt;/p&gt;

</description>
      <category>product</category>
      <category>ai</category>
      <category>sauna</category>
    </item>
    <item>
      <title>How We Integrate AI in epilot - Chapter 1: AWS Bedrock &amp; Prompt Engineering</title>
      <dc:creator>Kerem Nalbant</dc:creator>
      <pubDate>Thu, 18 Jul 2024 14:58:31 +0000</pubDate>
      <link>https://dev.to/epilot/how-we-integrate-ai-in-epilot-chapter-1-aws-bedrock-prompt-engineering-17jh</link>
      <guid>https://dev.to/epilot/how-we-integrate-ai-in-epilot-chapter-1-aws-bedrock-prompt-engineering-17jh</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;When we decided to bring AI to epilot, we had so many potential use cases that we first needed to do user research and identify the most repetitive and time consuming tasks. After that, we had to find out what our customers needed the most from these.&lt;/p&gt;

&lt;p&gt;Our research showed that users heavily utilized the messaging feature and for some cases when long email threads come into the play, we noticed that some customers were spending an average time of 16 minutes replying to an email, and we knew we could make it better by providing them with a shorter and clearer thread summary.&lt;/p&gt;

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

&lt;p&gt;&lt;a href="https://platforms.substack.com/p/how-to-win-at-enterprise-ai-a-playbook" rel="noopener noreferrer"&gt;Enterprise AI Playbook&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Work is a bundle of tasks, which are performed towards specific goals.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Tasks are the ‘atomic unit’ of any work done in the enterprise. Tasks may be performed as a human service, or may be performed by software, towards achieving a goal.&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Our goal was to reduce that time down to less than a minute, and there were two different tasks being performed by users which we need to perform with AI to achieve our goal. This article addresses the &lt;strong&gt;Task: Send emails&lt;/strong&gt; and the steps to complete this task:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read and understand the email thread:&lt;/strong&gt; Help users understand long email threads faster by providing AI-generated summary, next steps and topics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write an answer:&lt;/strong&gt; Provide AI-generated suggested answers.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Problem
&lt;/h2&gt;

&lt;p&gt;Our customers often deal with long email threads, requiring a long time to read and answer. We needed a solution that could summarize email threads and provide recommendations for next actions.&lt;/p&gt;

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

&lt;p&gt;While some problems could be solved with prompt engineering alone, some could be solved with Retrieval-Augmented Generation (RAG).&lt;/p&gt;

&lt;p&gt;For generating summaries, we didn't need any external contextual data  other than email thread to feed the prompt, so we decided to just go ahead with prompt engineering.&lt;/p&gt;

&lt;p&gt;The next task is suggesting AI-generated replies to email threads, where RAG would be really useful. For that, we will use a Vector DB and an embedding model, which I'll write about in the next chapter.&lt;/p&gt;

&lt;h3&gt;
  
  
  AWS Bedrock
&lt;/h3&gt;

&lt;p&gt;We decided to use AWS Bedrock, as epilot we already make use of AWS in almost every area of our platform. It provides state-of-the-art LLMs from multiple providers and out of the box solutions such as Knowledge Base to achieve RAG easily and Model Evaluation to compare models and prompts with a fancy UI.&lt;/p&gt;

&lt;p&gt;AWS Bedrock also ensures the processed data is protected. One of our concerns was where the data would be stored, how it would be processed and whether 3rd parties would be involved.&lt;/p&gt;

&lt;p&gt;By default, AWS Bedrock offers zero-retention policy, ensuring that logs, prompts, LLM output and any personal data are not shared with any third parties or model providers. Bedrock also ensures that all processed data remains within the EU region.&lt;/p&gt;

&lt;h3&gt;
  
  
  GenAI Foundation
&lt;/h3&gt;

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

&lt;p&gt;GenAI Foundation has a central SQS queue and a handler function, ensures exactly one, concurrent and batch processing while staying within the rate limits of Bedrock.&lt;/p&gt;

&lt;p&gt;Integrating GenAI related code and logic to our existing APIs was not a good idea. So, I've decided to create a new monorepo. Here's the reasons:&lt;/p&gt;

&lt;h4&gt;
  
  
  Separation of Concerns
&lt;/h4&gt;

&lt;p&gt;By creating a separate repository, we maintain a clear separation of concerns. This ensures that the core functionalities of existing APIs remain clean and focused.&lt;/p&gt;

&lt;h4&gt;
  
  
  Language Flexibility
&lt;/h4&gt;

&lt;p&gt;Since GenAI-related code requires Python, having a separate repository allows us to leverage Python's capabilities without interfering with the TypeScript-based APIs. This separation ensures that each project can use the best-suited language for its specific tasks.&lt;/p&gt;

&lt;h4&gt;
  
  
  Encapsulation
&lt;/h4&gt;

&lt;p&gt;Encapsulating the GenAI logic within a dedicated repository makes it easier to manage, update, and scale. This also allows engineers with specific expertise in GenAI to work independently of the rest of the system.&lt;/p&gt;

&lt;h4&gt;
  
  
  Modularity
&lt;/h4&gt;

&lt;p&gt;A modular approach allows for easier testing, maintenance, and deployment of the GenAI features. Updates and bug fixes in the GenAI module can be rolled out independently of the core APIs.&lt;/p&gt;

&lt;h3&gt;
  
  
  Model Choice &amp;amp; Prompt Engineering
&lt;/h3&gt;

&lt;p&gt;We decided to go with Claude 3 Sonnet because it was the best option for us at the time, given the costs and the complexity of the task. &lt;/p&gt;

&lt;p&gt;We are planning to switch to &lt;a href="https://www.anthropic.com/news/claude-3-5-sonnet" rel="noopener noreferrer"&gt;Claude 3.5 Sonnet&lt;/a&gt; once it's available in AWS Bedrock, which we're really excited about!&lt;/p&gt;

&lt;p&gt;To choose the best model for the task, AWS Bedrock offers &lt;a href="https://docs.aws.amazon.com/bedrock/latest/userguide/model-evaluation.html" rel="noopener noreferrer"&gt;Model Evaluation&lt;/a&gt;, which lets you easily compare models with each other. &lt;br&gt;
You can also do Prompt Evaluation by creating a dataset and executing it against a single model to compare the results of different prompts.&lt;/p&gt;



&lt;p&gt;There are lots of great resources online for prompt engineering, but I want to mention that Anthropic provides really helpful tips in their documentation. I strongly suggest you to take a look, if you haven't already. For me, the most important point was &lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/use-xml-tags" rel="noopener noreferrer"&gt;using XML tags&lt;/a&gt;. Anthropic mentions that their models produce much better results when used with XML tags.&lt;/p&gt;

&lt;p&gt;Prompt engineering is all about experimenting, so we spent some time on optimizing our prompt. I'd love to share an example of prompts with you!&lt;/p&gt;

&lt;p&gt;Below you can see some example usages of common prompt engineering techniques such as "Giving a Role to LLM", "Using XML Tags", "Using Examples (Multishot Prompting)", "Being clear and direct".&lt;/p&gt;
&lt;h4&gt;
  
  
  System Prompt
&lt;/h4&gt;

&lt;p&gt;System prompt is where we &lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/system-prompts" rel="noopener noreferrer"&gt;give Claude a role&lt;/a&gt;, and some clear instructions about the task.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are an intelligent assistant specialized in assisting customer service agents in energy industry.

Your task is summarizing email conversations between customer service agents and customers, providing a clear and concise overview of the key points by following the instructions provided.

Your summaries will help your colleagues quickly understand the main aspects of each conversation without having to read through the entire email thread.

Your goal is to ensure that the agent can grasp the key points and next steps from your summary alone, making their workflow more efficient and effective.

You are a third person observer and must not provide any personal opinions or make any assumptions about the conversation.

You must use the third-person objective narration. You must report the events that take place without knowing the motivations or thoughts of any of the characters.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  User Prompt
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Here is the conversation between the customer and the agent:

&amp;lt;Email Thread&amp;gt;
&amp;lt;Subject&amp;gt;
{subject}
&amp;lt;/Subject&amp;gt;
&amp;lt;Messages&amp;gt;
{conversation}
&amp;lt;/Messages&amp;gt;
&amp;lt;/Email Thread&amp;gt;

&amp;lt;General Instructions&amp;gt;
- You must use &amp;lt;Email Thread&amp;gt; tags to identify the email conversation.
...
- You must use only the knowledge provided in the &amp;lt;Email Thread&amp;gt; tags and do not access any other external information or knowledge you already possess.
&amp;lt;/General Instructions&amp;gt;

&amp;lt;Language Instructions&amp;gt;
- You must optimize the language for making the summary easy and fast for humans to read.
...
- You must use a professional, respectful and informative tone.
&amp;lt;/Language Instructions&amp;gt;

&amp;lt;Reference Instructions&amp;gt;
- You must always refer to the Customer and Agent by their names.
...
&amp;lt;/Reference Instructions&amp;gt;

&amp;lt;Summary Instructions&amp;gt;
- You must get relevant quotes to complete the task from the conversation.
...
- You must write the summary in reverse chronological order.
&amp;lt;/Summary Instructions&amp;gt;

&amp;lt;Output Instructions&amp;gt;
- You must give your output in JSON format. The JSON object should be valid.
...
- If you will provide quotes or emphasize any part of the conversation, you must use single quotes. e.g. 'quote'.
&amp;lt;/Output Instructions&amp;gt;

# Few Shot Prompting
&amp;lt;Example Outputs&amp;gt;
{
  "summary": [
    "John Doe has processed the reimbursement request and informed Jane Doe.",
    "Jane Doe has provided the requested information and is awaiting further instructions from the team member.",
    "John Doe has acknowledged the Jane Doe refund request and has requested additional information to process the refund.",
    "Jane Doe is requesting a refund for a defective PV inverter."
  ],
  "topics": [
    "Refund request for defective product"
  ],
  "next_steps": [
    "If the information is sufficient, John Doe should process the refund and inform Jane Doe of the completion.",
    "John Doe should document the refund process for record-keeping."
  ]
}
...
&amp;lt;/Example Outputs&amp;gt;

You must follow the instructions listed in the following tags:
- &amp;lt;General Instructions&amp;gt; for general guide and rules,
- &amp;lt;Language Instructions&amp;gt; for lingual instructions that declares your tone, grammar, and output language,
...
- &amp;lt;Example Output&amp;gt; for example of the output as a reference.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Human-in-the-Loop (HITL)
&lt;/h3&gt;

&lt;p&gt;It's challenging to ship AI features, especially when it's the first one in the company which has thousands of users. We need to build trust, and keep it. To do that, we need to collect as much as feedback we can. Then, evaluate these feedbacks and take actions.&lt;/p&gt;

&lt;p&gt;HITL can be applied in different ways in different solutions. In our use case, we utilize it to evaluate feedback and detect hallucinations. Also, our team evaluating the feedback can change the result generated by the AI.&lt;/p&gt;

&lt;h3&gt;
  
  
  Migration Strategy
&lt;/h3&gt;

&lt;p&gt;Our migration strategy involves both runtime and one-time migration to ensure a smooth transition and boost the adoption for our users.&lt;br&gt;
With this way, we ensure we don't unnecessarily make use of LLMs, and we save costs while ensuring seamless integration.&lt;/p&gt;
&lt;h3&gt;
  
  
  Rate Limits
&lt;/h3&gt;

&lt;p&gt;Since AWS Bedrock imposes rate limiting, we had to reflect this to our users. In order to offer a fair usage, we set limits according to the pricing tier of our users.&lt;/p&gt;
&lt;h3&gt;
  
  
  Code Examples
&lt;/h3&gt;

&lt;p&gt;Pay attention to the way I &lt;a href="https://docs.anthropic.com/en/docs/build-with-claude/prompt-engineering/prefill-claudes-response#example-maintaining-character-with-role-prompting" rel="noopener noreferrer"&gt;prefill Claude's response&lt;/a&gt; to force it to answer only with JSON object.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;settings = {
    "anthropic_version": "bedrock-2023-05-31",
    "max_tokens": 1000,
    "system": system_prompt,
    "messages": [
        {
            "role": "user",
            "content": [
                {"type": "text", "text": prompt},
            ],
        },
        {"role": "assistant", "content": "{"}, # Prefill Claude's response
    ],
    "temperature": temperature,
    "top_p": top_p,
    "top_k": top_k,
}
res = await client.invoke_model(
    modelId=MODEL_ID,
    contentType="application/json",
    accept="application/json",
    body=json.dumps(settings)
)

model_response = json.loads(await res["body"].read())
response_text = model_response["content"][0]["text"]

res_model = LLMResponseModel.model_validate_json("{" + response_text)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;By integrating AI into epilot, we have significantly enhanced the capabilities of our platform. This integration not only improves the efficiency of daily tasks, but also accelerates customer support. Furthermore, it's the first step in positioning epilot as a leader in the adoption of advanced AI technologies in the energy sector.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>bedrock</category>
      <category>anthropic</category>
      <category>promptengineering</category>
    </item>
    <item>
      <title>The Product Engineer Checklist</title>
      <dc:creator>Viljami Kuosmanen</dc:creator>
      <pubDate>Tue, 04 Jun 2024 09:34:22 +0000</pubDate>
      <link>https://dev.to/epilot/the-product-engineer-checklist-469d</link>
      <guid>https://dev.to/epilot/the-product-engineer-checklist-469d</guid>
      <description>&lt;p&gt;&lt;em&gt;Download the PDF at &lt;a href="https://productengineer.org" rel="noopener noreferrer"&gt;productengineer.org&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Think Like a Product Engineer
&lt;/h2&gt;

&lt;p&gt;The following is a checklist of questions for product engineers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
1 Understand

&lt;ul&gt;
&lt;li&gt;1.1 Who's the user?&lt;/li&gt;
&lt;li&gt;1.2 Who's the customer?&lt;/li&gt;
&lt;li&gt;1.3 What's the market?&lt;/li&gt;
&lt;li&gt;1.4 Ask Why&lt;/li&gt;
&lt;li&gt;1.5 What do we already know?&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

2 Craft

&lt;ul&gt;
&lt;li&gt;2.1 Am I proud of what I'm building?&lt;/li&gt;
&lt;li&gt;2.2 Does the product feel good?&lt;/li&gt;
&lt;li&gt;2.3 How do I get there faster?&lt;/li&gt;
&lt;li&gt;2.4 Teamwork&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

3 Growth

&lt;ul&gt;
&lt;li&gt;3.1 How do I measure the success of my work?&lt;/li&gt;
&lt;li&gt;3.2 How do I maximise the impact of my work?&lt;/li&gt;
&lt;li&gt;3.3 How do I stay ahead of the curve?&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

4 Product Vision

&lt;ul&gt;
&lt;li&gt;4.1 What’s our North Star?&lt;/li&gt;
&lt;li&gt;4.2 How does my work impact the overall design of the product?&lt;/li&gt;
&lt;li&gt;4.3 What's the ambition level?&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  1 Understand
&lt;/h2&gt;

&lt;h2&gt;
  
  
  1.1 Who's the user?
&lt;/h2&gt;

&lt;p&gt;As a product engineer, your #1 goal is to create happy users. These are your fans! Always start with the user!&lt;/p&gt;

&lt;p&gt;Your user is the person that primarily interacts with your product and whose experience your work will directly impact. &lt;/p&gt;

&lt;p&gt;You may have multiple user groups. People who may have varying reasons to use your product or might interact with it in different ways from different angles, maybe on different kinds of devices.&lt;/p&gt;

&lt;p&gt;You should understand how to help these users. What drives them to use your product? What delights them? What are their pains?&lt;/p&gt;

&lt;p&gt;As a product engineer you should demand and support your team find answers to these questions before jumping into writing code. &lt;/p&gt;

&lt;h2&gt;
  
  
  1.2 Who's the customer?
&lt;/h2&gt;

&lt;p&gt;Yes, this is a different question to "Who’s the user?". The customer is whoever pays for your product, not always who uses it.&lt;/p&gt;

&lt;p&gt;You should know what makes your product valuable to your customers to make better decisions on what to invest your time.&lt;/p&gt;

&lt;p&gt;Hint: If you're in B2B the answer always has to do with helping your customers save money or make more money. Understanding your customers' core business is key to understanding why they would pay for your product.&lt;/p&gt;

&lt;p&gt;Most importantly, you want to make whoever is paying for your product look good. After all, they're the ones taking a risk by picking your product. You ALWAYS want to reward them for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  1.3 What's the market?
&lt;/h2&gt;

&lt;p&gt;Zooming out, let's take a look at the wider market landscape. Who are the potential customers we haven’t captured yet? Why would a customer pick a competitor’s product vs. mine? Are we leaving opportunities on the table?&lt;/p&gt;

&lt;p&gt;What can I learn from similar products in the market? What are our USPs? How do I create a competitive advantage against competition?&lt;/p&gt;

&lt;p&gt;Are there rules to the market? Are there regulations or industry standards I need to know about? Does my product have to look or feel a certain way to be taken seriously?&lt;/p&gt;

&lt;p&gt;Knowing the market and regularly bechmarking yourself against other players helps become aware of your strengths and weaknesses and give ideas for where to invest strategically in your own product.&lt;/p&gt;

&lt;h2&gt;
  
  
  1.4 Ask Why
&lt;/h2&gt;

&lt;p&gt;This is a bit of a product thinking cliche but still holds true: always pays to ask “why” a few times to uncover root causes of problems and underlying motivations.&lt;/p&gt;

&lt;p&gt;Asking why can be helpful in almost any situation to build understanding in your team. Some examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Why should we invest into building this feature?&lt;/li&gt;
&lt;li&gt;Why are customers asking for this feature?&lt;/li&gt;
&lt;li&gt;Why is this a pain for our users?&lt;/li&gt;
&lt;li&gt;Why do users give us that feedback?&lt;/li&gt;
&lt;li&gt;Why now?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1.5 What do we already know?
&lt;/h2&gt;

&lt;p&gt;It’s smart to build on what’s already known rather than always starting from scratch.&lt;/p&gt;

&lt;p&gt;Always leverage the existing knowledge and experience of peers and leaders: founders, product leadership, designers, other product engineers, etc.&lt;/p&gt;

&lt;p&gt;Examine the status quo to see how a problem is currently solved. Look at ideas, feedback, metrics, KPIs already collected in the past.&lt;/p&gt;

&lt;p&gt;Do we have users already doing something like this? What are their existing workflows? How could we improve their experience?&lt;/p&gt;

&lt;p&gt;Am I duplicating or potentially deprecating some functionality that already exists? Could this be achieved by extending or leveraging existing features? How are competitors solving this?&lt;/p&gt;

&lt;h2&gt;
  
  
  2 Craft
&lt;/h2&gt;

&lt;h2&gt;
  
  
  2.1 Am I proud of what I'm building?
&lt;/h2&gt;

&lt;p&gt;Your track record as a product engineer is the products and features you've delivered. Your last feature represents your professional competence level. No excuses.&lt;/p&gt;

&lt;p&gt;Ask yourself what quality standard do I want to set for the work I put out there?&lt;/p&gt;

&lt;p&gt;Can I be proud of the product I worked on? Is my work well tested and polished? Did I cut corners where I shouldn't have?&lt;/p&gt;

&lt;p&gt;As a highly paid professional engineer your craft is to produce high quality software which includes avoiding the creation of technical debt. Never ask for permission to improve quality!&lt;/p&gt;

&lt;p&gt;What makes a great product engineer stand out from the average software engineer is an intense sense of professional pride in their work and product.&lt;/p&gt;

&lt;h2&gt;
  
  
  2.2 Does the product feel good?
&lt;/h2&gt;

&lt;p&gt;This may be a slightly controversial take, but I believe a surprisingly large part of building great products is about developing a good taste for it.&lt;/p&gt;

&lt;p&gt;Simply knowing the difference between great vs. average and not settling for just ”ok” helps tremendously in making good decisions as a product engineer.&lt;/p&gt;

&lt;p&gt;Does the product feel smooth and consistent? Is it intuitive, simple, and familiar to the user? Or does it feel cheap and janky?&lt;/p&gt;

&lt;p&gt;Note that this doesn’t only concern the visual aspects of your product. Just slapping a fancy UI design on a shaky foundation doesn’t create a great experience.&lt;/p&gt;

&lt;p&gt;Simple. Elegant. Clean. This is what we're after as product engineers.&lt;/p&gt;

&lt;h2&gt;
  
  
  2.3 How do I get there faster?
&lt;/h2&gt;

&lt;p&gt;The pace of innovation especially in software is so rapid that in order to be competitive you must deliver fast, early and often.&lt;/p&gt;

&lt;p&gt;Too slow and your customers will lose trust in you while your competitors overtake you.&lt;/p&gt;

&lt;p&gt;What many get wrong about agile and building products is optimizing for predictability with estimates and roadmaps. Rather, what you really should care about is visible and continuous progress towards product goals.&lt;/p&gt;

&lt;p&gt;The goal of estimates should not be to try to be as accurate as possible but rather to set ambitious and yet achievable goals for yourself.&lt;/p&gt;

&lt;p&gt;The question is not how long you think it will take to build, but how long should it take? What’s an acceptable amount of time and effort I should invest on this?&lt;/p&gt;

&lt;p&gt;What’s the rollout strategy? How do I get this into customers' hands as soon as possible? What’s the MVP?&lt;/p&gt;

&lt;h2&gt;
  
  
  2.4 Teamwork
&lt;/h2&gt;

&lt;p&gt;Building products is a team sport. &lt;/p&gt;

&lt;p&gt;You are absolutely not expected to work alone and do everything yourself from writing code to doing user research. &lt;/p&gt;

&lt;p&gt;Am I effectively communicating with my team? Am I leveraging my team members' strengths? Are we celebrating our successes?&lt;/p&gt;

&lt;p&gt;Great teamwork results in great products. Invest in your team, and you will see the dividends in your product's success.&lt;/p&gt;

&lt;h2&gt;
  
  
  3 Growth
&lt;/h2&gt;

&lt;h2&gt;
  
  
  3.1 How do I measure the success of my work?
&lt;/h2&gt;

&lt;p&gt;Our work as product engineers is not just about building and shipping feature after feature. &lt;/p&gt;

&lt;p&gt;We make educated guesses about the most valuable thing to work on, so we should also be able to answer the question: What’s the impact of my work?&lt;/p&gt;

&lt;p&gt;How many customers did we talk to to validate our progress? Are they coming back? How much money does it generate?&lt;/p&gt;

&lt;p&gt;Use analytics tools and user feedback to help you understand what’s working and what’s not. Talk to real users to get qualitative insights about the product.&lt;/p&gt;

&lt;p&gt;Most importantly, make sure to share your outcomes openly and transparently. What did I ship in the last few months? What were the results?&lt;/p&gt;

&lt;h2&gt;
  
  
  3.2 How do I maximise the impact of my work?
&lt;/h2&gt;

&lt;p&gt;The most important question you should regularly ask yourself is: am I focused on the right thing?&lt;/p&gt;

&lt;p&gt;Being a good engineer is finding the biggest bottlenecks that hold us back and figuring out how to solve them. You should prioritise your efforts ruthlessly.&lt;/p&gt;

&lt;p&gt;Building products is a team sport. You should actively communicate what you’re working on and why, so that others can help you and keep you accountable. Demo progress frequently and actively seek feedback.&lt;/p&gt;

&lt;p&gt;Don’t fear taking risks. Being bold and taking the lead on delivering new and innovative features you believe in can lead to big wins.&lt;/p&gt;

&lt;p&gt;Ask yourself: How can I align my work with our company goals? How does my work directly benefit our users? Is there a way I can communicate my work better to others to get better feedback?&lt;/p&gt;

&lt;h2&gt;
  
  
  3.3 How do I stay ahead of the curve?
&lt;/h2&gt;

&lt;p&gt;Stay updated on market trends. Attend conferences, read up, follow experts.&lt;/p&gt;

&lt;p&gt;Make sure to research and benchmark competitor products, or other similar products whenever possible.&lt;/p&gt;

&lt;p&gt;Hold frequent retrospectives and brainstorming sessions. Experiment with new ideas. Encourage creativity in your team.&lt;/p&gt;

&lt;p&gt;What are the latest trends in my industry? How can I encourage innovation within my team? Are we taking enough time to learn from our successes &amp;amp; failures?&lt;/p&gt;

&lt;h2&gt;
  
  
  4 Product Vision
&lt;/h2&gt;

&lt;h2&gt;
  
  
  4.1 What’s our North Star?
&lt;/h2&gt;

&lt;p&gt;To align your work in the context of the broader product vision, you should deeply care about what others in the company are doing and saying, making sure you fully understand and are committed to the overall product strategy. Asking “why” is crucial here.&lt;/p&gt;

&lt;p&gt;What are our current product goals? What’s our growth strategy? Where do we want to be in the next 2-5 years?&lt;/p&gt;

&lt;p&gt;When presenting your work it always helps to put it in the context of how it pushes us forward in the big picture. How does my work help us reach our North Star?&lt;/p&gt;

&lt;h2&gt;
  
  
  4.2 How does my work impact the overall design of the product?
&lt;/h2&gt;

&lt;p&gt;Understanding the existing software’s design and architecture decisions helps make sure that your work fits well within the larger product design. Always consider how your work influences and is influenced by other components. &lt;/p&gt;

&lt;p&gt;When adding new functionality, you should ask: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Could this be achieved by extending or leveraging existing core features?&lt;/li&gt;
&lt;li&gt;Does my design follow a consistent style to the rest of the product?&lt;/li&gt;
&lt;li&gt;Am I adding complexity or reducing it? &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Simplicity is worth fighting for.&lt;/p&gt;

&lt;h2&gt;
  
  
  4.3 What's the ambition level?
&lt;/h2&gt;

&lt;p&gt;It’s a good idea to define the ambition level when setting off to build something new. Are you aiming for an incremental improvement or a revolutionary new functionality? &lt;/p&gt;

&lt;p&gt;Is this feature a USP or a basic expectation? What’s the ROI on building this feature? Does it make sense to spend the effort building this ourselves, or could we use an off the shelf library or service?&lt;/p&gt;

&lt;p&gt;Your time is extremely valuable. Think like an owner: Would you invest your salary to build the feature, or rather on something else?&lt;/p&gt;

&lt;h2&gt;
  
  
  Product Engineer Mindset
&lt;/h2&gt;

&lt;p&gt;If you enjoyed this checklist, you might also like the &lt;a href="https://github.com/anttiviljami/product-engineer-manifesto" rel="noopener noreferrer"&gt;Product Engineer Manifesto on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Consider giving the repository a star if the Product Engineering philosophy resonates with you!&lt;/p&gt;

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

</description>
      <category>career</category>
      <category>product</category>
      <category>engineer</category>
    </item>
  </channel>
</rss>
