<?xml version="1.0" encoding="utf-8" standalone="yes"?><feed xmlns="http://www.w3.org/2005/Atom">
  <title>End Point Dev blog</title>
  <subtitle>Ongoing observations by End Point Dev people</subtitle>
  <id>https://www.endpointdev.com/blog/</id>
  <link href="https://www.endpointdev.com/blog/"/>
  <link href="https://www.endpointdev.com/blog/" rel="self"/>
  <updated>2026-05-11T00:00:00+00:00</updated>
  <author>
    <name>End Point Dev</name>
  </author>
  
    <entry>
      <title>Introducing EP Audit</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2026/05/introducing-ep-audit/"/>
      <id>https://www.endpointdev.com/blog/2026/05/introducing-ep-audit/</id>
      <published>2026-05-11T00:00:00+00:00</published>
      <author>
        <name>Dan Briones</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2026/05/introducing-ep-audit/cover.webp&#34; alt=&#34;A panoramic view from a mountain overlooks a vast valley stretching to the horizon under a stormy sky.&#34;&gt;&lt;br&gt;
Photo by Bimal Gharti Magar, 2026.&lt;/p&gt;
&lt;p&gt;In the complex landscape of cloud security, staying compliant and prepared for audits is important. Enter EP Audit, a compliance-focused audit process, AI-powered but led by senior engineers. It&amp;rsquo;s designed for Azure, AWS, and Google Cloud setups. EP Audit emerged from necessity: When a financial services client required an Azure security audit, we at End Point built a solution that not only met their immediate needs, but also evolved into an internal tool we continue to refine. Today, it serves as a key part of our multi-cloud security audit framework and is tested against our own cloud accounts before being used in client engagements.&lt;/p&gt;
&lt;h4 id=&#34;the-problem&#34;&gt;The Problem&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Cloud environments are dynamic&lt;/strong&gt;: New projects, personnel changes, and vendor updates can significantly alter configurations over time. As environments evolve, the security posture originally reviewed and approved by your engineers may no longer fully match what is running in production. Unused resources, overly permissive access, and orphaned infrastructure can also accumulate over time, increasing both risk and monthly costs. This often goes unnoticed until an audit, customer review, or security incident forces a closer look.&lt;/p&gt;
&lt;p&gt;Modern compliance frameworks—SOC 2, ISO 27001, HIPAA, PCI DSS—require regular, documented security reviews. Yet many organizations still struggle to provide consistent documentation with timestamps, severity tracking, and proof of remediation. EP Audit helps address this gap by generating a structured audit trail throughout the review process.&lt;/p&gt;
&lt;h4 id=&#34;what-ep-audit-looks-like&#34;&gt;What EP Audit Looks Like&lt;/h4&gt;
&lt;p&gt;An EP Audit engagement is structured as a scheduled review cycle with two phases: a Base scan to establish a security snapshot, and a Final scan performed after remediation work to verify fixes. Both phases use a command-line runner with a read-only audit identity, ensuring no changes are made during the audit itself. Each Base scan produces nine deliverables designed to provide both high-level summaries and detailed technical findings:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Executive Summary:&lt;/strong&gt; Severity-ranked audit findings.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Client Report:&lt;/strong&gt; Full narrative with risk scorecard and roadmap.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Structured Findings Report:&lt;/strong&gt; Engineer-friendly findings by category.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pre-filled Hardening Checklist:&lt;/strong&gt; CIS-aligned checklist with auto-populated FAIL statuses.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fillable Hardening Checklist:&lt;/strong&gt; A remediation tool for your team.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Network Diagram:&lt;/strong&gt; Auto-generated deployment topology.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Findings Workbook:&lt;/strong&gt; Excel file with color-coded severity tabs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Remediation Plan:&lt;/strong&gt; Phased plan with affected resources.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Partner Remediation Identity Setup:&lt;/strong&gt; Guide for temporary write scope during remediation.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These deliverables are standardized across Azure, AWS, and GCP, helping simplify multi-cloud audit reviews and recurring engagements.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/05/introducing-ep-audit/epaudit-findings-category.webp&#34; alt=&#34;Findings by severity and category&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Sample summary of findings by severity and category on EP Audit.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Every finding in EP Audit is mapped to controls from recognized security frameworks. For Azure, we reference the CIS Microsoft Azure Foundations Benchmark and the Microsoft Cloud Security Benchmark. AWS findings align with the CIS AWS Foundations Benchmark, AWS Foundational Security Best Practices, and related guidance. GCP findings follow the CIS GCP Foundation Benchmark and the Google Cloud Architecture Framework Security Pillar. This helps keep audit findings grounded in established industry standards rather than proprietary scoring systems or arbitrary recommendations.&lt;/p&gt;
&lt;h4 id=&#34;a-service-not-just-a-tool&#34;&gt;A Service, Not Just a Tool&lt;/h4&gt;
&lt;p&gt;There are already plenty of cloud configuration scanners available. What matters is how the findings are reviewed, prioritized, and applied in real environments.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Senior Engineering Review:&lt;/strong&gt; Our engineers provide context and actionable recommendations for each finding, bridging the gap between automated results and practical solutions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;End Point-Specific Enhancements:&lt;/strong&gt; We include checks beyond published frameworks, such as billing data analysis for orphaned resources and third-party security tool integration.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Internal Validation:&lt;/strong&gt; Every framework change is tested on End Point&amp;rsquo;s own cloud accounts before client deployment.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A single audit provides a snapshot in time, but recurring audits make it easier to identify trends, validate remediation progress, and detect configuration drift over time. EP Audit includes year-over-year comparison reporting to help highlight both improvements and regressions. In practice, this helps support compliance reviews, procurement questionnaires, and ongoing security review processes while providing better visibility into how environments evolve over time.&lt;/p&gt;
&lt;h4 id=&#34;phased-remediation&#34;&gt;Phased Remediation&lt;/h4&gt;
&lt;p&gt;EP Audit generates remediation scripts and recommendations organized by severity:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;P1 (0–7 days):&lt;/strong&gt; Critical issues like exposed management ports.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P2 (30 days):&lt;/strong&gt; High-priority issues such as encryption gaps.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P3 (90 days):&lt;/strong&gt; Medium-priority defense-in-depth weaknesses.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;P4 (Maintenance):&lt;/strong&gt; Low-priority configuration hygiene.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Scripts default to dry-run mode, allowing proposed changes to be reviewed before execution. Our team can assist with remediation directly, or clients can implement the recommendations independently using the provided guidance.&lt;/p&gt;
&lt;p&gt;Over multiple engagements, organizations begin building a clearer picture of how their cloud environments evolve over time. Recurring audits make it easier to track remediation progress, identify recurring issues, and detect configuration drift before it becomes a larger problem. The resulting documentation can also help support compliance reviews, customer security questionnaires, cyber insurance requirements, internal security tracking, and reduce monthly costs.&lt;/p&gt;
&lt;h4 id=&#34;working-with-end-point&#34;&gt;Working with End Point&lt;/h4&gt;
&lt;p&gt;If your organization operates workloads in Azure, AWS, or Google Cloud and needs a structured, standards-aligned cloud security review process, feel free to &lt;a href=&#34;/contact/&#34;&gt;contact us&lt;/a&gt;. If you are working with us already, you can talk with your representative to find out more.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Designing software architecture with Domain-Driven Design</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2026/05/designing-software-architecture-ddd-part-3/"/>
      <id>https://www.endpointdev.com/blog/2026/05/designing-software-architecture-ddd-part-3/</id>
      <published>2026-05-05T00:00:00+00:00</published>
      <author>
        <name>Kevin Campusano</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2026/05/designing-software-architecture-ddd-part-3/cover.webp&#34; alt=&#34;A panoramic view from a mountain overlooks a vast valley stretching to the horizon under a stormy sky.&#34;&gt;&lt;br&gt;
Photo by Juan Pablo Ventoso, 2023.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is part 3 of a series of blog posts on Domain-Driven Design:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&#34;/blog/2026/04/high-level-system-analysis-and-design-ddd-part-1/&#34;&gt;High level system analysis and design with Domain-Driven Design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;/blog/2026/04/implementing-business-logic-ddd-part-2/&#34;&gt;Implementing business logic with Domain-Driven Design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;/blog/2026/05/designing-software-architecture-ddd-part-3/&#34;&gt;Designing software architecture with Domain-Driven Design&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Domain-Driven Design&lt;/strong&gt; is an approach to software development that focuses on, &lt;a href=&#34;https://www.oreilly.com/library/view/domain-driven-design-tackling/0321125215/&#34;&gt;as Eric Evans puts it&lt;/a&gt;, &amp;ldquo;tackling the complexity in the heart of software&amp;rdquo;. And what is in the heart of software? The business domain in which it operates. Or more specifically: a &lt;strong&gt;model&lt;/strong&gt; of it, made of code. That is, the code that implements the business logic that comes into play when solving problems within the realm of a particular business activity.&lt;/p&gt;
&lt;p&gt;DDD is not just about writing code though. It&amp;rsquo;s a whole methodology that touches on business needs, requirements gathering, organizational dynamics, high level architectural design, and lower level patterns for implementing software intensive systems.&lt;/p&gt;
&lt;p&gt;As a result, DDD offers a treasure trove of concepts, patterns and tools that can be applied to any software project, regardless of the size and complexity.&lt;/p&gt;
&lt;p&gt;In this series of blog posts we&amp;rsquo;re going to explore many aspects of DDD. We will be following the structure laid out by &lt;a href=&#34;https://vladikk.com/&#34;&gt;Vlad Khononov&lt;/a&gt;&amp;rsquo;s excellent book on the topic &amp;ldquo;&lt;a href=&#34;https://www.oreilly.com/library/view/learning-domain-driven-design/9781098100124/&#34;&gt;Learning Domain-Driven Design: Aligning Software Architecture and Business Strategy&lt;/a&gt;&amp;rdquo;. So you can think of this series as a summary of that book. An abridged version that can serve as a review for anybody who has read it; but also as an entry point for people who are new to DDD.&lt;/p&gt;
&lt;h3 id=&#34;table-of-contents&#34;&gt;Table of contents&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#designing-software-architecture-with-domain-driven-design&#34;&gt;Designing software architecture with Domain-Driven Design&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#table-of-contents&#34;&gt;Table of contents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#section-8-architectural-patterns&#34;&gt;Section 8: Architectural patterns&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#layered-architecture&#34;&gt;Layered architecture&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#ports-and-adapters&#34;&gt;Ports and adapters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#command-query-responsibility-segregation&#34;&gt;Command query responsibility segregation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#scope&#34;&gt;Scope&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#section-9-communication-patterns&#34;&gt;Section 9: Communication patterns&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#model-translation&#34;&gt;Model translation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#stateless-model-translation&#34;&gt;Stateless model translation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#stateful-model-translation&#34;&gt;Stateful model translation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#integrating-aggregates&#34;&gt;Integrating aggregates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#outbox&#34;&gt;Outbox&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#saga&#34;&gt;Saga&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#process-manager&#34;&gt;Process manager&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#architectural-patterns-to-use-within-bounded-contexts&#34;&gt;Architectural patterns to use within bounded contexts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#patterns-for-communicating-across-bounded-contexts&#34;&gt;Patterns for communicating across bounded contexts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;section-8-architectural-patterns&#34;&gt;Section 8: Architectural patterns&lt;/h3&gt;
&lt;p&gt;Now that we&amp;rsquo;ve seen various patterns for implementing business logic, i.e. &amp;ldquo;the heart of software&amp;rdquo;. We turn our attention to architecture.&lt;/p&gt;
&lt;p&gt;Indeed, the business logic is the raison d&amp;rsquo;être for a software application. But applications have other responsibilities that are also important. Like interacting with users, receiving requests and returning results, storing data, interfacing with external services. In order to balance all these concerns and make sure the code base does not devolve into an unmaintainable big ball of mud, we need to be intentional in how we organize it. We need to design its architecture.&lt;/p&gt;
&lt;p&gt;That is, the rules and principles that we follow to organize the various aspects of the code base and create clear boundaries between them. In essence, software architecture is about defining a system&amp;rsquo;s big logical components, their dependencies and interactions.&lt;/p&gt;
&lt;p&gt;In this section we will see three common architectural patterns: layered architecture, ports and adapters, and command query responsibility segregation.&lt;/p&gt;
&lt;h4 id=&#34;layered-architecture&#34;&gt;Layered architecture&lt;/h4&gt;
&lt;p&gt;The &lt;strong&gt;layered architecture&lt;/strong&gt; is one of the most common architectural patterns out there. It has been present, in one form or another, for a long time. The main idea of the pattern is to separate applications into three layers: the presentation layer, the business logic layer, and the data access layer.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/05/designing-software-architecture-ddd-part-3/layered-architecture.png&#34; alt=&#34;Layered architecture&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Layered architecture.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;presentation layer&lt;/strong&gt; (or user interface layer) is meant to implement the mechanisms through which consumers interact with the application. This means the app&amp;rsquo;s graphical user interface (GUI), command line interface (CLI) or application programming interface (API).&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;business logic layer&lt;/strong&gt; (or domain layer) implements the business rules, validation and invariants. This is where we&amp;rsquo;d implement the patterns that we&amp;rsquo;ve seen so far like transaction script, active record, or a domain model.&lt;/p&gt;
&lt;p&gt;Sometimes, an additional &amp;ldquo;&lt;strong&gt;service layer&lt;/strong&gt;&amp;rdquo; or &amp;ldquo;&lt;strong&gt;application layer&lt;/strong&gt;&amp;rdquo; emerges between the presentation and business logic layers. When the domain logic needs some level of orchestration, such as it is the case with active records and domain models, it is often useful to further separate the presentation and business logic layers by exposing a sort of &amp;ldquo;public interface&amp;rdquo; to the domain. That is, a series of procedures (i.e. transaction scripts) that serve as a facade that maps presentation layer actions (e.g. user interactions) to business domain transactions.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/05/designing-software-architecture-ddd-part-3/layered-architecture-with-service-layer.png&#34; alt=&#34;Layered architecture with service layer&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The service layer sits between the presentation and business logic layers. It implements a series of actions which map closely to the operations exposed to users by the presentation layer.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example of refactoring a &amp;ldquo;fat&amp;rdquo; controller by introducing a service layer:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// This is an MVC controller that implements a REST API endpoint for adding&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// items to a shopping cart. It lives in the application&amp;#39;s presentation layer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// and leverages business logic layer components to run its logic.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;namespace&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;MvcEcommerce.WebApi.Controllers&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#369&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#369&#34;&gt;[Route(&amp;#34;api/[controller]&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;)]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#369&#34;&gt;[ApiController]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;QuoteItemsController&lt;/span&gt; : ControllerBase
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// This action takes care of processing user input, fetching HTTP context&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// values like cookies, responding with proper HTTP status codes and&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// orchestrating the business logic.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#369&#34;&gt;    [HttpPost]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;async&lt;/span&gt; Task&amp;lt;ActionResult&amp;gt; Post([FromBody] QuoteItemPost payload)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; quoteId = _quoteCookieManager.GetQuoteIdFromCookie(Request);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; quote = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; _quoteRepository.FindOpenByIdAsync(quoteId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; (quote == &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;null&lt;/span&gt;) &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; NotFound(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Quote not found&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; product = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; _productRepository.FindByIdAsync(payload.ProductId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; (product == &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;null&lt;/span&gt;) &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; NotFound(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Product not found&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; matchingQuoteItem = quote.GetItemBy(productId: payload.ProductId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; (matchingQuoteItem != &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;null&lt;/span&gt;) &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; BadRequest(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Item already exists&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; quoteItem = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; QuoteItem()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            Product = product,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            Quantity = payload.Quantity,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        };
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        quote.Items.Add(quoteItem);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; _quoteRepository.UpdateAsync(quote);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; Ok(quoteItem);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This controller does too much. We can move a lot of its logic into a service layer component:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;namespace&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;MvcEcommerce.WebApi.Controllers&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#369&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#369&#34;&gt;[Route(&amp;#34;api/[controller]&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;)]
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#369&#34;&gt;[ApiController]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;QuoteItemsController&lt;/span&gt; : ControllerBase
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// After refactoring, this action is now much simpler and only concerned&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// with presentation layer issues. That is, handling user input and the HTTP&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// specific aspects of request processing. It relies on the service layer to&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// orchestrate the business logic.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#369&#34;&gt;    [HttpPost]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;async&lt;/span&gt; Task&amp;lt;ActionResult&amp;gt; Post([FromBody] QuoteItemPost quoteItem)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;try&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; quoteId = _quoteCookieManager.GetQuoteIdFromCookie(Request);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; result = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; _quoteItemCreator.Run(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                QuoteId = quoteId.Value,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                ProductId = quoteItem.ProductId,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                Quantity = quoteItem.Quantity
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; Ok(result);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;catch&lt;/span&gt; (EntityNotFoundException ex)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; NotFound(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; ErrorMessage(ex));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;catch&lt;/span&gt; (DomainException ex)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; BadRequest(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; ErrorMessage(ex));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// This service object implements all the domain logic orchestration that&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// used to live in the controller. Notice how this code is not concerned with&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// presentation layer responsibilities like handling HTTP specific logic, for&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// example.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;namespace&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;MvcEcommerce.ServiceLayer.Services&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;QuoteItemCreator&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;async&lt;/span&gt; Task&amp;lt;QuoteItem&amp;gt; Run(InputPayload payload)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; product =
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; _productRepository.FindByIdAsync(payload.ProductId) ??
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; EntityNotFoundException(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Product not found&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; quote =
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; _quoteRepository.FindOpenByIdAsync(payload.QuoteId) ??
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; EntityNotFoundException(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Quote not found&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; matchingQuoteItem = quote.GetItemBy(productId: payload.ProductId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; (matchingQuoteItem != &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;null&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; DomainException(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Item already exists&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; quoteItem = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; QuoteItem()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            Product = product,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            Quantity = payload.Quantity,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        };
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        quote.Items.Add(quoteItem);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; _quoteRepository.UpdateAsync(quote);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; quoteItem;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally, the &lt;strong&gt;data access layer&lt;/strong&gt; is meant to provide the means of interacting with persistent storage like databases, search indexes, file systems, etc. In modern systems, this layer has evolved into more of a &amp;ldquo;infrastructure&amp;rdquo; layer, and taken on the responsibility of interacting with external APIs and other kinds of web services. So, not strictly limited to pure &amp;ldquo;data storage&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;The communication between these layers is one-way, from top to bottom. Meaning that the presentation layer holds a reference to, depends on, and calls to the business logic layer. Same with the business logic layer to the data access layer.&lt;/p&gt;
&lt;p&gt;This communication pattern is excellent for active record and transaction script based systems. For domain models, it begins to fall a bit short. This is because the business logic depending on the data access logic contradicts one of the core principles of domain models: the fact that they are supposed to be plain old objects, with no dependencies on frameworks or infrastructure.&lt;/p&gt;
&lt;h4 id=&#34;ports-and-adapters&#34;&gt;Ports and adapters&lt;/h4&gt;
&lt;p&gt;The &lt;strong&gt;ports and adapters architecture&lt;/strong&gt; leverages the dependency inversion principle to address the shortcomings of the traditional layered architecture and make it ideal for implementing domain models. Its main advantage is that it decouples the business logic layer from the infrastructure.&lt;/p&gt;
&lt;p&gt;The &lt;a href=&#34;https://en.wikipedia.org/wiki/Dependency_inversion_principle&#34;&gt;&lt;strong&gt;dependency inversion principle&lt;/strong&gt;&lt;/a&gt; dictates that, instead of higher level components directly depending on and referencing lower level ones; it is the lower level components that should depend on the higher level ones. This is done by the higher level components defining contracts for the lower level components to implement, and through those, be integrated into the higher level components&amp;rsquo; workflows. The higher level components only ever interact with the lower level ones through the contracts that they themselves define.&lt;/p&gt;
&lt;p&gt;Case in point: instead of the business logic depending on data access logic, like in the layered architecture; the business logic layer takes center stage and defines the contracts that the data access layer (and really, all infrastructure) must abide to in order to be usable to the business logic.&lt;/p&gt;
&lt;p&gt;And that&amp;rsquo;s precisely where the ports and adapters name comes from. The business logic defines contracts/interfaces, AKA &amp;ldquo;ports&amp;rdquo;; and the infrastructure provides concrete implementations for these interfaces which can talk to external components: the &amp;ldquo;adapters&amp;rdquo;. Then, application bootstrapping logic, or &lt;a href=&#34;https://en.wikipedia.org/wiki/Dependency_injection&#34;&gt;&lt;strong&gt;dependency injection&lt;/strong&gt;&lt;/a&gt;, takes care of supplying the concrete objects (or adapters) to the abstract interfaces (or ports) that the business logic specifies. This is exactly what a domain model calls for.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example of a minimal, thin vertical slice of an application designed using the ports and adapters pattern:&lt;/p&gt;
&lt;p&gt;The business logic implements some procedure which necessitates interacting with the database, an email delivery service, and a payment processor:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;namespace&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Ecommerce.BusinessLogicLayer.Services&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;OrderCreator&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// This service does not directly depend on concrete classes. Instead, it&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// references abstract interfaces. The concrete implementations are given&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// via dependency injection through the constructor.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;readonly&lt;/span&gt; IOrderRepository _orderRepository;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;readonly&lt;/span&gt; IOrderConfirmationMailer _orderConfirmationMailer;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;readonly&lt;/span&gt; IPaymentGateway _paymentGateway;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; OrderCreator(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        IOrderRepository orderRepository,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        IOrderConfirmationMailer orderConfirmationMailer,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        IPaymentGateway paymentGateway
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _orderRepository = orderRepository;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _orderConfirmationMailer = orderConfirmationMailer;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _paymentGateway = paymentGateway;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;async&lt;/span&gt; Task&amp;lt;Order&amp;gt; Run(InputPayload payload)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; order = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; Order(payload);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; result = _paymentGateway.SubmitPayment(order);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; (!result.IsSuccess)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; DomainException(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Error processing payment&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; _orderRepository.Save(order);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; _orderConfirmationMailer.Send(order);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; order;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The business logic layer also defines its ports. That is, interfaces that the infrastructure layer will have to implement:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;namespace&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Ecommerce.BusinessLogicLayer.Interfaces&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;IPaymentGateway&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    PaymentTransactionResult SubmitPayment(Order order);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;IOrderRepository&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    Task&amp;lt;Order&amp;gt; Save(Order Order);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;IOrderConfirmationMailer&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    Task Send(Order order);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The infrastructure layer provides implementations for these interfaces:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;namespace&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Ecommerce.InfrastructureLayer.Payments&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;AuthorizeNetPaymentGateway&lt;/span&gt; : IPaymentGateway
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; AuthorizeNetPaymentGateway(&lt;span style=&#34;color:#888&#34;&gt;/* ... */&lt;/span&gt;) { &lt;span style=&#34;color:#888&#34;&gt;/* ... */&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; PaymentTransactionResult SubmitPayment(Order order) { &lt;span style=&#34;color:#888&#34;&gt;/* ... */&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;namespace&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Ecommerce.InfrastructureLayer.Repositories&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;OrderRepository&lt;/span&gt; : IOrderRepository
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; OrderRepository(&lt;span style=&#34;color:#888&#34;&gt;/* ... */&lt;/span&gt;) { &lt;span style=&#34;color:#888&#34;&gt;/* ... */&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; Task&amp;lt;Order&amp;gt; Save(Order Order) { &lt;span style=&#34;color:#888&#34;&gt;/* ... */&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;namespace&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Ecommerce.InfrastructureLayer.Mailers&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;OrderConfirmationAwsSesMailer&lt;/span&gt; : IOrderConfirmationMailer
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; OrderConfirmationAwsSesMailer(&lt;span style=&#34;color:#888&#34;&gt;/* ... */&lt;/span&gt;) { &lt;span style=&#34;color:#888&#34;&gt;/* ... */&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; Task Send(Order order) { &lt;span style=&#34;color:#888&#34;&gt;/* ... */&lt;/span&gt; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Like mentioned before, everything can be wired together via dependency injection or some other form of bootstrapping. Most frameworks have their own way of resolving dependencies and instantiating service objects like these; in order to execute them as a result of requests from consumers (e.g. a CLI command, a web request, the click of a button). In essence, it&amp;rsquo;s something like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; repository = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; OrderRepository(dbContext);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; mailer = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; OrderConfirmationAwsSesMailer(mailerConfig);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; paymentGateway = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; AuthorizeNetPaymentGateway(paymentConfig);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; orderCreator = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; OrderCreator(repository, mailer, paymentGateway);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;orderCreator.Run(payload);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;So, the ports and adapters architectural pattern has a &lt;strong&gt;business logic layer&lt;/strong&gt; which has no dependencies on any other components outside of itself. It defines a set of interfaces for all external components that want to interact with it. It also has an &lt;strong&gt;infrastructure layer&lt;/strong&gt; which implements concrete classes for the domain layer&amp;rsquo;s interfaces. Data access, interaction with external services, user interface, and presentation logic all live here. And finally, it has a &lt;strong&gt;service/application layer&lt;/strong&gt; which, similar to its counterpart from the layered architecture, can emerge between the business logic and infrastructure layers when needed to expose a set of procedures that closely map to user interface actions. It exposes all the business operations that the system supports and orchestrates the business logic to carry them out.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/05/designing-software-architecture-ddd-part-3/ports-and-adapters.png&#34; alt=&#34;The ports and adapters architectural pattern&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;When organizing the code following the the ports and adapters architectural pattern, the business logic layer defines interfaces/ports, which the infrastructure layer implements concrete objects for. These objects take care of interacting with the external world.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Clean architecture, onion architecture and hexagonal architecture are all different names for the same core concepts and principles espoused by ports and adapters; sometimes with slight variations depending on the particular flavor and tech stack.&lt;/p&gt;
&lt;h4 id=&#34;command-query-responsibility-segregation&#34;&gt;Command query responsibility segregation&lt;/h4&gt;
&lt;p&gt;The &lt;strong&gt;command query responsibility segregation pattern&lt;/strong&gt; (CQRS) builds on the principles from ports and adapters and adds support for multiple different representations of the system&amp;rsquo;s data. That is, having multiple persistence models for the same data set. One of the most common examples is a system that stores and operates on day to day business transactions using an OLTP (&lt;a href=&#34;https://en.wikipedia.org/wiki/Online_transaction_processing&#34;&gt;online transaction processing&lt;/a&gt;) representation, but also needs to provide an OLAP (&lt;a href=&#34;https://en.wikipedia.org/wiki/Online_analytical_processing&#34;&gt;online analytical processing&lt;/a&gt;) data warehouse for high level business analysis. One ground-truth source of data (the OLTP) is used to produce additional representations with a different schema (the OLAP). CQRS enables this.&lt;/p&gt;
&lt;p&gt;As such, CQRS is ideal for event sourced domain models, because it allows persisting the many projections of an aggregate&amp;rsquo;s data into their own databases. (Remember that in the context of event sourcing, a projection is a representation of the state of a business entity which is constructed based on its stored domain events). It allows of course, querying of these projections with much more flexibility than what a pure event store allows on its own.&lt;/p&gt;
&lt;p&gt;At the core of CQRS there are two types of models: a command execution model (the C in CQRS) and one or many read, or query, models (the Q in CQRS). In database terms, this is similar to a &lt;a href=&#34;https://en.wikipedia.org/wiki/Master%E2%80%93slave_(technology)&#34;&gt;&lt;strong&gt;primary-replica&lt;/strong&gt;&lt;/a&gt; type of situation, where the command execution model represents the primary, and the read models represent the replicas.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/05/designing-software-architecture-ddd-part-3/cqrs.png&#34; alt=&#34;Command query responsibility segregation&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;CQRS exposes two types of models, one for executing commands and many others for reading. In the backend, a projection engine keeps the read models up to date with the latest changes from the command execution model.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;command execution model&lt;/strong&gt; is the system&amp;rsquo;s source of truth, whose data is strongly consistent. It&amp;rsquo;s the one used to execute and record business operations, and enforce business rules and invariants. All operations that result in changes to the system state are handled here.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;query, or read, models&lt;/strong&gt; take care of exposing different projections of the system state. These are read-only models that are meant for presenting the system&amp;rsquo;s data to its consumers. They are generated based on the main data source, which is the data maintained in the command execution model. In fact, read models should be capable of being easily destroyed and recreated from the main data.&lt;/p&gt;
&lt;p&gt;Read models are generated using components that we call &lt;strong&gt;projection engines&lt;/strong&gt;, which can work synchronously or asynchronously when fetching data from the command execution model to generate their projections.&lt;/p&gt;
&lt;p&gt;Synchronously, projection engines generate read models using a &lt;strong&gt;catch-up subscription&lt;/strong&gt; design. It works like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The projection engine determines the last query checkpoint. Basically, when was the last time it read the main database.&lt;/li&gt;
&lt;li&gt;The projection engine queries the main database and identifies newly added and updated records since the last query checkpoint.&lt;/li&gt;
&lt;li&gt;The projection engine uses the new data to regenerate or update the read model.&lt;/li&gt;
&lt;li&gt;The projection engine updates the latest query checkpoint, to be used during the next execution of the engine, starting again at step 1.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/05/designing-software-architecture-ddd-part-3/synchronous-projection-engine.png&#34; alt=&#34;Synchronous projection engine&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Synchronous projection engine.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This checkpoint can be implemented in various ways. The main idea is to offer a mechanism for the projection engine to be able to tell which records are new or have changed since its last run. One way to do it is using something akin to SQL Server&amp;rsquo;s &lt;a href=&#34;https://learn.microsoft.com/en-us/sql/t-sql/data-types/rowversion-transact-sql?view=sql-server-ver17&#34;&gt;&lt;strong&gt;rowversion&lt;/strong&gt;&lt;/a&gt; feature. This is a database-wide, auto-incrementing numeric value that increases after every INSERT and UPDATE operation. The newly incremented value is assigned to the rows that were added or updated. With this, the projection engine can easily query all records whose rowversion is greater than the last checkpoint. Similar functionality can be implemented using database triggers too. Or database tables can also include &lt;strong&gt;timestamp&lt;/strong&gt; fields that indicate when records were last touched.&lt;/p&gt;
&lt;p&gt;Asynchronous projection engines on the other hand, rely on the command execution model publishing all changes to a &lt;strong&gt;message bus&lt;/strong&gt;. The projection engine can subscribe to these messages and update its read model as they come. This method, while scalable, comes with drawbacks inherent to distributed computing like handling duplicates and out of order messages. It&amp;rsquo;s also more difficult to destroy and regenerate read models that rely on asynchronous messaging only, since the messages are gone after they are processed. Often the better solution is to use a synchronous design and then, only if needed, augment it with the asynchrony.&lt;/p&gt;
&lt;p&gt;Naturally, CQRS is ideal when the system needs to support different types of databases. Imagine an online store, for example, which has an inventory management component that works directly with the command execution model backed by a relational database. But for displaying the product catalog to users, it uses a read model backed by a search index, optimized for full text search. And for event sourced domain models, CQRS is practically mandatory.&lt;/p&gt;
&lt;h4 id=&#34;scope&#34;&gt;Scope&lt;/h4&gt;
&lt;p&gt;The patterns that we&amp;rsquo;ve seen in this section are not exclusively meant as system-wide nor even bounded-context-wide organizational principles. Arbitrarily enforcing a single pattern everywhere often leads to accidental complexity. Instead we should follow DDD&amp;rsquo;s core principles and deploy these strategies according to what the business domain necessitates. Indeed, within a bounded context, especially one that deals with many subdomains, there should be logical separation between these subdomains. Each resulting module could use a different architectural pattern. In other words, these patterns divide the code base into horizontal slices; and the subdomains can be used to define the vertical slices. This way, a monolithic bounded context can be modularized. This leaves the code base in a good position for future refactoring and further physical separation into distinct bounded contexts in the future (i.e. into separate applications, services and/or processes).&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/05/designing-software-architecture-ddd-part-3/architectural-slices.png&#34; alt=&#34;Architectural slices&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;When needed, different architectural patterns can be deployed to different subdomains within the same bounded context.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;section-9-communication-patterns&#34;&gt;Section 9: Communication patterns&lt;/h3&gt;
&lt;p&gt;In the last few sections we&amp;rsquo;ve discussed how to implement business logic and how to leverage architectural patterns to organize the code within a bounded context. In this section, we will take a higher level view and go beyond the scope of a single bounded context. We will learn about patterns of communication across bounded contexts. In other words, how to integrate them.&lt;/p&gt;
&lt;h4 id=&#34;model-translation&#34;&gt;Model translation&lt;/h4&gt;
&lt;p&gt;Back when we talked about integrating bounded contexts, we discussed how they can use different communication strategies depending on the disposition of the teams that own them. When the teams have strong communication, &lt;strong&gt;cooperation&lt;/strong&gt; patterns like &lt;strong&gt;partnership&lt;/strong&gt; and &lt;strong&gt;shared kernel&lt;/strong&gt; can be used for integration. Here, bounded contexts can communicate without friction. The protocols for doing so are defined by the teams in an ad-hoc manner and development moves forward without too many issues.&lt;/p&gt;
&lt;p&gt;However, when the teams are not closely aligned, and cooperation patterns are impossible, a &lt;strong&gt;customer-supplier&lt;/strong&gt; relationship emerges. This can be addressed by implementing an &lt;strong&gt;anticorruption layer&lt;/strong&gt; in the downstream consumer, to adapt the upstream supplier&amp;rsquo;s model to the consumer&amp;rsquo;s needs. Another option is for the upstream supplier to implement an &lt;strong&gt;open-host service&lt;/strong&gt; and expose an integration-specific &lt;strong&gt;published language&lt;/strong&gt;, which isolates consumers from the details of its internal model. Both approaches are meant to protect a bounded context&amp;rsquo;s model from the influence of external models.&lt;/p&gt;
&lt;p&gt;In these non-cooperation cases, we have to be more intentional in how we design the communication between bounded contexts, and the need for a translation of their models/languages arises. This logic that translates the models can either be stateless or stateful.&lt;/p&gt;
&lt;h4 id=&#34;stateless-model-translation&#34;&gt;Stateless model translation&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Stateless model translation&lt;/strong&gt; can happen on the fly via the &lt;a href=&#34;https://refactoring.guru/design-patterns/proxy&#34;&gt;&lt;strong&gt;proxy design pattern&lt;/strong&gt;&lt;/a&gt;. The idea is to put an intermediary component in place, which receives messages in one language, translates them, and forwards them to their destination.&lt;/p&gt;
&lt;p&gt;If the proxy is implemented by the consumer or downstream component, functioning as an anticorruption layer, then it intercepts outgoing requests and translates them to a language that the upstream supplier can understand. It does the same with the incoming responses from upstream: it translates them to a language that the consumer can understand.&lt;/p&gt;
&lt;p&gt;If the proxy is implemented by the supplier or upstream component, it functions as an open-host service. It takes the incoming requests that arrive using the published language and translates them to the supplier&amp;rsquo;s internal one. Then, outgoing responses get translated to the published language.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/05/designing-software-architecture-ddd-part-3/synchronous-proxy.png&#34; alt=&#34;Synchronous proxy&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;In essence, a proxy is nothing more than a component that sits between two other components and translates the messages passing between them.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;These proxies can be synchronous or asynchronous.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Synchronous&lt;/strong&gt; translation is straightforward. Messages are translated just as they are being received or sent, depending on whether the translation happens upstream or downstream. Normally, the translation logic is implemented directly in the bounded context that needs it.&lt;/p&gt;
&lt;p&gt;In some cases though, it makes sense to separate it into its own independent component, implementing an &lt;a href=&#34;https://microservices.io/patterns/apigateway.html&#34;&gt;&lt;strong&gt;API gateway&lt;/strong&gt;&lt;/a&gt; pattern. Sometimes, off the shelf software or cloud services like &lt;a href=&#34;https://www.krakend.io/&#34;&gt;KrakenD&lt;/a&gt; or &lt;a href=&#34;https://docs.aws.amazon.com/apigateway/latest/developerguide/welcome.html&#34;&gt;AWS API Gateway&lt;/a&gt; can be used to implement them.&lt;/p&gt;
&lt;p&gt;Having a separate API gateway has some advantages, other than the obvious decoupling of model translation logic from actual business logic. It can make it easier to &lt;a href=&#34;https://restfulapi.net/versioning/&#34;&gt;expose multiple versions&lt;/a&gt; of the API. That is, of the published language. It can also be consumed by multiple downstream components, making it essentially an integration-specific bounded context. We call these, &lt;strong&gt;interchange contexts&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/05/designing-software-architecture-ddd-part-3/api-gateway.png&#34; alt=&#34;API Gateway&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;When extracted into its own component, a proxy becomes an API gateway. This is useful for further separation of concerns, offering multiple versions of the API&amp;rsquo;s published language, and serving multiple consumers.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Asynchronous&lt;/strong&gt; translation on the other hand, is not direct. It relies on an event driven design to implement a &lt;strong&gt;message proxy&lt;/strong&gt;; which subscribes to events published by one bounded context, translates them, and forwards them to their destination.&lt;/p&gt;
&lt;p&gt;The proxy can also apply filtering to the messages, deciding which ones to forward and which ones to ignore. This is useful, for example, to keep the published language free of domain events that are meant to be internal to the bounded context that produces them.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/05/designing-software-architecture-ddd-part-3/asynchronous-proxy.png&#34; alt=&#34;Asynchronous proxy&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;An asynchronous proxy listens to events in one language and translates them to another language.&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&#34;stateful-model-translation&#34;&gt;Stateful model translation&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Stateful model translation&lt;/strong&gt; comes into play when more complex translation logic that requires persistent storage is needed. This is useful for two common scenarios: when the requests need to be aggregated, and when data from multiple sources needs to be unified. In both cases the component doing the translation needs to keep track of the incoming requests and messages. It stores them in a database in order to reconstruct them eventually.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Data aggregation&lt;/strong&gt; may be needed for performance reasons when, for example, multiple requests need to be collected together and dispatched as one batch. Another case is when multiple disparate messages need to be combined into one single bigger message that conveys more complete information. This can work both synchronously and asynchronously. That is, the messages can be direct requests or published events.&lt;/p&gt;
&lt;p&gt;An API gateway on its own is not appropriate for this type of model translation, as it doesn&amp;rsquo;t provide persistent storage or more complex processing logic beyond mapping and rerouting. Instead, it can be implemented from scratch as a bespoke solution, or leveraging off the shelf stream processing platforms like &lt;a href=&#34;https://kafka.apache.org/&#34;&gt;Apache Kafka&lt;/a&gt; or &lt;a href=&#34;https://aws.amazon.com/kinesis/&#34;&gt;AWS Kinesis&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/05/designing-software-architecture-ddd-part-3/stateful-proxy.png&#34; alt=&#34;Stateful model translation&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A proxy can aggregate and/or combine multiple separate messages into a single one for performance and consolidation purposes.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;unification of multiple data sources&lt;/strong&gt; is also a common use case for stateful model translation. This is necessary for example when a component needs to process data from many different upstream suppliers and apply complex business logic to it. This is commonly seen in the &lt;a href=&#34;https://samnewman.io/patterns/architectural/bff/&#34;&gt;&lt;strong&gt;backend for frontend&lt;/strong&gt;&lt;/a&gt; pattern. In this pattern, an API is defined to meet all of a frontend&amp;rsquo;s needs, while serving as a facade for a number of backend services. This allows the frontend to call a single backend to interact with all the various services it may need.&lt;/p&gt;
&lt;h4 id=&#34;integrating-aggregates&#34;&gt;Integrating aggregates&lt;/h4&gt;
&lt;p&gt;Before, we talked about the rules that we follow to limit the scope of aggregates and ensure they encapsulate a coherent set of business logic. We strive to keep them focused, with tight boundaries. They represent strong data consistency and transactional boundaries. That is, they should only contain strongly consistent data and the system&amp;rsquo;s transactions should be limited to a single aggregate instance. It is also true however, that there are business processes that involve multiple aggregates. The following patterns allow us to implement such business processes without breaking the aggregates&amp;rsquo; isolation rules.&lt;/p&gt;
&lt;h4 id=&#34;outbox&#34;&gt;Outbox&lt;/h4&gt;
&lt;p&gt;As we know, along with commands, domain events are part of an aggregate&amp;rsquo;s public interface. By publishing domain events to a message bus, aggregates communicate with the outside world. Or more specifically, to their subscribers, who listen to these events in order to trigger their own logic. The outbox pattern allows aggregates to reliably publish domain events.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;outbox&lt;/strong&gt; pattern works by storing domain events in the same database as the aggregate&amp;rsquo;s data. Leveraging database transactions to ensure both the state changes and the events are saved atomically. The process is this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Aggregate state changes and new events are committed in the same database transaction.&lt;/li&gt;
&lt;li&gt;The message relay picks up the new events from the database.&lt;/li&gt;
&lt;li&gt;The message relay publishes the new events to the message bus.&lt;/li&gt;
&lt;li&gt;After successful publishing, the message relay marks the event as such, or deletes it.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/05/designing-software-architecture-ddd-part-3/outbox-pattern.png&#34; alt=&#34;The outbox pattern&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The outbox pattern is about storing an aggregate&amp;rsquo;s domain events in a database, making sure to commit them in the same transaction as state changes.&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&#34;saga&#34;&gt;Saga&lt;/h4&gt;
&lt;p&gt;A &lt;strong&gt;saga&lt;/strong&gt; leverages the reliable publishing of domain events afforded to us by the outbox pattern to implement business processes that span multiple aggregates. They do this by listening to events and responding to them by issuing commands to relevant components. A saga represents a long-running business process. One that spans multiple transactions.&lt;/p&gt;
&lt;p&gt;Sagas can vary in complexity. Sometimes it is enough to respond to events by directly issuing commands from the saga itself.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;OrderProcessingSaga&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;readonly&lt;/span&gt; IOrderRepository _orderRepository;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;readonly&lt;/span&gt; IPaymentService _paymentService;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;readonly&lt;/span&gt; IInventoryService _inventoryService;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;readonly&lt;/span&gt; IShippingService _shippingService;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;//...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// These event handler methods all follow a similar pattern:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// Receive the event, then call the corresponding command.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Process(OrderCaptured @event)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; order = _orderRepository.Fetch(@event.OrderId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _paymentService.ProcessPayment(order);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Process(PaymentAccepted @event)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; order = _orderRepository.Fetch(@event.OrderId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _inventoryService.PreparePackage(order);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        order.UpdateStatus(@event.NewOrderStatus);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _orderRepository.Save(order);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Process(InventoryCleared @event)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; order = _orderRepository.Fetch(@event.OrderId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _shippingService.ScheduleDelivery(order);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        order.UpdateStatus(@event.NewOrderStatus);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _orderRepository.Save(order);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Process(ShipmentDispatched @event)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; order = _orderRepository.Fetch(@event.OrderId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        order.UpdateStatus(@event.NewOrderStatus);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _orderRepository.Save(order);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Other times, a saga needs to keep its own state. For example, when the saga needs to track received events; to react to certain failure conditions; issue retries, etc. In these cases, the saga can be implemented as an event sourced aggregate, keep a record of all received events, and publish events of its own that contain the commands that it wants to execute. Then, a separate message relay component should be listening to the saga&amp;rsquo;s events and run the commands contained within.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;OrderProcessingSaga&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// Like any other event sourced aggregate, this saga stores its list of&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// events.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;readonly&lt;/span&gt; IList&amp;lt;DomainEvent&amp;gt; _events = [];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// These event handlers all also follow a similar pattern. Instead of&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// directly calling the commands though, they register an event with the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// necessary information for the message relay to call the commands.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Process(OrderCaptured @event)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _events.Append(@event);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _events.Append(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; CommandIssuedEvent(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            target: Target.PaymentService,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            command: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; ProcessPaymentCommand(@event.OrderId)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Process(PaymentAccepted @event)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _events.Append(@event);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _events.Append(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; CommandIssuedEvent(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            target: Target.InventoryService,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            command: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; PreparePackageCommand(@event.OrderId)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Process(InventoryCleared @event)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _events.Append(@event);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _events.Append(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; CommandIssuedEvent(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            target: Target.ShippingService,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            command: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; ScheduleDeliveryCommand(@event.OrderId)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Process(ShipmentDispatched @event)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _events.Append(@event);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&#34;/blog/2026/05/designing-software-architecture-ddd-part-3/saga.png&#34; alt=&#34;The saga pattern&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Sagas represent a long running business process. One that spans many transactions. They receive events and invoke commands as a result.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Always remember though, sagas make it possible to implement business processes that span multiple aggregates, but the data across aggregates is only eventually consistent. The core aggregate design rules still apply: only the data within an aggregate is strongly consistent.&lt;/p&gt;
&lt;h4 id=&#34;process-manager&#34;&gt;Process manager&lt;/h4&gt;
&lt;p&gt;The &lt;strong&gt;process manager&lt;/strong&gt; pattern is essentially a more complex saga. While sagas mostly deal with linear flows, and direct mapping between events and commands; a process manager implements more complex business logic that keeps track of the sequence of events, has its own state, and determines how to react. It does not only have a simple mapping of events to commands. Instead it has complex logic that decides how to respond to incoming events. It is a central processing unit, listening to events from and issuing commands to multiple sources, and managing an overall business process.&lt;/p&gt;
&lt;p&gt;Another difference between them is that sagas are instantiated implicitly, while process managers are instantiated explicitly. Meaning that sagas live and die within the context of the events that they listen to. They &amp;ldquo;get activated&amp;rdquo; when an event that they are interested in is published. Process managers on the other hand get activated when the business process they manage gets initiated. They stay alive through the duration of the process&amp;rsquo;s many steps.&lt;/p&gt;
&lt;p&gt;Implementation wise, they are essentially aggregates with a strong focus on responding to events. They have their own persistent state, explicit identity, lifecycle, and related objects; as well as a series of event handlers, just like a saga would have.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/05/designing-software-architecture-ddd-part-3/process-manager.png&#34; alt=&#34;Process manager&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A process manager orchestrates a long running business process with complex logic that involves many components. The application layer instantiates them explicitly, as it would any other aggregate.&lt;/em&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;OrderProcessManager&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;readonly&lt;/span&gt; IList&amp;lt;DomainEvent&amp;gt; _events = [];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// Process managers have their own explicit identity. They are very similar&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// to aggregates or entities.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; OrderId _id;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// They can include other properties to track state as needed.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// Process managers usually include an initialization procedure that kicks&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// off the process, and provides the necessary parameters.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Initialize(&lt;span style=&#34;color:#888&#34;&gt;/* ... */&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _events.Append(orderProcessingInitiated);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _events.Append(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; CommandIssuedEvent(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            target: Target.PaymentService,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            command: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; ProcessPaymentCommand(@event.OrderId)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// The event handlers are very similar to those of the event sourced&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// stateful saga. The difference is that here they run more complex logic to&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// determine how to continue the process. As opposed to the saga&amp;#39;s simpler&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// mapping of incoming events to commands.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Process(PaymentAccepted @event)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _events.Append(@event);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _events.Append(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; CommandIssuedEvent(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            target: Target.InventoryService,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            command: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; PreparePackageCommand(@event.OrderId)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Process(InventoryCleared @event)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _events.Append(@event);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _events.Append(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; CommandIssuedEvent(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            target: Target.ShippingService,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            command: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; ScheduleDeliveryCommand(@event.OrderId)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Process(ShipmentDispatched @event)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _events.Append(@event);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id=&#34;architectural-patterns-to-use-within-bounded-contexts&#34;&gt;Architectural patterns to use within bounded contexts&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/05/designing-software-architecture-ddd-part-3/architecture-concept-map.png&#34; alt=&#34;Concept map of the architectural patterns&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;These are the main concepts around the architectural patterns that we&amp;rsquo;ve discussed.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;patterns-for-communicating-across-bounded-contexts&#34;&gt;Patterns for communicating across bounded contexts&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/05/designing-software-architecture-ddd-part-3/communication-concept-map.png&#34; alt=&#34;Concept map of the communication patterns&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;These are the main concepts around the communication patterns that we&amp;rsquo;ve discussed.&lt;/em&gt;&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Implementing business logic with Domain-Driven Design</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2026/04/implementing-business-logic-ddd-part-2/"/>
      <id>https://www.endpointdev.com/blog/2026/04/implementing-business-logic-ddd-part-2/</id>
      <published>2026-04-21T00:00:00+00:00</published>
      <author>
        <name>Kevin Campusano</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2026/04/implementing-business-logic-ddd-part-2/cover.webp&#34; alt=&#34;A panoramic view from a mountain overlooks a vast valley stretching to the horizon under a stormy sky.&#34;&gt;&lt;br&gt;
Photo by Bimal Gharti Magar, 2026.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is part 2 of a series of blog posts on Domain-Driven Design:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&#34;/blog/2026/04/high-level-system-analysis-and-design-ddd-part-1/&#34;&gt;High level system analysis and design with Domain-Driven Design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;/blog/2026/04/implementing-business-logic-ddd-part-2/&#34;&gt;Implementing business logic with Domain-Driven Design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;/blog/2026/05/designing-software-architecture-ddd-part-3/&#34;&gt;Designing software architecture with Domain-Driven Design&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Domain-Driven Design&lt;/strong&gt; is an approach to software development that focuses on, &lt;a href=&#34;https://www.oreilly.com/library/view/domain-driven-design-tackling/0321125215/&#34;&gt;as Eric Evans puts it&lt;/a&gt;, &amp;ldquo;tackling the complexity in the heart of software&amp;rdquo;. And what is in the heart of software? The business domain in which it operates. Or more specifically: a &lt;strong&gt;model&lt;/strong&gt; of it, made of code. That is, the code that implements the business logic that comes into play when solving problems within the realm of a particular business activity.&lt;/p&gt;
&lt;p&gt;DDD is not just about writing code though. It&amp;rsquo;s a whole methodology that touches on business needs, requirements gathering, organizational dynamics, high level architectural design, and lower level patterns for implementing software intensive systems.&lt;/p&gt;
&lt;p&gt;As a result, DDD offers a treasure trove of concepts, patterns and tools that can be applied to any software project, regardless of the size and complexity.&lt;/p&gt;
&lt;p&gt;In this series of blog posts we&amp;rsquo;re going to explore many aspects of DDD. We will be following the structure laid out by &lt;a href=&#34;https://vladikk.com/&#34;&gt;Vlad Khononov&lt;/a&gt;&amp;rsquo;s excellent book on the topic &amp;ldquo;&lt;a href=&#34;https://www.oreilly.com/library/view/learning-domain-driven-design/9781098100124/&#34;&gt;Learning Domain-Driven Design: Aligning Software Architecture and Business Strategy&lt;/a&gt;&amp;rdquo;. So you can think of this series as a summary of that book. An abridged version that can serve as a review for anybody who has read it; but also as an entry point for people who are new to DDD.&lt;/p&gt;
&lt;h3 id=&#34;table-of-contents&#34;&gt;Table of contents&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#implementing-business-logic-with-domain-driven-design&#34;&gt;Implementing business logic with Domain-Driven Design&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#table-of-contents&#34;&gt;Table of contents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#section-5-implementing-simple-business-logic&#34;&gt;Section 5: Implementing simple business logic&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#transaction-script&#34;&gt;Transaction script&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#active-record&#34;&gt;Active record&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#when-to-use-them&#34;&gt;When to use them&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#section-6-tackling-complex-business-logic&#34;&gt;Section 6: Tackling complex business logic&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#domain-model&#34;&gt;Domain model&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#value-object&#34;&gt;Value object&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#entities&#34;&gt;Entities&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#aggregates&#34;&gt;Aggregates&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#domain-event&#34;&gt;Domain event&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#domain-service&#34;&gt;Domain service&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#data-access-concerns&#34;&gt;Data access concerns&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#section-7-modeling-the-dimension-of-time&#34;&gt;Section 7: Modeling the dimension of time&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#data-storage-and-retrieval&#34;&gt;Data storage and retrieval&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#advantages-and-disadvantages&#34;&gt;Advantages and disadvantages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#the-event-sourced-domain-model&#34;&gt;The event sourced domain model&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#ddd-tools-for-implementing-business-logic&#34;&gt;DDD tools for implementing business logic&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;section-5-implementing-simple-business-logic&#34;&gt;Section 5: Implementing simple business logic&lt;/h3&gt;
&lt;p&gt;Now that we&amp;rsquo;ve explored DDD&amp;rsquo;s higher level system design concepts, it&amp;rsquo;s time to zoom in and start looking at how to implement business logic: the most important part of software. We will start by discussing two patterns that are ideal for implementing simple business logic: transaction script and active record.&lt;/p&gt;
&lt;h4 id=&#34;transaction-script&#34;&gt;Transaction script&lt;/h4&gt;
&lt;p&gt;&lt;a href=&#34;https://martinfowler.com/eaaCatalog/transactionScript.html&#34;&gt;According to Martin Fowler&lt;/a&gt;, the &lt;strong&gt;transaction script&lt;/strong&gt; pattern &amp;ldquo;organizes business logic by procedures where each procedure handles a single request from the presentation.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;For most programmers that have experience with procedural languages, the transaction script is easy to grasp. This pattern is about conceptualizing a system as a collection of transactions. And organizing these transactions as independent, transactional procedural scripts. Think one transaction script per use case, exposed for the users to invoke when they need to.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/implementing-business-logic-ddd-part-2/transaction-script.png&#34; alt=&#34;Transaction script example&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The core idea of transaction script is about putting business transactions front and center. Closely related scripts can be implemented as methods in a service class. Or you could also have a separate &amp;ldquo;service object&amp;rdquo; class for each script.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Indeed, on its own, the transaction script pattern is a simple way of organizing relatively simple domain logic. However, as we&amp;rsquo;ll see throughout the next few sections, it is also a foundational pattern that is present when implementing the more advanced ones. Also, even though the concept is simple, implementation of transaction script in the real world has to be done carefully, in order to avoid falling into common pitfalls, often related to ensuring atomicity.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;AddItemToQuote&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// This method adds a new item to a shopping cart and records the operation&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// into a log table. Both operations are done separately.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Run(&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; quoteId, &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; productId, &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; quantity)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _db.ExecuteSql(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;$&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;            INSERT INTO quote_items(quote_id, product_id, quantity)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;            VALUES ({quoteId}, {productId}, {quantity});
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;        &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// If some error happens after the previous command and before the one&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// below, the system will be left in an inconsistent state. The new item&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// would have been added to the shopping cart, but the audit log would&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// not have a corresponding event.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _db.ExecuteSql(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;$&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;            INSERT INTO quote_audit_log(
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;                quote_id, event_description, date_created
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;            )
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;            VALUES ({quoteId}, &amp;#39;Item added to quote&amp;#39;, CURRENT_DATE);
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;        &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That is, a transaction script needs to actually be &lt;strong&gt;transactional&lt;/strong&gt;. It needs to be atomic. Of course, each domain&amp;rsquo;s requirements will dictate how true this is, but in general, we want transaction scripts to operate as a unit, and make sure that they don&amp;rsquo;t leave the system in an inconsistent state in situations when the script fails midway through its execution.&lt;/p&gt;
&lt;p&gt;If all the script does is interact with a relational database, it is easy to address this issue. The solution is to perform the operations within a &lt;strong&gt;database transaction&lt;/strong&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;AddItemToQuoteWithTransaction&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Run(&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; quoteId, &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; productId, &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; quantity)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;try&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#888&#34;&gt;// We can use a database transaction to fix the issue.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            _db.BeginTransaction();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            _db.ExecuteSql(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;$&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;                INSERT INTO quote_items(quote_id, product_id, quantity)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;                VALUES ({quoteId}, {productId}, {quantity});
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;            &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            _db.ExecuteSql(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;$&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;                INSERT INTO quote_audit_log(
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;                    quote_id, event_description, date_created
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;                )
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;                VALUES ({quoteId}, &amp;#39;Item added to quote&amp;#39;, CURRENT_DATE);
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;            &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            _db.CommitTransaction();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;catch&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            _db.RollbackTransaction();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;throw&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The problem gets more complicated when the script performs a &lt;strong&gt;distributed transaction&lt;/strong&gt;. That is, when it interacts with other systems which are outside of the scope of a relational database transaction. For example, interacting with the file system, or with an external web service.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;AddItemToQuoteWithDistributedTransaction&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Run(&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; quoteId, &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; productId, &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; quantity)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _db.ExecuteSql(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;$&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;            INSERT INTO quote_items(quote_id, product_id, quantity)
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;            VALUES ({quoteId}, {productId}, {quantity});
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;        &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// We&amp;#39;re back to our initial problem. Any failure at this point in the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// script will have updated the shopping cart without having sent the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// notification to the external system.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _api.NotifyQuoteItemAdded(quoteId, productId, quantity);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;These are hard to deal with and often require very particular code to handle properly. Further in this series we&amp;rsquo;ll discuss this topic again when we talk about &lt;a href=&#34;https://martinfowler.com/bliki/CQRS.html&#34;&gt;CQRS&lt;/a&gt; and the &lt;a href=&#34;https://microservices.io/patterns/data/transactional-outbox.html&#34;&gt;outbox pattern&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Sometimes though, the distributed transaction is not as obvious. Consider a web application that exposes an endpoint to increment the quantity of particular shopping cart items by one:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;IncrementQuoteItemQuantity&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Run(&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; quoteItemId)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _db.ExecuteSql(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;$&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;            UPDATE quote_items
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;            SET quantity = quantity + 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;            WHERE id = {quoteItemId};
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;        &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Even though that&amp;rsquo;s a single database operation, there is inter-system communication between the user&amp;rsquo;s browser and the application server; and between the application server and the database. A network outage, for example, that happens in the middle of this operation can leave the system in an inconsistent state. If it fails after it&amp;rsquo;s already submitted the &lt;code&gt;UPDATE&lt;/code&gt; command to the database, and responds with a failure message to the client, the client might attempt to try and increase the quantity again. This would result in the increment being done two times, when actually it should have been only one.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/implementing-business-logic-ddd-part-2/implicit-distributed-transaction.png&#34; alt=&#34;Implicit distributed transaction&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;In the context of client-server applications, some transactions are distributed even though at first glance they might not seem that way. In this simple operation, there are three systems interacting over the network: The client, the application server and the database.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;There are two possible ways of handling this type of situation: making the operation idempotent, or using optimistic concurrency control.&lt;/p&gt;
&lt;p&gt;One way to make the operation &lt;strong&gt;idempotent&lt;/strong&gt;, that is, to make sure that it always produces the same results, no matter how many times it&amp;rsquo;s done, is to have the caller specify the quantity to update the item to. That is, changing the operation from &amp;ldquo;add one quantity&amp;rdquo;, to &amp;ldquo;set the quantity to this&amp;rdquo;. Like so:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;IncrementQuoteItemQuantityIdempotent&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Run(&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; quoteItemId, &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; quantity)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _db.ExecuteSql(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;$&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;            UPDATE quote_items
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;            SET quantity = {quantity}
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;            WHERE id = {quoteItemId};
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;        &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;On the other hand, implementing &lt;strong&gt;optimistic concurrency control&lt;/strong&gt; in this scenario could be done by effectuating the record update while using the current state of the record when checking for a match. This boils down to including the expected values of the different fields in the &lt;code&gt;WHERE&lt;/code&gt; clause. This way, the record will be updated only if it is in the state that the caller expected it to be:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;IncrementQuoteItemQuantityOptimisticConcurrency&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Run(&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; quoteItemId, &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; quantity)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _db.ExecuteSql(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;$&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;            UPDATE quote_items
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;            SET quantity = quantity + 1
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;            WHERE id = {quoteItemId} AND quantity = {quantity};
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;        &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In both cases, the caller would have to obtain the current state of the record before attempting the update operation.&lt;/p&gt;
&lt;h4 id=&#34;active-record&#34;&gt;Active record&lt;/h4&gt;
&lt;p&gt;&lt;a href=&#34;https://www.martinfowler.com/eaaCatalog/activeRecord.html&#34;&gt;Martin Fowler describes&lt;/a&gt; the &lt;strong&gt;active record&lt;/strong&gt; pattern as &amp;ldquo;an object that wraps a row in a database table or view, encapsulates the database access, and adds domain logic on that data.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;The main advantage of the active record pattern is that it greatly simplifies database access. Especially when leveraging &lt;a href=&#34;https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping&#34;&gt;ORM frameworks&lt;/a&gt;. With active record, you usually end up with a set of classes that closely mirror your database structure. You have one class per table, where the fields of those classes represent table columns and the instances of those classes represent individual records in those tables.&lt;/p&gt;
&lt;p&gt;So, when the underlying data model is complex, business logic organized in transaction scripts can be augmented with active record to reduce a great deal of the complexity involved in database interactions. This is because the active record pattern takes care of mapping between database records and in-memory objects, as well as all the data retrieval and manipulation commands (I.e. the &lt;a href=&#34;https://en.wikipedia.org/wiki/Create,_read,_update_and_delete&#34;&gt;CRUD&lt;/a&gt;). This allows the transaction scripts to focus more on domain logic, and less on manipulating the underlying data.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;AddItemToQuoteActiveRecord&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Run(&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; quoteId, &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; productId, &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; quantity)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// Active record allows us to work with objects instead of directly&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// issuing database commands.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; item = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; QuoteItem
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            QuoteId = quoteId,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ProductId = productId,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            Quantity = quantity
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        };
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        item.Save();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Of course, active records can (and should) also include domain logic. Aspects like the relationships between the domain entities, validation, complex database queries, and even the business procedures that involve them are all part of an active record object.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# This class is an Order active record. It establishes relationships with other&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# entities, defines basic validation rules, and has custom queries.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Order&lt;/span&gt; &amp;lt;  &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ApplicationRecord&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.table_name = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;orders&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    enum &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:status&lt;/span&gt;, [ &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:pending&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:paid&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:shipped&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:cancelled&lt;/span&gt; ]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    enum &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:payment_method&lt;/span&gt;, [ &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:cash&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:credit_card&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:transfer&lt;/span&gt; ]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    has_many &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:order_items&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    has_one &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:shipping_address&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    has_one &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:billing_address&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    validates &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:email&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;presence&lt;/span&gt;: &lt;span style=&#34;color:#080&#34;&gt;true&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;email&lt;/span&gt;: {&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;mode&lt;/span&gt;: &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:strict&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    validates &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:phone&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;length&lt;/span&gt;: {&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;minimum&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;10&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;allow_blank&lt;/span&gt;: &lt;span style=&#34;color:#080&#34;&gt;true&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    scope &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:sorted&lt;/span&gt;, -&amp;gt; { order(&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;created_at&lt;/span&gt;: &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:desc&lt;/span&gt;) }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    scope &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:new_ones&lt;/span&gt;, -&amp;gt; { where(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;created_at &amp;gt;= ?&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt;.day.ago) }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    scope &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:have_phone&lt;/span&gt;, -&amp;gt; { where.not(&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;phone&lt;/span&gt;: &lt;span style=&#34;color:#080&#34;&gt;nil&lt;/span&gt;) }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    scope &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:where_email_is&lt;/span&gt;, -&amp;gt;(email) { where(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;lower(email) = ?&amp;#34;&lt;/span&gt;, email.downcase) }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# The framework augments it with data access logic, like querying:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;order = &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Order&lt;/span&gt;.find(&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;123&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;expensive_items = order.order_items.where(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;price &amp;gt;= ?&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;200&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;orders_from_us = &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Order&lt;/span&gt;.where(&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;country_code&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;US&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;new_orders = &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Order&lt;/span&gt;.new_ones.sorted
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# ...and making changes:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;new_order = &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Order&lt;/span&gt;.new(&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;email&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;test@email.com&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;new_order.save
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;order.update(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;shipping_number&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;1234567890&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;status&lt;/span&gt;: &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:shipped&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;The &lt;a href=&#34;https://guides.rubyonrails.org/active_record_basics.html&#34;&gt;Active Record&lt;/a&gt; framework from Ruby on Rails is a great implementation of the pattern that puts business logic in a centralized location while &lt;em&gt;magically&lt;/em&gt; extending the objects with all the data access logic they need.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This is a big step up from raw transaction scripts, but still it has its own disadvantages. First, the active records can end up having too much responsibility, and grow to unmanageable sizes, especially for core business entities. Second, the active record pattern by itself does not enforce access control on its properties. Meaning that external processes can freely modify their state, potentially ignoring any business rules that could bind them.&lt;/p&gt;
&lt;p&gt;Something else to be aware of is that when active records define little to no behavior, that is, when they don&amp;rsquo;t implement business logic, they become an &lt;a href=&#34;https://martinfowler.com/bliki/AnemicDomainModel.html&#34;&gt;anemic domain model&lt;/a&gt;. Active record objects that focus only on database access tasks, and higher level &amp;ldquo;service objects&amp;rdquo; that manipulate them and implement all the domain logic themselves are the hallmark of anemic domain models.&lt;/p&gt;
&lt;p&gt;An anemic domain model may look like a full fledged domain model, with all the objects representing domain concepts and their relationships, but they are hollow. Their most important part is missing. In the next section we will see what a real domain model looks like, DDD&amp;rsquo;s definitive pattern for implementing business logic in complex subdomains.&lt;/p&gt;
&lt;h4 id=&#34;when-to-use-them&#34;&gt;When to use them&lt;/h4&gt;
&lt;p&gt;In some circles, the transaction script and active record patterns are considered anti patterns. But really, they are just tools for the job. When misapplied, they become detrimental, but when used to solve the problems they are good at, they shine. In fact, they can give you a lot of bang for your design buck. But when the domain logic you&amp;rsquo;re implementing is very complex, they can begin to fall short, as their relatively low level of abstraction becomes insufficient and prone to code repetition, and inconsistencies when this repeated code goes out of sync. Which is a big problem when dealing with complex subdomains.&lt;/p&gt;
&lt;h3 id=&#34;section-6-tackling-complex-business-logic&#34;&gt;Section 6: Tackling complex business logic&lt;/h3&gt;
&lt;p&gt;When implementing complex business logic, the patterns that we&amp;rsquo;ve seen up to this point can only get you so far. They start to leave a lot to be desired due to their relatively low level of abstraction. When the situation calls for a higher level of abstraction, in order to produce a more supple design, DDD calls for the domain model pattern.&lt;/p&gt;
&lt;h4 id=&#34;domain-model&#34;&gt;Domain model&lt;/h4&gt;
&lt;p&gt;&lt;a href=&#34;https://martinfowler.com/eaaCatalog/domainModel.html&#34;&gt;Taking from Martin Fowler&amp;rsquo;s definition&lt;/a&gt;, we learn that the &lt;strong&gt;domain model&lt;/strong&gt; is a full object-oriented model of the domain, that incorporates both behavior and data. Domain models take the form of a large web of interconnected objects, where each one represents a meaningful concept in the business domain.&lt;/p&gt;
&lt;p&gt;In his book, Eric Evans expanded greatly on this definition, introducing a set of patterns and tools for implementing domain models. Indeed, to build a domain model, we incorporate other building block patterns: value objects, entities, aggregates, domain events, domain services.&lt;/p&gt;
&lt;p&gt;Also, there are two main rules that a domain model needs to follow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;There should be no dependency on particular frameworks or infrastructure. Objects in the domain model should be &lt;strong&gt;plain old objects&lt;/strong&gt;, focused only on domain logic.&lt;/li&gt;
&lt;li&gt;The domain model should speak the &lt;strong&gt;ubiquitous language&lt;/strong&gt; of the &lt;strong&gt;bounded context&lt;/strong&gt; in which it operates. That means that all identifiers should call back to business concepts and it should represent the mental model of the domain experts.&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&#34;value-object&#34;&gt;Value object&lt;/h4&gt;
&lt;p&gt;A &lt;strong&gt;value object&lt;/strong&gt; is an object whose identity is given by its properties. That is, its value. It does not have an explicit identifier, like an Id field. Consider for example a &lt;code&gt;Point&lt;/code&gt; object:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Point&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;double&lt;/span&gt; _x;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;double&lt;/span&gt; _y;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The X and Y values completely define a point. Two instances of this class with the same X and Y coordinates represent the same point. Changing the value of either coordinate produces a new point.&lt;/p&gt;
&lt;p&gt;They are useful for representing properties of other objects and are usually implemented as &lt;strong&gt;immutable objects&lt;/strong&gt;. One of the great strengths of value objects is that they allow the model to speak the ubiquitous language by replacing primitives with bespoke small objects that make the code clearer and encapsulate related business logic.&lt;/p&gt;
&lt;p&gt;As an example, consider this &lt;code&gt;Order&lt;/code&gt; class.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Order&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; _id;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;string&lt;/span&gt; _status;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;string&lt;/span&gt; _email;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;string&lt;/span&gt; _phone;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;double&lt;/span&gt; _shippingWeight;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;string&lt;/span&gt; _countryCode;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; Order(&lt;span style=&#34;color:#888&#34;&gt;/*...*/&lt;/span&gt;) {&lt;span style=&#34;color:#888&#34;&gt;/*...*/&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// This class can be instantiated like this:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; order = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; Order(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    id: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;12345&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    status: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Processing&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    email: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;test@email.com&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    phone: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;123-456-7890&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    shippingWeight: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;10&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    countryCode: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;US&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here, all the values are assigned by convention. An email, a phone number, a status&amp;hellip; They are all just strings with no special behavior or meaning. We must know what they look like beforehand in order to assign them correctly.&lt;/p&gt;
&lt;p&gt;If we use value objects, this class could be implemented like this instead:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Order&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; OrderId _id;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; OrderStatus _status;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; PersonName _name;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; EmailAddress _email;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; PhoneNumber _phone;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; Weight _shippingWeight;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; CountryCode _country;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; Order(&lt;span style=&#34;color:#888&#34;&gt;/*...*/&lt;/span&gt;) {&lt;span style=&#34;color:#888&#34;&gt;/*...*/&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// Now, instances can be created like this:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; order = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; Order(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    id: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; OrderId(&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;12345&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    status: OrderStatus.Processing,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    name: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; PersonName(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Kevin&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Campusano&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    email: EmailAddress.Parse(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;test@email.com&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    phone: PhoneNumber.Parse(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;1234567890&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    shippingWeight: Weight.FromLbs(&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;10.25&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    country: CountryCode.Parse(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;US&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This approach has many advantages:&lt;/p&gt;
&lt;p&gt;First of all the &lt;code&gt;Order&lt;/code&gt; class does not have to validate its fields. Validation can happen in the value objects. This is good because it allows other domain objects to have fields of the same type without having to duplicate the validation logic themselves. Imagine you also have a contact object somewhere in your model that includes a phone number field, for example. Both it and &lt;code&gt;Order&lt;/code&gt; can reuse the phone number value object, and the logic it carries.&lt;/p&gt;
&lt;p&gt;Secondly, value objects can capture the business logic that&amp;rsquo;s closely related to them. A phone number field, for example can implement methods to obtain further information about it like its area code or the country it belongs to. A &amp;ldquo;weight&amp;rdquo; value object can implement logic for converting from one measuring system to another.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// Here&amp;#39;s a weight value object that encapsulates logic like converting from one&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// unit to another and comparing the magnitude of different values.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Weight&lt;/span&gt; : IComparable&amp;lt;Weight&amp;gt;, IEquatable&amp;lt;Weight&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;double&lt;/span&gt; LbsPerKg = &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;2.20462&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;double&lt;/span&gt; _lbs;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; Weight(&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;double&lt;/span&gt; lbs) { _lbs = lbs; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;static&lt;/span&gt; Weight FromLbs(&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;double&lt;/span&gt; lbs) =&amp;gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; Weight(lbs);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;static&lt;/span&gt; Weight FromKgs(&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;double&lt;/span&gt; kgs) =&amp;gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; Weight(kgs * LbsPerKg);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;double&lt;/span&gt; ToLbs() =&amp;gt; _lbs;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;double&lt;/span&gt; ToKgs() =&amp;gt; _lbs / LbsPerKg;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; CompareTo(Weight? other) =&amp;gt; ToLbs().CompareTo(other?.ToLbs());
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;bool&lt;/span&gt; Equals(Weight? other) =&amp;gt; ToLbs().Equals(other?.ToLbs());
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally, using value objects lets the model speak the ubiquitous language, and thus makes it clearer. Strongly typing properties in this way, beyond just using language primitives, very clearly captures the intent of the property.&lt;/p&gt;
&lt;h4 id=&#34;entities&#34;&gt;Entities&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Entities&lt;/strong&gt; are objects that represent the concepts in the domain that have a lifecycle and explicit identification. A person, an order, a lead, a transaction. These are all examples of entities. Entities, as opposed to value objects, are &lt;strong&gt;not immutable&lt;/strong&gt;, and are expected to change throughout their life in the system. Value objects, like we saw before, are ideal for representing properties of entities.&lt;/p&gt;
&lt;p&gt;Entities are a core building block for a domain model. However, they are not used independently. They are used as part of an aggregate.&lt;/p&gt;
&lt;h4 id=&#34;aggregates&#34;&gt;Aggregates&lt;/h4&gt;
&lt;p&gt;An &lt;strong&gt;aggregate&lt;/strong&gt; is a &lt;strong&gt;hierarchy&lt;/strong&gt; of entities and value objects that are bound together by closely related business logic. The aggregate forms a boundary that protects the consistency of the objects that compose it. It achieves this by preventing external objects from directly modifying its state and defining a public interface through which other parts of the system can interact with it.&lt;/p&gt;
&lt;p&gt;The rest of the system cannot directly mutate the state of the entities within an aggregate. They can only do so via the methods exposed by the aggregate&amp;rsquo;s public interface. These methods, so called &lt;strong&gt;commands&lt;/strong&gt;, encapsulate the aggregate&amp;rsquo;s business logic and protect them from corruption, by enforcing the necessary validations and invariants. Indeed, all the business logic that&amp;rsquo;s closely related to the aggregate lives in one place: the aggregate itself.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// Here we have an aggregate that represents a shopping cart and its items.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Quote&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// It does not expose its list of items publicly. Instead, it implements&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// public methods for manipulating them.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    IList&amp;lt;QuoteItem&amp;gt; _items = [];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    QuoteItem? GetItemBy(ProductId productId) =&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _items.FirstOrDefault(i =&amp;gt; i.ProductId == productId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; AddItem(ProductId productId, &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; quantity)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; item = GetItemBy(productId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; (item != &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;null&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; InvalidOperationException(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Item already exists.&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _items.Add(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; QuoteItem
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ProductId = productId,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            Quantity = quantity
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// Even though this method modifies an item, and not the quote itself, it&amp;#39;s&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// still implemented here, because this is the aggregate root.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; SetItemQuantity(ProductId productId, &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; quantity)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; item = GetItemBy(productId) ??
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; InvalidOperationException(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Item does not exist.&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        item.Quantity = quantity;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;These commands, which are the public interface of an aggregate, should all be defined in a single entity within the aggregate. We call this entity the &lt;strong&gt;aggregate root&lt;/strong&gt;. If the aggregate is a hierarchy of objects, and we can picture it as a tree, then the root is the object that exists at the root of the tree, where all branches come from.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/implementing-business-logic-ddd-part-2/aggregate-root.png&#34; alt=&#34;The aggregate root&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The aggregate is a hierarchy of objects. The aggregate root is the sole object in this hierarchy with which other components interact.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This focus on commands makes the implementation of the application layer components more straightforward. I.e. they become &lt;strong&gt;transaction scripts&lt;/strong&gt;. By &amp;ldquo;application layer components&amp;rdquo;, I mean those components that orchestrate calls to the domain model in order to fulfill use cases in response to, say, user requests. They follow a general pattern of:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Load the aggregate. Typically from persistent storage.&lt;/li&gt;
&lt;li&gt;Invoke the desired command.&lt;/li&gt;
&lt;li&gt;Persist the new state of the aggregate.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For example:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// Here&amp;#39;s an application service leveraging the quote aggregate to add an item&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// to a shopping cart.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;AddItemToQuote&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; Result Run(QuoteId quoteId, ProductId productId, &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; quantity)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;try&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; quote = _quoteRepository.FindById(quoteId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; (quote == &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;null&lt;/span&gt;) &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; Result.Error(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Quote not found.&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            quote.AddItem(productId, quantity);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            _quoteRepository.Save(quote);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; Result.Success();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// Notice how the application layer logic takes care of database related&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// errors. Remember that the aggregate is a plain old object which has&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// no knowledge about infrastructure or frameworks. The data access&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// logic, in this case encapsulated in the _quoteRepository, is the one&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// that will produce such errors when trying to commit changes to the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// underlying storage.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;catch&lt;/span&gt; (ConcurrencyException ex)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; Result.Error(ex);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There are other rules that aggregates need to follow. One of them is that the aggregate acts as a &lt;strong&gt;transaction boundary&lt;/strong&gt; for aggregate operations. That is, all changes to an aggregate should be transactional, atomic. Also, no system operation should involve a transaction that includes different aggregates. We should have one aggregate per database transaction.&lt;/p&gt;
&lt;p&gt;This reveals another aspect of aggregates: An aggregate should expect &lt;strong&gt;strong consistency&lt;/strong&gt; only on its own objects. For objects that are outside of the aggregate, eventual consistency should suffice. Or, looking at it from a different angle, this means that when designing an aggregate, data consistency is a guiding principle. The data that needs to be strongly consistent in order to fulfill the business requirements, should be included in the aggregate. The data that can be eventually consistent and still meet the requirements, probably belongs in a different aggregate.&lt;/p&gt;
&lt;p&gt;At first glance, all these rules for aggregates may seem overly limiting. But the main idea is to keep their scope as constrained as possible, to prevent them from growing too much and taking on too many responsibilities. We should strive to keep aggregates small, highly cohesive, and decoupled from other system components. That unlocks the ability for them to be reorganized and reused in many ways. This helps avoid code duplication when fulfilling the requirements of today while also reducing the cost of evolving to meet the requirements of tomorrow.&lt;/p&gt;
&lt;h4 id=&#34;domain-event&#34;&gt;Domain event&lt;/h4&gt;
&lt;p&gt;Through their commands, the outside world can send messages to aggregates. &lt;strong&gt;Domain events&lt;/strong&gt; are the mechanism through which aggregates can themselves send messages to the outside world. As their name suggests, domain events are messages that describe important events that have happened in the business domain, related to an aggregate. Think &amp;ldquo;order placed&amp;rdquo;, &amp;ldquo;user registered&amp;rdquo; or &amp;ldquo;product out of stock&amp;rdquo;. The events should provide all necessary data that allows consumers to understand what has happened.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// This JSON data describes the event of an item being added to a shopping cart.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// It includes all the details that subscribers need to know about the event.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;quote-id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;f3774200-9e57-4ad2-9d93-ffb9e92b8364&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;event-id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;123&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;event-type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;item-added-to-quote&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;event-time&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1628970815&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;product-id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;7e7aee52-9aa1-4e0e-810e-666cedab5a7a&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;quantity&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Quote&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    IList&amp;lt;DomainEvent&amp;gt; _domainEvents = [];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; AddItem(ProductId productId, &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; quantity)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// This command creates the event and adds it to the aggregate&amp;#39;s list.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _domainEvents.Add(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; ItemAddedToQuote(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            quoteId: _id,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            productId: productId,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            quantity: quantity
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ));
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Domain events are also part of an aggregate&amp;rsquo;s public interface. Just like its commands. Other parts of the system can subscribe to these events, and when they happen, react accordingly. We will learn more about domain events, and see how to push them to subscribers, later in the series.&lt;/p&gt;
&lt;h4 id=&#34;domain-service&#34;&gt;Domain service&lt;/h4&gt;
&lt;p&gt;Sometimes there&amp;rsquo;s business logic that doesn&amp;rsquo;t belong to a particular aggregate or value object, or that involves multiple aggregates. &lt;strong&gt;Domain services&lt;/strong&gt; can be implemented in these cases. Domain services are simple stateless objects that implement some business logic. Somewhat like an aggregate&amp;rsquo;s command, but defined outside of an aggregate.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// Domain services are a good solution for implementing logic that orchestrates&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// calls to different system components.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;PlaceOrder&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; Result Run(QuoteId quoteId)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;try&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; quote = _quoteRepository.FindById(quoteId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; (quote == &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;null&lt;/span&gt;) &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; Result.Error(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Quote not found.&amp;#34;&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            quote.Close();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            _quoteRepository.Save(quote);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; order = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; Order(quote);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            order.PlaceOrder();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            _orderRepository.Save(order);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#888&#34;&gt;// We can imagine the above code producing an &amp;#34;order placed&amp;#34; event,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#888&#34;&gt;// and a separate payment processing component picking it up and&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#888&#34;&gt;// getting to work. Then, inventory and shipping components could&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#888&#34;&gt;// continue processing the order after the payment is successful.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; Result.Success();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;catch&lt;/span&gt; (ConcurrencyException ex)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; Result.Error(ex);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Of course, the rule of &amp;ldquo;modify only one aggregate per transaction&amp;rdquo; still applies. Even for domain services that orchestrate business operations that involve multiple aggregates. Remember, if strong consistency is needed across separate aggregates, and thus they have operations that need to be executed within the same transaction, then maybe these objects should be part of the same aggregate in the first place.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s also important to consider domain services as a sort of last resort. A final domain modeling tool to use only when the other tools like aggregates, value objects, commands and domain events, fall short and truly can&amp;rsquo;t meet the requirements on their own.&lt;/p&gt;
&lt;h4 id=&#34;data-access-concerns&#34;&gt;Data access concerns&lt;/h4&gt;
&lt;p&gt;As we&amp;rsquo;ve stated at the beginning, the domain model should be made of plain old objects. That means no dependency on framework components or particular infrastructure, just pure domain logic. Data access logic is one of those things that it should be oblivious about.&lt;/p&gt;
&lt;p&gt;In practical terms, your choice of technology stack and software development framework will usually dictate the mechanisms you use for interacting with persistent data storage. But that doesn&amp;rsquo;t matter to the domain model, with the correct abstraction, it should be compatible with any data access mechanism. Here are a few patterns worth mentioning:&lt;/p&gt;
&lt;p&gt;We&amp;rsquo;ve already seen &lt;a href=&#34;https://www.martinfowler.com/eaaCatalog/activeRecord.html&#34;&gt;active record&lt;/a&gt; being used as a data access strategy. Particularly useful when paired with an ORM framework. Due to its nature of tieing infrastructure concerns (i.e. data access logic) with the business logic, you have to jump through some hoops to make it work with a domain model. But it can be done. The &lt;a href=&#34;https://martinfowler.com/eaaCatalog/repository.html&#34;&gt;repository pattern&lt;/a&gt; is also a good fit for solving the data access problem in the context of DDD. It offers a clear, intention-revealing interface for data retrieval and modification. &lt;a href=&#34;https://martinfowler.com/eaaCatalog/unitOfWork.html&#34;&gt;Unit of work&lt;/a&gt; is also a pattern worth looking into, for coordinating numerous database operations.&lt;/p&gt;
&lt;p&gt;But the main takeaway is this: the domain model does not concern itself with data access, or frameworks, or infrastructure. So make sure to keep it plain and use abstractions to keep it unconcerned.&lt;/p&gt;
&lt;h3 id=&#34;section-7-modeling-the-dimension-of-time&#34;&gt;Section 7: Modeling the dimension of time&lt;/h3&gt;
&lt;p&gt;The &lt;strong&gt;event sourced domain model&lt;/strong&gt; is a further evolution of the domain model which incorporates the dimension of &lt;strong&gt;time&lt;/strong&gt;. By leveraging domain events as the source of truth for system data, it allows for a model that can provide deeper insight into the data, rich audit logging, and visibility into the state of the aggregates and entities at any previous point in their lifecycle.&lt;/p&gt;
&lt;h4 id=&#34;data-storage-and-retrieval&#34;&gt;Data storage and retrieval&lt;/h4&gt;
&lt;p&gt;The main characteristic that differentiates event sourcing from a traditional domain modeling is how the data that represents the aggregates is persisted. Instead of persisting the aggregate&amp;rsquo;s current state, event sourced domain models persist the domain events produced by the aggregates. These domain events are generated as a result of any operation that changes the state of the aggregate. Then, to obtain the current state of the aggregates, all the events are retrieved from storage and used to reconstruct the full object in memory.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/implementing-business-logic-ddd-part-2/event-sourced-aggregate.png&#34; alt=&#34;The event sourcing data flow&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;In an event sourced domain model, aggregates produce events and commit them to the event store. To instantiate aggregates then, the same events are fetched from the event store and used to rehydrate the in-memory objects.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;For example, in a database that backs an order processing system, an &lt;code&gt;orders&lt;/code&gt; table might look like this:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;id&lt;/th&gt;
          &lt;th&gt;status&lt;/th&gt;
          &lt;th&gt;email&lt;/th&gt;
          &lt;th&gt;phone&lt;/th&gt;
          &lt;th&gt;shipping_weight&lt;/th&gt;
          &lt;th&gt;country_code&lt;/th&gt;
          &lt;th&gt;created_at&lt;/th&gt;
          &lt;th&gt;updated_at&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;1&lt;/td&gt;
          &lt;td&gt;pending&lt;/td&gt;
          &lt;td&gt;john.doe@example.com&lt;/td&gt;
          &lt;td&gt;555-0101&lt;/td&gt;
          &lt;td&gt;2.5&lt;/td&gt;
          &lt;td&gt;US&lt;/td&gt;
          &lt;td&gt;2025-01-10&lt;/td&gt;
          &lt;td&gt;2025-01-10&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;2&lt;/td&gt;
          &lt;td&gt;paid&lt;/td&gt;
          &lt;td&gt;sarah.smith@example.com&lt;/td&gt;
          &lt;td&gt;555-0102&lt;/td&gt;
          &lt;td&gt;5.3&lt;/td&gt;
          &lt;td&gt;CA&lt;/td&gt;
          &lt;td&gt;2025-01-09&lt;/td&gt;
          &lt;td&gt;2025-01-11&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;3&lt;/td&gt;
          &lt;td&gt;shipped&lt;/td&gt;
          &lt;td&gt;mike.johnson@example.com&lt;/td&gt;
          &lt;td&gt;555-0103&lt;/td&gt;
          &lt;td&gt;1.8&lt;/td&gt;
          &lt;td&gt;GB&lt;/td&gt;
          &lt;td&gt;2025-01-08&lt;/td&gt;
          &lt;td&gt;2025-01-12&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;4&lt;/td&gt;
          &lt;td&gt;pending&lt;/td&gt;
          &lt;td&gt;emma.wilson@example.com&lt;/td&gt;
          &lt;td&gt;555-0104&lt;/td&gt;
          &lt;td&gt;3.2&lt;/td&gt;
          &lt;td&gt;AU&lt;/td&gt;
          &lt;td&gt;2025-01-11&lt;/td&gt;
          &lt;td&gt;2025-01-11&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;5&lt;/td&gt;
          &lt;td&gt;cancelled&lt;/td&gt;
          &lt;td&gt;david.brown@example.com&lt;/td&gt;
          &lt;td&gt;555-0105&lt;/td&gt;
          &lt;td&gt;4.7&lt;/td&gt;
          &lt;td&gt;US&lt;/td&gt;
          &lt;td&gt;2025-01-07&lt;/td&gt;
          &lt;td&gt;2025-01-13&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Here, each row represents an order and their current state. The database schema resembles the order domain entity.&lt;/p&gt;
&lt;p&gt;With event sourcing, what we persist into our database is a series of events that capture the full history of each order, as they journey through the system:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;order_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;123&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;event_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;order_created&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;timestamp&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;2025-01-10T10:00:00Z&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;data&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;email&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;john.doe@example.com&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;phone&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;555-0101&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;country_code&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;US&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;status&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;pending&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;order_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;123&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;event_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;payment_failed&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;timestamp&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;2025-01-11T12:00:00Z&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;data&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;status&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;payment_failed&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;reason&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;insufficient_funds&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;order_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;123&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;event_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;2&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;payment_details_updated&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;timestamp&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;2025-01-11T12:05:00Z&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;data&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;status&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;pending&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;payment_method_nonce&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;new-nonce-xyz&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;order_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;123&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;event_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;3&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;payment_succeeded&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;timestamp&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;2025-01-11T12:05:00Z&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;data&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;status&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;paid&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;order_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;123&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;event_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;4&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;inventory_stock_updated&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;timestamp&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;2025-01-11T15:00:00Z&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;data&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;status&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;processing&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;items&amp;#34;&lt;/span&gt;: [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            {&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;product_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;A1&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;quantity&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;2&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            {&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;product_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;B2&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;quantity&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;order_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;123&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;event_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;5&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;shipping_scheduled&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;timestamp&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;2025-01-12T09:00:00Z&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;data&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;status&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;awaiting_shipment&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;shipping_number&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;TRACK1234567890&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;order_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;123&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;event_id&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;6&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;event_type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;order_shipped&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;timestamp&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;2025-01-13T14:00:00Z&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;data&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;status&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;shipped&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We call this database, the &lt;strong&gt;event store&lt;/strong&gt;. This is an append-only storage mechanism that needs to support two features: fetching events that belong to a particular business entity and adding new ones.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// An event store only needs to support two features: fetching and appending.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;IEventStore&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    IEnumerable&amp;lt;DomainEvent&amp;gt; Fetch(Guid Id);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Append(Guid Id, IEnumerable&amp;lt;DomainEvent&amp;gt; events, &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; expectedVersion);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In code, when we want to retrieve an order from storage, what we do is fetch all its events, iterate over them and apply the changes they represent to an in-memory object, until it is fully reconstructed, or &amp;ldquo;rehydrated&amp;rdquo;. In the context of event sourcing, we call these representations of collections of events, &lt;strong&gt;projections&lt;/strong&gt;. In an event sourced domain model, aggregates leverage these projections to figure out their current state.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// This is a hypothetical order aggregate in an event sourced domain model.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// Its constructor expects a collection of domain events which it iterates over&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// to apply them to its internal state representaton object.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Order&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; List&amp;lt;DomainEvent&amp;gt; _events = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; OrderStateProjection _state = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; Order(IEnumerable&amp;lt;DomainEvent&amp;gt; events)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _state = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; OrderStateProjection();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;foreach&lt;/span&gt; (&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; e &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;in&lt;/span&gt; events)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            _events.Add(e);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            _state.Apply((&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;dynamic&lt;/span&gt;)e);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// Properties can be exposed to the outside world by leveraging the state&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// projection.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; OrderId Id =&amp;gt; _state.Id;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; Version =&amp;gt; _state.Version;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// This is a general use event sourced projection of an order, used to capture&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// the current state of an order entity or aggregate.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;OrderStateProjection&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// This projection is meant to expose the full state of the order aggregate&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// so it includes all the properties that belong to the order.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; OrderId Id { &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;get&lt;/span&gt;; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// With event sourcing, we need to keep track of the version of the entity.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// Every event that happens increments the version number.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; Version { &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;get&lt;/span&gt;; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; OrderStatus Status { &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;get&lt;/span&gt;; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; PersonName Name { &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;get&lt;/span&gt;; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; EmailAddress Email { &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;get&lt;/span&gt;; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; PhoneNumber Phone { &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;get&lt;/span&gt;; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; Weight ShippingWeight { &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;get&lt;/span&gt;; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; CountryCode Country { &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;get&lt;/span&gt;; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;string&lt;/span&gt; PaymentMethodNonce { &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;get&lt;/span&gt;; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;bool&lt;/span&gt; InventoryResolved { &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;get&lt;/span&gt;; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;string&lt;/span&gt; TrackingNumber { &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;get&lt;/span&gt;; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// It defines a series of &amp;#34;Apply&amp;#34; method overloads, one for each supported&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// domain event. The Order aggregate from above passes all the domain events&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// to the various overloads, calling them one by one, in order, until the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// full state is rehydrated into the projection. Each &amp;#34;Apply&amp;#34; overload takes&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// the data captured in its event and assigns it to the correct property in&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// the projection.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Apply(OrderCreatedEvent e)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Id = e.Id;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Status = e.Status;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Name = e.Name;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Email = e.Email;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Phone = e.Phone;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        ShippingWeight = e.ShippingWeight;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Country = e.Country;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Version = &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Apply(PaymentFailedEvent e)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Status = OrderStatus.PaymentFailed;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Version++;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Apply(PaymentDetailsUpdatedEvent e)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Status = OrderStatus.Pending;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        PaymentMethodNonce = e.PaymentMethodNonce;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Version++;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Apply(PaymentSucceededEvent e)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Status = OrderStatus.Processing;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Version++;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Apply(InventoryStockUpdatedEvent e)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Status = OrderStatus.Processing;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        InventoryResolved = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;true&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Version++;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Apply(ShippingScheduledEvent e)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Status = OrderStatus.AwaitingShipment;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        TrackingNumber = e.TrackingNumber;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Version++;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Apply(OrderShippedEvent e)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Status = OrderStatus.Shipped;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Version++;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Notice the &lt;code&gt;Version&lt;/code&gt; field in the projection, which indicates the number of changes that the entity has gone through.&lt;/p&gt;
&lt;p&gt;On the other hand, when it comes to appending new events for an aggregate, the event store needs to be aware of potential concurrency problems and handle them. After all, the ordering of the events matter. That&amp;rsquo;s why the event store usually implements &lt;strong&gt;optimistic concurrency control&lt;/strong&gt;, leveraging the version field we touched on earlier. Essentially, when trying to append new events, the version being worked with is specified. If it doesn&amp;rsquo;t match the current version of the aggregate in the event store (maybe because some other process appended a new event), then the operation has to fail, or otherwise adapt to make sure the data remains consistent.&lt;/p&gt;
&lt;h4 id=&#34;advantages-and-disadvantages&#34;&gt;Advantages and disadvantages&lt;/h4&gt;
&lt;p&gt;One advantage of this design is that we can produce many distinct projections of the same underlying event sourced data. In the previous example, we saw a projection that represents the full current state of the order. But we might need other projections which are optimized for different use cases.&lt;/p&gt;
&lt;p&gt;For example here&amp;rsquo;s a projection that captures all the statuses that an order went through, which might help different business analysis use cases:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// This projection only cares about the changes in status of the order, so&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// instead of attempting to capture its full state, in keeps track of all the&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// statuses in a collection.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;OrderStatusHistoryProjection&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; OrderId Id { &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;get&lt;/span&gt;; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;int&lt;/span&gt; Version { &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;get&lt;/span&gt;; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;set&lt;/span&gt;; }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; List&amp;lt;OrderStatus&amp;gt; Statuses = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt;();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// All the &amp;#34;Apply&amp;#34; overloads capture the status change that each event&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// caused.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Apply(OrderCreatedEvent e)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Statuses.Add(OrderStatus.Pending);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Version = &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Apply(PaymentFailedEvent e)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Statuses.Add(OrderStatus.PaymentFailed);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Version++;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Apply(PaymentDetailsUpdatedEvent e)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Statuses.Add(OrderStatus.Pending);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Version++;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Apply(PaymentSucceededEvent e)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Statuses.Add(OrderStatus.Processing);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Version++;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Apply(InventoryStockUpdatedEvent e)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Statuses.Add(OrderStatus.Processing);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Version++;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Apply(ShippingScheduledEvent e)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Statuses.Add(OrderStatus.AwaitingShipment);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Version++;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; Apply(OrderShippedEvent e)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Statuses.Add(OrderStatus.Shipped);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        Version++;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We can even store these projections into separate databases if we need to. We&amp;rsquo;ll learn more about that aspect later when we discuss CQRS.&lt;/p&gt;
&lt;p&gt;Another feature that we can implement easily thanks to event sourcing is time travel. We can easily apply the stored events up to a specific point in time or up to a specific version number and obtain the state of the entity as it was at that time. This can, for example, help troubleshooting, and unlock more avenues for business analysis.&lt;/p&gt;
&lt;p&gt;An obvious disadvantage of event sourcing is the complexity that it introduces. This complexity, compounded by the potential learning curve on teams that aren&amp;rsquo;t used to this kind of design, can be very detrimental when utilized in projects that don&amp;rsquo;t need it. As usual, follow DDD&amp;rsquo;s core principle of tying the system&amp;rsquo;s design to the domain&amp;rsquo;s needs, make sure to use the right tool for the job, and only deploy an event sourced domain model when the situation really calls for it. That is, whenever requirements dictate that the features enabled by such a design are necessary.&lt;/p&gt;
&lt;h4 id=&#34;the-event-sourced-domain-model&#34;&gt;The event sourced domain model&lt;/h4&gt;
&lt;p&gt;So, in summary, an event sourced domain model is a domain model that uses the event sourcing pattern to represent and operate on its aggregates. In a traditional, non-event-sourced domain model, the current state of the aggregates is persisted, commands modify this state and domain events are emitted for certain important operations, when needed. In the event sourced variant, domain events are used much more frequently, as they are the only source of truth. Everything and anything that changes the state of the aggregates produces an event. No changes are done directly, only through events. This is necessary because it is the events that are persisted, and it is from these events that the aggregates&amp;rsquo; current state is derived.&lt;/p&gt;
&lt;p&gt;In general, operations involving event sourced aggregates go through the following steps:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Load the aggregate&amp;rsquo;s domain events from the event store.&lt;/li&gt;
&lt;li&gt;Reconstruct the aggregate&amp;rsquo;s state using these events. You can use the particular projection needed for the task at hand.&lt;/li&gt;
&lt;li&gt;Run the necessary aggregate commands. Which in turn produce new domain events.&lt;/li&gt;
&lt;li&gt;Append the new events into the event store. Making sure to handle any concurrency errors.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-csharp&#34; data-lang=&#34;csharp&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;RescheduleOrderForPayment&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;private&lt;/span&gt; IOrderRepository _orderRepository;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// This is the general pattern that application services often follow when&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// interacted with event sourced domain model aggregates: load events,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// construct the aggregate with the events, run commands and save the newly&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// created events.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; Result Run(OrderId orderId, &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;string&lt;/span&gt; paymentMethodNonce)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;try&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; events = _orderRepository.LoadEvents(orderId);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; order = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; Order(events);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; originalVersion = order.Version;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#888&#34;&gt;// See below for what commands like these generally look like.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            order.UpdatePaymentDetails(paymentMethodNonce);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            order.ScheduleForPaymentProcessing();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            _orderRepository.Save(order, expectedVersion: originalVersion);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; Result.Success();
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// The repository uses the given version parameter to implement&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;// optimistic concurrency control.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;catch&lt;/span&gt; (ConcurrencyException ex)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; Result.Error(ex.Message);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Order&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// Commands of event sourced aggregates don&amp;#39;t modify state directly.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// Instead, they create the appropriate events and append them.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;void&lt;/span&gt; UpdatePaymentDetails(&lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;string&lt;/span&gt; paymentMethodNonce)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888;font-weight:bold&#34;&gt;var&lt;/span&gt; e = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;new&lt;/span&gt; PaymentDetailsUpdatedEvent(_state.Id, paymentMethodNonce);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _events.Add(e);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        _state.Apply(e);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id=&#34;ddd-tools-for-implementing-business-logic&#34;&gt;DDD tools for implementing business logic&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/implementing-business-logic-ddd-part-2/concept-map.png&#34; alt=&#34;Concept map of the DDD patterns for implementing domain logic&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;These are the main concepts that we&amp;rsquo;ve explored, and how they relate to each other.&lt;/em&gt;&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Observing End Point Dev&#39;s Approach to AI</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2026/04/observing-end-point-dev-approach-to-ai/"/>
      <id>https://www.endpointdev.com/blog/2026/04/observing-end-point-dev-approach-to-ai/</id>
      <published>2026-04-16T00:00:00+00:00</published>
      <author>
        <name>Jesse Gardner</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2026/04/observing-end-point-dev-approach-to-ai/cover.webp&#34; alt=&#34;A sweeping desert canyon stretches, framed by towering red sandstone formations and sparse desert scrub under a cloudless blue sky.&#34;&gt;&lt;br&gt;
Photo by Garrett Skinner, 2022.&lt;/p&gt;
&lt;p&gt;When I joined End Point Dev in October of 2025, there was one clear directive among a wide-ranging set of responsibilities: AI is causing drastic changes in our industry, and we need to tackle it head-on.&lt;/p&gt;
&lt;p&gt;As a non-engineer working in a software development consultancy, there is a double edged sword in focusing my work on AI. The downside, of course, is my lack of expertise in anything involving code. I’ve sold software for a majority of my career, but I haven’t been the one building it. A reasonable person could wonder: how would I have a useful perspective on the developments around AI?&lt;/p&gt;
&lt;p&gt;To that there are a few answers. For starters, I don’t get bogged down in the “how” AI is working as much as I am interested in the results of its work. I’ve certainly learned context windows, token usage, rate limits, and other immediately useful information around utilizing AI.&lt;/p&gt;
&lt;p&gt;I also get to be a guinea pig for End Point’s suite of AI services. Our team might be primarily technical users, but that does not mean our clients necessarily have that same background. I get to approach AI tools as an interested user, not a development wizard.&lt;/p&gt;
&lt;p&gt;With that said, these past six months have been an incredible learning experience within the space. I’ve experienced how AI can help with projects that would otherwise take ten times the amount of work, seen how development times can be improved with properly utilized generative AI, and observed healthy debate internally about where AI ought to be deployed.&lt;/p&gt;
&lt;h3 id=&#34;time-sheet-analysis-and-ai-analysis&#34;&gt;Time Sheet Analysis and AI Analysis&lt;/h3&gt;
&lt;p&gt;One of the most impressive aspects of the End Point culture is an emphasis on dutiful and detailed timesheet management. As I first stepped into the role, I gained access to the historical timesheet entries across the entire organization, which was a little overwhelming at first, if I’m being completely honest! For billable and non-billable work alike, whether from directors or part-time developers, there was a detailed record of work in 15-minute increments.&lt;/p&gt;
&lt;p&gt;The application itself was novel—a lightweight yet powerful tool that organized this work by type and client and allowed multiple types of reporting for someone like me to get a quick lay of the land. Within a few hours of my first day on the job, I was able to get an overview of our top clients, what work was done with them, and which developers were involved.&lt;/p&gt;
&lt;p&gt;This was directionally useful, but it was also a bit of context overload—a good problem to have.&lt;/p&gt;
&lt;p&gt;So, I decided to keep my findings high-level. I got my overview and moved on, using these reports to pick a lane in the company and start swimming (Visionport). At the risk of mixing metaphors, I didn’t want to boil the ocean. A few months later, though, the allure of that timesheet data reentered my mind. There was so much valuable data there—beyond what I had looked at from that bird’s-eye view. AI is a great way to aggregate data, so I wondered: What else could be explored with the use of AI?&lt;/p&gt;
&lt;p&gt;With a little help from my OpenClaw instance, JJ, a plan was devised. I downloaded a year’s worth of timesheet data for virtually every developer on our team, down to the nitty-gritty of each daily entry. I then fed this into my OpenAI pro account and gave it a fairly detailed prompt, asking it to locate patterns of work and opportunities to expand within our current clients. For good measure, I also asked it to organize the data to my specifications, creating a spreadsheet with the tech stack, percentage of hours worked by client for each employee, and other quality-of-life tracking.&lt;/p&gt;
&lt;p&gt;As we all know, AI sometimes aims to please a bit too much, so before I acted on any of this data, I took it to the team. While there were certainly some clean upsell opportunities given by ChatGPT, it became clear that the real value was a common understanding for each of our clients between myself and the client rep—a conversation starter, if you will. The project led to an expedited process for me to know our clients, our common workloads, and a better relationship between me and my fellow End Pointers. A solid case for AI enabling real, human work.&lt;/p&gt;
&lt;h3 id=&#34;traditional-software-engineering-and-spec-driven-development&#34;&gt;Traditional Software Engineering and Spec-Driven Development&lt;/h3&gt;
&lt;p&gt;There has been a healthy debate that I’ve witnessed from both my timesheet analysis and internal conversations here at End Point, and that has been the function of AI in a team of expert coders.&lt;/p&gt;
&lt;p&gt;It’s no secret that AI has made tremendous progress in its ability to write large quantities of code, especially with the recent leaps in aptitude from the likes of GPT-5.3-Codex and Opus 4.6. Those most bullish on AI might claim that software engineers will be the first industry gobbled up by the ever-looming march of our sycophantic AI overlords. A step below that is an &lt;a href=&#34;https://www.tomshardware.com/tech-industry/artificial-intelligence/jensen-huang-says-nvidia-engineers-should-use-ai-tokens-worth-half-their-annual-salary-every-year-to-be-fully-productive-compares-not-using-ai-to-using-paper-and-pencil-for-designing-chips&#34;&gt;emphatic push from Nvidia’s CEO&lt;/a&gt; for their head developers to spend around two hundred and fifty thousand dollars on tokens, lest there be a problem with their productivity.&lt;/p&gt;
&lt;p&gt;On the other side of the spectrum, there are those who believe that, while AI can sure as heck write code quickly, it can also cause more harm than good. As much as &amp;ldquo;vibe coders&amp;rdquo; have taken over, the myriad bugs and suboptimizations in their projects have given birth to a novel job title: the &lt;a href=&#34;https://www.indeed.com/career-advice/news/vibe-code-cleanup-specialist&#34;&gt;&amp;ldquo;Vibe Code Cleanup Specialist.&amp;rdquo;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;With these bullish opinions seemingly everywhere, I had to search for a counterargument within End Point itself. As the resident AI cynic at End Point, Senior Developer Gered King takes a very wary stance towards the involvement of AI in software development:&lt;/p&gt;
&lt;p&gt;&amp;ldquo;While vibe-coding can certainly feel like magic when you first give it a try, as the saying goes, &amp;rsquo;there is no such thing as a free lunch.&amp;rsquo; Under a very strict definition of &amp;lsquo;vibe-coding&amp;rsquo; as it was originally coined by Andrej Karpathy, the vibe-coder is not even looking at the code at all. I think most today would agree without too much arm-twisting that this is likely a bad idea for anything that you eventually want to release into production. Languages like English can be incredibly ambiguous, and combined with the fact that LLMs are not deterministic, you could be really taking quite the gamble that you are crafting sufficiently detailed and unambiguous prompts to get an LLM to spit out quality results without looking at the code to see for yourself.&lt;/p&gt;
&lt;p&gt;Even if we decide to take a less strict approach and agree to actually look at the code but still predominantly use AI assistance to write the majority of it for us, this can still lead to problems down the road. &lt;a href=&#34;https://www.anthropic.com/research/AI-assistance-coding-skills&#34;&gt;Anthropic&amp;rsquo;s own research is clear&lt;/a&gt; that offloading parts of your coding to an AI leads to a statistically significant decrease in skills development and understanding. Struggling with problems is a big part of how we learn. If you constantly take the easy path or are otherwise taking shortcuts, you probably aren&amp;rsquo;t learning a lot along the way!&lt;/p&gt;
&lt;p&gt;Certainly, some of this AI-generated code is going to be of dubious quality, which further impairs our understanding of it. &lt;a href=&#34;https://www.linkedin.com/pulse/what-28-million-workflows-reveal-ai-codings-biggest-risk-circleci-j9syc/&#34;&gt;CircleCI recently did an analysis&lt;/a&gt; and found that their data across 28 million workflows from customers using their CI tools showed that in the last couple of years, where AI tools are seeing more and more adoption, only 5% of teams are measurably improving their output. The other 95% are barely seeing any difference at all, and in many cases, teams have even gotten slightly worse at shipping new software releases.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;What Gered’s viewpoint represents, beyond the skepticism towards an industry enthralled with powerful tools, is a stalwart protection of human knowledge. I interpret his words as noting that coding is an art as much as a science, and that offloading too much of our problem-solving to AI comes with the risk of losing the ability to develop creative or optimal solutions to the sticky problems within software development.&lt;/p&gt;
&lt;p&gt;As our company pushes toward a future where AI is not only prevalent but ever-improving, this skepticism is welcome and, in my humble opinion, critical.&lt;/p&gt;
&lt;h3 id=&#34;spec-driven-development-with-ai&#34;&gt;Spec-Driven Development with AI&lt;/h3&gt;
&lt;p&gt;While it’s foolish to proclaim that any sort of best practice around AI truly exists, a few truisms have emerged since the launch of ChatGPT in 2023. Chief among them is that AI produces dramatically better results when given clear, detailed specifications, and that unstructured &amp;ldquo;vibe coding&amp;rdquo; tends to produce unstructured results.&lt;/p&gt;
&lt;p&gt;End Point has leaned into this reality. Over the past several months, our team has developed and begun using a framework for AI-assisted, spec-driven development, and the results have been nothing short of a revelation. Rather than treating AI as a replacement for engineering discipline, the approach doubles down on it: detailed project specifications, strict security standards baked into the framework’s core governance, rigorous testing requirements, and structured issue-driven execution with AI agents working within those guardrails rather than outside them.&lt;/p&gt;
&lt;p&gt;A tremendous bonus we’ve found is the quality of documentation that AI agents produce alongside the code itself. When working within well-defined specifications, the agents generate thorough, consistent documentation as a natural byproduct—something that has historically been one of the easier things to let slip in traditional development.&lt;/p&gt;
&lt;p&gt;CEO Ben Goldstein has been hands-on in developing and refining the framework in consultation with End Point team members and multiple AI systems, and End Point developers are actively using it on real projects. The shared experience has reinforced a central insight: the better the spec, the better the output.&lt;/p&gt;
&lt;h4 id=&#34;final-thoughts&#34;&gt;Final Thoughts&lt;/h4&gt;
&lt;p&gt;Depending on where you look, generative AI is being touted as either the most important technological development in our lifetimes or a bubble that’s just waiting to burst. The beauty of working with the brilliant developers at End Point is that, regardless of how things shake out, preparations are being made.&lt;/p&gt;
&lt;p&gt;Buried in the hype of AI’s progress is a question begging to be answered: Does &lt;em&gt;every&lt;/em&gt; solution need to utilize AI? One of our leaders in AI, Edgar Mlowe, recently wrote about a &lt;a href=&#34;/blog/2026/03/why-i-am-focusing-on-intelligent-document-processing/&#34;&gt;niche use case for LLMs&lt;/a&gt;, drilling into a specific need with a specific solution. When a clear solution isn’t present, though, can it be helped to avoid bringing in LLMs for a solution that simple automation is more appropriate to solve? In other words, is our industry becoming too AI-reliant, or is the emphasis on bringing the &amp;ldquo;Elephant Gun&amp;rdquo; drawn from a need to better familiarize us with working alongside LLMs?&lt;/p&gt;
&lt;p&gt;This is why a healthy balance must be struck in how these models are leveraged, and why I find it so valuable and important that End Point is home to a range of opinions on AI—from the overt enthusiast to the hardened critic. My role requires a vantage point of all sides and helping craft solutions that leverage the full power of AI, without sacrificing that which makes End Point a special place: the human touch.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Getting the Most from your Claude Subscription</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2026/04/getting-the-most-from-claude-subscription/"/>
      <id>https://www.endpointdev.com/blog/2026/04/getting-the-most-from-claude-subscription/</id>
      <published>2026-04-14T00:00:00+00:00</published>
      <author>
        <name>Dan Briones</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2026/04/getting-the-most-from-claude-subscription/thistles-river.webp&#34; alt=&#34;Tall dry grasses and thistles sway in the foreground, with a calm blue river inlet and green hills stretching out under a partly cloudy sky.&#34;&gt;&lt;br&gt;
Photo by Josh Ausborne, 2006.&lt;/p&gt;
&lt;p&gt;Every prompt you send to Claude Code costs tokens. If you understand where those tokens go and how to control them, you can stretch your subscription dramatically further. This guide covers the practical steps I have taken to keep my usage lean without sacrificing capability.&lt;/p&gt;
&lt;h3 id=&#34;what-are-tokens&#34;&gt;What are Tokens?&lt;/h3&gt;
&lt;p&gt;A &lt;em&gt;token&lt;/em&gt; is the smallest unit of text Claude processes. A token is roughly three-quarters of a word. The sentence &amp;ldquo;Hello, how are you today?&amp;rdquo; is about seven tokens. Every interaction, yours and Claude&amp;rsquo;s, is measured in tokens drawn from a fixed &lt;strong&gt;context window&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Think of the context window as a whiteboard. Everything Claude needs to know must fit on it: your instructions, the conversation so far, any files it reads, and its own responses. When the whiteboard fills up, older content must be erased to make room.&lt;/p&gt;
&lt;h3 id=&#34;what-loads-with-every-prompt&#34;&gt;What Loads with every Prompt&lt;/h3&gt;
&lt;p&gt;Most people assume they are only paying for the text they type. In reality, Claude loads a stack of context before it even reads your message.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/getting-the-most-from-claude-subscription/loads-with-every-prompt.webp&#34; alt=&#34;What loads with every Prompt&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Breakdown of what gets loaded:&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Source&lt;/th&gt;
          &lt;th&gt;Tokens&lt;/th&gt;
          &lt;th&gt;When&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;System prompt&lt;/td&gt;
          &lt;td&gt;~4,200&lt;/td&gt;
          &lt;td&gt;Every message&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Environment info&lt;/td&gt;
          &lt;td&gt;~280&lt;/td&gt;
          &lt;td&gt;Every message&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;CLAUDE.md hierarchy&lt;/td&gt;
          &lt;td&gt;Varies&lt;/td&gt;
          &lt;td&gt;Session start&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;MEMORY.md index&lt;/td&gt;
          &lt;td&gt;~200–680&lt;/td&gt;
          &lt;td&gt;Session start&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Conversation history&lt;/td&gt;
          &lt;td&gt;Grows&lt;/td&gt;
          &lt;td&gt;Every message&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;File reads and tool output&lt;/td&gt;
          &lt;td&gt;~2,500 per file&lt;/td&gt;
          &lt;td&gt;On demand&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The conversation history is the big one. It compounds with every exchange. A ten-turn conversation where Claude reads a few files each turn can consume most of the context window before you realise it.&lt;/p&gt;
&lt;h4 id=&#34;the-claudemd-hierarchy&#34;&gt;The CLAUDE.md Hierarchy&lt;/h4&gt;
&lt;p&gt;CLAUDE.md files are persistent instructions that load at session start. They exist at multiple levels and all of them stack:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/getting-the-most-from-claude-subscription/claude-hierarchy.webp&#34; alt=&#34;The CLAUDE.md Hierarchy&#34;&gt;&lt;/p&gt;
&lt;p&gt;Each file you add here is loaded in full every session. Keep them concise. Aim for under 200 lines per file. Long CLAUDE.md files reduce instruction adherence and burn tokens on every single message.&lt;/p&gt;
&lt;h4 id=&#34;memory&#34;&gt;Memory&lt;/h4&gt;
&lt;p&gt;Claude Code maintains an auto-memory system at &lt;code&gt;~/.claude/projects/&amp;lt;project&amp;gt;/memory/&lt;/code&gt;. The &lt;code&gt;MEMORY.md&lt;/code&gt; index file (first 200 lines) loads every session. Individual topic files only load on demand when Claude decides they are relevant.&lt;/p&gt;
&lt;p&gt;This is generally lightweight, but if your MEMORY.md grows large, it silently eats into your budget on every prompt.&lt;/p&gt;
&lt;h3 id=&#34;the-three-models-and-how-they-use-tokens&#34;&gt;The Three Models and How They Use Tokens&lt;/h3&gt;
&lt;p&gt;Claude Code gives you access to three models, each with different token economics. The model you use makes a significant difference in how fast you burn through your allocation.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Model&lt;/th&gt;
          &lt;th&gt;Strength&lt;/th&gt;
          &lt;th&gt;Token Cost&lt;/th&gt;
          &lt;th&gt;Best For&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Haiku&lt;/td&gt;
          &lt;td&gt;Fast, cheap&lt;/td&gt;
          &lt;td&gt;Lowest&lt;/td&gt;
          &lt;td&gt;Simple tasks, quick lookups&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Sonnet&lt;/td&gt;
          &lt;td&gt;Balanced&lt;/td&gt;
          &lt;td&gt;Medium&lt;/td&gt;
          &lt;td&gt;Most coding work&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Opus&lt;/td&gt;
          &lt;td&gt;Deep reasoning&lt;/td&gt;
          &lt;td&gt;Highest&lt;/td&gt;
          &lt;td&gt;Complex architecture, debugging&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h4 id=&#34;how-your-prompt-selects-the-model&#34;&gt;How Your Prompt Selects the Model&lt;/h4&gt;
&lt;p&gt;Your subscription plan and settings determine the default model. You can override it several ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;During a session:&lt;/strong&gt; &lt;code&gt;/model sonnet&lt;/code&gt; or &lt;code&gt;/model opus&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;At startup:&lt;/strong&gt; &lt;code&gt;claude --model haiku&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Permanently:&lt;/strong&gt; Add &lt;code&gt;&amp;quot;model&amp;quot;: &amp;quot;sonnet&amp;quot;&lt;/code&gt; to your settings file&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The key insight is that &lt;strong&gt;you should match the model to the task&lt;/strong&gt;. Do not use Opus to rename a variable. Do not use Haiku to design a database schema. The difference in token consumption between Opus and Haiku for the same task can be several times over.&lt;/p&gt;
&lt;h4 id=&#34;effort-levels&#34;&gt;Effort Levels&lt;/h4&gt;
&lt;p&gt;Each model also has an effort setting that controls how much &amp;ldquo;thinking&amp;rdquo; it does:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Level&lt;/th&gt;
          &lt;th&gt;Behaviour&lt;/th&gt;
          &lt;th&gt;Use When&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;low&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Minimal reasoning&lt;/td&gt;
          &lt;td&gt;Straightforward, mechanical tasks&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;medium&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Balanced (default)&lt;/td&gt;
          &lt;td&gt;General development&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;high&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Deep analysis&lt;/td&gt;
          &lt;td&gt;Tricky bugs, architecture decisions&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;max&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;No token limit on thinking&lt;/td&gt;
          &lt;td&gt;Last resort, Opus only&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Change it with &lt;code&gt;/effort low&lt;/code&gt; before a simple task, then &lt;code&gt;/effort high&lt;/code&gt; when you need deep analysis. This alone can save significant tokens.&lt;/p&gt;
&lt;h3 id=&#34;monitoring-your-token-usage&#34;&gt;Monitoring your Token Usage&lt;/h3&gt;
&lt;p&gt;You cannot manage what you cannot see. Setting up a statusline is the single most impactful thing I did for token awareness.&lt;/p&gt;
&lt;h4 id=&#34;setting-up-the-statusline&#34;&gt;Setting up the Statusline&lt;/h4&gt;
&lt;p&gt;The statusline sits at the bottom of your Claude Code terminal and shows real-time token consumption. Configure it in &lt;code&gt;~/.claude/settings.json&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;statusLine&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;type&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;command&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;command&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;bash -c &amp;#39;data=$(cat); m=$(echo \&amp;#34;$data\&amp;#34; | jq -r \&amp;#34;.model.display_name // \\\&amp;#34;Unknown\\\&amp;#34;\&amp;#34;); p=$(echo \&amp;#34;$data\&amp;#34; | jq -r \&amp;#34;.context_window.used_percentage // 0\&amp;#34;); ti=$(echo \&amp;#34;$data\&amp;#34; | jq -r \&amp;#34;.context_window.total_input_tokens // 0\&amp;#34;); to=$(echo \&amp;#34;$data\&amp;#34; | jq -r \&amp;#34;.context_window.total_output_tokens // 0\&amp;#34;); tt=$((ti+to)); f=$(awk \&amp;#34;BEGIN{printf \\\&amp;#34;%d\\\&amp;#34;,int($p/10+0.5)}\&amp;#34;); pi=$(awk \&amp;#34;BEGIN{printf \\\&amp;#34;%d\\\&amp;#34;,$p+0.5}\&amp;#34;); bar=\&amp;#34;\&amp;#34;; for((i=0;i&amp;lt;f;i++)); do bar+=\&amp;#34;\\xe2\\x96\\x88\&amp;#34;; done; for((i=f;i&amp;lt;10;i++)); do bar+=\&amp;#34;\\xe2\\x96\\x91\&amp;#34;; done; if((pi&amp;lt;50)); then pc=\&amp;#34;\\033[32m\&amp;#34;; elif((pi&amp;lt;80)); then pc=\&amp;#34;\\033[33m\&amp;#34;; else pc=\&amp;#34;\\033[31m\&amp;#34;; fi; if((tt&amp;gt;=1000000)); then td=$(awk \&amp;#34;BEGIN{printf \\\&amp;#34;%.1fM\\\&amp;#34;,$tt/1000000}\&amp;#34;); elif((tt&amp;gt;=1000)); then td=$(awk \&amp;#34;BEGIN{printf \\\&amp;#34;%.1fk\\\&amp;#34;,$tt/1000}\&amp;#34;); else td=\&amp;#34;$tt\&amp;#34;; fi; printf \&amp;#34;\\033[1;36m%s\\033[0m  [${pc}%b\\033[90m%b\\033[0m] ${pc}%s%%\\033[0m  \\033[37m%s tok\\033[0m\\n\&amp;#34; \&amp;#34;$m\&amp;#34; \&amp;#34;$(printf \&amp;#34;%b\&amp;#34; \&amp;#34;$(for((i=0;i&amp;lt;f;i++)); do printf \&amp;#34;\\\\xe2\\\\x96\\\\x88\&amp;#34;; done)\&amp;#34;)\&amp;#34; \&amp;#34;$(printf \&amp;#34;%b\&amp;#34; \&amp;#34;$(for((i=f;i&amp;lt;10;i++)); do printf \&amp;#34;\\\\xe2\\\\x96\\\\x91\&amp;#34;; done)\&amp;#34;)\&amp;#34; \&amp;#34;$pi\&amp;#34; \&amp;#34;$td\&amp;#34;&amp;#39;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src=&#34;/blog/2026/04/getting-the-most-from-claude-subscription/claude-statusline.png&#34; alt=&#34;Claude Code statusline example&#34;&gt;&lt;/p&gt;
&lt;p&gt;This reads the session JSON from stdin via &lt;code&gt;jq&lt;/code&gt;, then renders:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Model name&lt;/strong&gt; in cyan&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;A 10-segment progress bar&lt;/strong&gt; using block characters, colour coded: green under 50%, yellow under 80%, red above 80%&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context percentage&lt;/strong&gt; as a number&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Total tokens&lt;/strong&gt; (input + output) in a compact format (e.g. &lt;code&gt;142.3k tok&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Drop this into &lt;code&gt;~/.claude/settings.json&lt;/code&gt; and it appears at the bottom of every Claude Code session. When the bar turns yellow, compact. When it turns red, compact immediately or &lt;code&gt;/clear&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&#34;other-monitoring-commands&#34;&gt;Other Monitoring Commands&lt;/h4&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Command&lt;/th&gt;
          &lt;th&gt;What It Shows&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;/context&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Visual grid of context usage with tips&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;/status&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Current model, account, version&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;/cost&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;Token usage stats (API users)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;compaction-your-most-important-habit&#34;&gt;Compaction: Your Most Important Habit&lt;/h3&gt;
&lt;p&gt;Compaction summarizes older conversation history to free up context space. Claude Code does this automatically when the context fills up, but &lt;strong&gt;waiting for autocompact is wasteful&lt;/strong&gt;.&lt;/p&gt;
&lt;h4 id=&#34;why-compact-at-60&#34;&gt;Why Compact at 60%&lt;/h4&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/getting-the-most-from-claude-subscription/claude-compacting.webp&#34; alt=&#34;Why Compact at 60%&#34;&gt;&lt;/p&gt;
&lt;p&gt;When you wait until the context is nearly full:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Autocompact fires but can only free a small amount&lt;/li&gt;
&lt;li&gt;The next file read or tool output fills it right back up&lt;/li&gt;
&lt;li&gt;Autocompact fires again. Then again. This is &lt;em&gt;thrashing&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Eventually Claude gives up and you lose your session&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;When you compact at 60%:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;There is enough history to summarize meaningfully&lt;/li&gt;
&lt;li&gt;The resulting summary is concise&lt;/li&gt;
&lt;li&gt;You get a large chunk of free space back&lt;/li&gt;
&lt;li&gt;You can continue working without interruption&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&#34;using-compact-effectively&#34;&gt;Using /compact Effectively&lt;/h4&gt;
&lt;p&gt;You can guide what gets preserved:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;/compact                          -- general compaction
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;/compact focus on the API changes -- preserve specific context
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;/compact keep only the plan       -- aggressive, targeted&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Always tell Claude what matters. A focused compaction produces a better summary and frees more space.&lt;/p&gt;
&lt;h3 id=&#34;time-of-day-and-rate-limits&#34;&gt;Time of Day and Rate Limits&lt;/h3&gt;
&lt;p&gt;Claude Code has rate limits measured in tokens per minute (TPM) and requests per minute (RPM). These limits are shared across your team.&lt;/p&gt;
&lt;p&gt;The practical effect: &lt;strong&gt;during peak hours when your entire team is active, you hit rate limits faster.&lt;/strong&gt; Early morning or late evening sessions often feel smoother because fewer concurrent users are competing for the same allocation.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Team Size&lt;/th&gt;
          &lt;th&gt;TPM per User&lt;/th&gt;
          &lt;th&gt;RPM per User&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;1–5&lt;/td&gt;
          &lt;td&gt;200k–300k&lt;/td&gt;
          &lt;td&gt;5–7&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;5–20&lt;/td&gt;
          &lt;td&gt;100k–150k&lt;/td&gt;
          &lt;td&gt;2.5–3.5&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;20–50&lt;/td&gt;
          &lt;td&gt;50k–75k&lt;/td&gt;
          &lt;td&gt;1.25–1.75&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Anthropic&amp;rsquo;s infrastructure serves a global user base, so peak load follows the sun. The busiest window is when North American and European working hours overlap. Here is a rough guide for three major time zones:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;City&lt;/th&gt;
          &lt;th&gt;Peak Hours (Local)&lt;/th&gt;
          &lt;th&gt;Low Traffic (Local)&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;London (GMT/​BST)&lt;/td&gt;
          &lt;td&gt;09:00 - 17:00&lt;/td&gt;
          &lt;td&gt;21:00 - 06:00&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;New York (EST/​EDT)&lt;/td&gt;
          &lt;td&gt;09:00 - 18:00&lt;/td&gt;
          &lt;td&gt;22:00 - 06:00&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Los Angeles (PST/​PDT)&lt;/td&gt;
          &lt;td&gt;08:00 - 17:00&lt;/td&gt;
          &lt;td&gt;21:00 - 06:00&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The global peak, when the most users are active simultaneously, is roughly &lt;strong&gt;13:00 - 17:00 UTC&lt;/strong&gt;. That window is mid-afternoon in London, late morning in New York, and early morning in Los Angeles. If you have flexibility in when you do heavy Claude work, shifting to early morning or evening local time can noticeably reduce rate limit friction.&lt;/p&gt;
&lt;p&gt;This means your workflow matters. If you are sending rapid-fire prompts during a team standup where everyone is also using Claude, you will hit limits. Batch your work thoughtfully.&lt;/p&gt;
&lt;h3 id=&#34;practical-habits-that-save-tokens&#34;&gt;Practical Habits That Save Tokens&lt;/h3&gt;
&lt;h4 id=&#34;keep-tasks-small-and-focused&#34;&gt;Keep Tasks Small and Focused&lt;/h4&gt;
&lt;p&gt;Vague prompts like &amp;ldquo;improve the codebase&amp;rdquo; force Claude to scan broadly, reading files it does not need. Specific prompts like &amp;ldquo;add input validation to the login endpoint in src/​auth.ts&amp;rdquo; let Claude work surgically.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/getting-the-most-from-claude-subscription/keep-tasks-focused.webp&#34; alt=&#34;Keep Tasks Small and Focused&#34;&gt;&lt;/p&gt;
&lt;h4 id=&#34;group-work-by-skill&#34;&gt;Group Work by Skill&lt;/h4&gt;
&lt;p&gt;If you need to write tests, write all the tests in one session. If you need to update API endpoints, do them together. Switching between unrelated tasks within a single session wastes context because Claude has to hold multiple unrelated threads in memory.&lt;/p&gt;
&lt;h4 id=&#34;use-clear-between-unrelated-tasks&#34;&gt;Use /clear Between Unrelated Tasks&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;/clear&lt;/code&gt; wipes the conversation history and frees all context. Before clearing, name your session with &lt;code&gt;/rename&lt;/code&gt; so you can find it later with &lt;code&gt;/resume&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A good workflow:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Start a focused task&lt;/li&gt;
&lt;li&gt;Work until done or until context hits 60%&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/compact&lt;/code&gt; if continuing, &lt;code&gt;/clear&lt;/code&gt; if switching topics&lt;/li&gt;
&lt;li&gt;&lt;code&gt;/rename &amp;quot;descriptive name&amp;quot;&lt;/code&gt; before clearing&lt;/li&gt;
&lt;/ol&gt;
&lt;h4 id=&#34;use-plan-mode-for-complex-work&#34;&gt;Use Plan Mode for Complex Work&lt;/h4&gt;
&lt;p&gt;Press &lt;code&gt;Shift+Tab&lt;/code&gt; twice to enter plan mode. Claude reads and proposes an approach before writing code. This prevents expensive rework when the initial direction is wrong. Review the plan, iterate in conversation, then execute.&lt;/p&gt;
&lt;h4 id=&#34;delegate-verbose-work-to-subagents&#34;&gt;Delegate Verbose Work to Subagents&lt;/h4&gt;
&lt;p&gt;Test runs, log parsing, and documentation fetching consume enormous context. Subagents run in their own context window and return only a summary. The verbose output stays out of your main conversation.&lt;/p&gt;
&lt;h3 id=&#34;understanding-your-subscription-quota&#34;&gt;Understanding Your Subscription Quota&lt;/h3&gt;
&lt;p&gt;Claude subscriptions use a &lt;strong&gt;rolling 5-hour window&lt;/strong&gt; for usage limits, plus a separate &lt;strong&gt;weekly quota&lt;/strong&gt;. Understanding these mechanics unlocks a simple but powerful trick.&lt;/p&gt;
&lt;h4 id=&#34;the-5-hour-rolling-window&#34;&gt;The 5-Hour Rolling Window&lt;/h4&gt;
&lt;p&gt;The timer does not reset at midnight. It starts when you send your first prompt and rolls forward from that moment. Messages you sent at 9 AM stop counting against your quota by 2 PM. After 5 hours, your allocation gradually replenishes.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/getting-the-most-from-claude-subscription/5-hour-rolling.webp&#34; alt=&#34;The 5-Hour Rolling Window&#34;&gt;&lt;/p&gt;
&lt;p&gt;This creates a practical opportunity: &lt;strong&gt;send a short prompt early to start the timer, even if you are not ready to work.&lt;/strong&gt; A quick &amp;ldquo;hello&amp;rdquo; or a &lt;code&gt;/status&lt;/code&gt; check is enough. The 5-hour clock begins ticking immediately. By the time you sit down for focused work 30 or 60 minutes later, you are already that much closer to your window expiring and resetting. Over a full workday this can mean the difference between one reset and two.&lt;/p&gt;
&lt;h4 id=&#34;the-weekly-quota&#34;&gt;The Weekly Quota&lt;/h4&gt;
&lt;p&gt;Separately from the 5-hour window, there is a 7-day rolling cap. If you exhaust the weekly quota, waiting 5 hours will not help. You must wait for the 7-day window to roll past your heaviest usage period. This is why daily token discipline matters. Burning through tokens recklessly on Monday can leave you throttled by Thursday.&lt;/p&gt;
&lt;h4 id=&#34;extra-usage-pay-as-you-go-overflow&#34;&gt;Extra Usage: Pay-as-You-Go Overflow&lt;/h4&gt;
&lt;p&gt;If you have a payment method on file, you can enable &lt;strong&gt;extra usage&lt;/strong&gt; so Claude Code continues working when you hit your plan limits instead of blocking you. Overflow tokens are billed at standard API rates.&lt;/p&gt;
&lt;p&gt;To enable it, go to &lt;strong&gt;Settings &amp;gt; Usage&lt;/strong&gt; on claude.ai and click &lt;strong&gt;Adjust limit&lt;/strong&gt; to set a monthly spending cap. You can also configure it from the terminal with &lt;code&gt;/extra-usage&lt;/code&gt;. Once enabled, hitting a rate limit is seamless. Claude keeps working and the overflow cost appears on your next bill.&lt;/p&gt;
&lt;p&gt;This is worth enabling even if you rarely hit limits. It acts as a safety net for those sessions where you are deep in a complex problem and cannot afford to stop.&lt;/p&gt;
&lt;h3 id=&#34;putting-it-all-together&#34;&gt;Putting It All Together&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/getting-the-most-from-claude-subscription/putting-it-all-together.webp&#34; alt=&#34;Putting It All Together&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The short version:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;em&gt;Monitor&lt;/em&gt; your token usage with a statusline&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Match&lt;/em&gt; the model and effort to the task&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Compact&lt;/em&gt; at 60%, not at the limit&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Clear&lt;/em&gt; between unrelated tasks&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Be specific&lt;/em&gt; in every prompt&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Group&lt;/em&gt; related work together&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Delegate&lt;/em&gt; verbose operations to subagents&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Time&lt;/em&gt; heavy work outside peak team hours&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Token efficiency is not about being stingy. It is about being intentional. Every token saved is a token available for actual work. Set up your monitoring, build the habits, and your subscription will go much further.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>High Level System Analysis and Design with Domain-Driven Design</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2026/04/high-level-system-analysis-and-design-ddd-part-1/"/>
      <id>https://www.endpointdev.com/blog/2026/04/high-level-system-analysis-and-design-ddd-part-1/</id>
      <published>2026-04-06T00:00:00+00:00</published>
      <author>
        <name>Kevin Campusano</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2026/04/high-level-system-analysis-and-design-ddd-part-1/pines-waterfall.webp&#34; alt=&#34;A wide waterfall cascades through a rocky, forested canyon surrounded by pine-covered hills under a cloudy sky.&#34;&gt;&lt;br&gt;
Photo by Zed Jensen, 2022.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is part 1 of a series of blog posts on Domain-Driven Design:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&#34;/blog/2026/04/high-level-system-analysis-and-design-ddd-part-1/&#34;&gt;High level system analysis and design with Domain-Driven Design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;/blog/2026/04/implementing-business-logic-ddd-part-2/&#34;&gt;Implementing business logic with Domain-Driven Design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;/blog/2026/05/designing-software-architecture-ddd-part-3/&#34;&gt;Designing software architecture with Domain-Driven Design&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Domain-Driven Design&lt;/strong&gt; is an approach to software development that focuses on, &lt;a href=&#34;https://www.oreilly.com/library/view/domain-driven-design-tackling/0321125215/&#34;&gt;as Eric Evans puts it&lt;/a&gt;, &amp;ldquo;tackling the complexity in the heart of software&amp;rdquo;. And what is in the heart of software? The business domain in which it operates. Or more specifically: a &lt;strong&gt;model&lt;/strong&gt; of it, made of code. That is, the code that implements the business logic that comes into play when solving problems within the realm of a particular business activity.&lt;/p&gt;
&lt;p&gt;DDD is not just about writing code though. It&amp;rsquo;s a whole methodology that touches on business needs, requirements gathering, organizational dynamics, high level architectural design, and lower level patterns for implementing software intensive systems.&lt;/p&gt;
&lt;p&gt;As a result, DDD offers a treasure trove of concepts, patterns and tools that can be applied to any software project, regardless of the size and complexity.&lt;/p&gt;
&lt;p&gt;In this series of blog posts we&amp;rsquo;re going to explore many aspects of DDD. We will be following the structure laid out by &lt;a href=&#34;https://vladikk.com/&#34;&gt;Vlad Khononov&lt;/a&gt;&amp;rsquo;s excellent book on the topic &amp;ldquo;&lt;a href=&#34;https://www.oreilly.com/library/view/learning-domain-driven-design/9781098100124/&#34;&gt;Learning Domain-Driven Design: Aligning Software Architecture and Business Strategy&lt;/a&gt;&amp;rdquo;. So you can think of this series as a summary of that book. An abridged version that can serve as a review for anybody who has read it; but also as an entry point for people who are new to DDD.&lt;/p&gt;
&lt;h3 id=&#34;table-of-contents&#34;&gt;Table of contents&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#high-level-system-analysis-and-design-with-domain-driven-design&#34;&gt;High level system analysis and design with Domain-Driven Design&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#table-of-contents&#34;&gt;Table of contents&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#section-1-analyzing-business-domains&#34;&gt;Section 1. Analyzing business domains&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#domains-and-subdomains&#34;&gt;Domains and subdomains&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#types-of-subdomains&#34;&gt;Types of subdomains&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#using-subdomains-to-make-strategic-decisions&#34;&gt;Using subdomains to make strategic decisions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#section-2-discovering-domain-knowledge&#34;&gt;Section 2. Discovering domain knowledge&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#the-ubiquitous-language&#34;&gt;The ubiquitous language&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#the-ubiquitous-language-as-a-model-of-the-domain&#34;&gt;The ubiquitous language as a model of the domain&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#tools-for-capturing-the-ubiquitous-language&#34;&gt;Tools for capturing the ubiquitous language&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#section-3-managing-domain-complexity&#34;&gt;Section 3. Managing domain complexity&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#reasons-for-creating-bounded-contexts&#34;&gt;Reasons for creating bounded contexts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#bounded-contexts-vs-subdomains&#34;&gt;Bounded contexts vs subdomains&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#section-4-integrating-bounded-contexts&#34;&gt;Section 4. Integrating bounded contexts&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;#cooperation-patterns&#34;&gt;Cooperation patterns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#customer-supplier-patterns&#34;&gt;Customer-supplier patterns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#separate-ways&#34;&gt;Separate ways&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#the-context-map&#34;&gt;The context map&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;#the-ddd-high-level-design-concept-map&#34;&gt;The DDD high level design concept map&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;section-1-analyzing-business-domains&#34;&gt;Section 1. Analyzing business domains&lt;/h3&gt;
&lt;p&gt;It is a well understood notion that before writing code, we need to understand the problem we&amp;rsquo;re trying to solve. DDD is consistent with this notion and argues that developers need to, first and foremost, gain an understanding of the business that the software is being built for. To this end, DDD relies on three concepts: domains, subdomains and domain experts.&lt;/p&gt;
&lt;h4 id=&#34;domains-and-subdomains&#34;&gt;Domains and subdomains&lt;/h4&gt;
&lt;p&gt;In simple terms, the &lt;strong&gt;domain&lt;/strong&gt; of a business is what it does, its area of activity. For example, Starbucks is in the business of making coffee, Ford is in the business of making automobiles, AMC is in the business of movie theaters.&lt;/p&gt;
&lt;p&gt;Of course, analyzing a business as a single integrated whole can be unmanageable. That&amp;rsquo;s where subdomains come in. &lt;strong&gt;Subdomains&lt;/strong&gt; are the different divisions within a domain. Starbuck&amp;rsquo;s domain for example, is making coffee. But there are many smaller parts that make up that business and allow it to be successful. There&amp;rsquo;s of course, the subdomain of coffee preparation. But there&amp;rsquo;s also real estate management to find and secure good locations, there&amp;rsquo;s inventory management and logistics, there&amp;rsquo;s marketing, there&amp;rsquo;s human resources, etc. All these are the subdomains that make up the overall business of Starbucks.&lt;/p&gt;
&lt;p&gt;Depending on the business and on the project, these will vary greatly in granularity. And you can also decompose subdomains further and discover new fine-grained subdomains nested within more coarse-grained ones. The sizes and nesting levels can be very different from business to business, so it can be difficult to clearly delineate where a subdomain ends and another one starts, which activities belong to one or the other. One good rule of thumb to keep in mind is that generally, subdomains encapsulate a set of coherent, closely related use cases. That is, use cases that involve the same set of closely related actors, business entities and/or data.&lt;/p&gt;
&lt;p&gt;And finally, we have &lt;strong&gt;domain experts&lt;/strong&gt;. As the name suggests, these are the people within the organization who have intimate knowledge of the business, or certain areas of it. They are the subject matter experts. Usually they are the ones who identify the problems and come up with requirements. Developers need to rely on domain experts to gain the necessary understanding to be able to produce useful software solutions.&lt;/p&gt;
&lt;p&gt;So, when approaching software projects, DDD suggests that developers work closely with domain experts in order to learn from them about the business domain and subdomains. After all, it is their mental models and understanding that will be modeled and implemented in code.&lt;/p&gt;
&lt;h4 id=&#34;types-of-subdomains&#34;&gt;Types of subdomains&lt;/h4&gt;
&lt;p&gt;With the help of domain experts, developers can identify subdomains, understand their business value and how they fit within the overall business strategy. This is very important because it helps making some initial architecturally significant decisions. Namely, the general approach to solving the problems in the subdomains, how much to invest, how to organize development teams, etc. The main objective in this analysis stage is to identify the subdomains and whether they fall into one of three types: core, generic, and supporting.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Core subdomains&lt;/strong&gt; are the activities of the business with highest value. The ones that confer differentiation in the market and a competitive advantage. They are the business&amp;rsquo; raison d&amp;rsquo;être. For example, for Google, their search engine is a core subdomain. For Ford, their automotive engineering area would be a core subdomain. Core subdomains are generally the most complex parts of the business. They are constantly evolving and improving, and the company is compelled to invest heavily in them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Generic subdomains&lt;/strong&gt; are also very complex. However, they are not business differentiators. Instead, these are the areas of the business that all organizations handle in the same way. Think accounting, a ticketing system, an online storefront. There&amp;rsquo;s no pressure to innovate in these areas, so the solutions are very stable and evolution is slow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Supporting subdomains&lt;/strong&gt; on the other hand, do not provide any competitive advantage, nor are they very complex. They are however, necessary because they support the core business activities, and are fairly unique. The solutions to problems in these areas usually take the form of &lt;a href=&#34;https://en.wikipedia.org/wiki/Create,_read,_update_and_delete&#34;&gt;CRUD&lt;/a&gt; or &lt;a href=&#34;https://en.wikipedia.org/wiki/Extract,_transform,_load&#34;&gt;ETL&lt;/a&gt; oriented activities. Imagine for example populating a data warehouse, translating transactional business data into a format appropriate for analytics and business intelligence in a manufacturing corporation. Or maybe the digitization and storage of a registry of court documents for a law firm. These are behind the scenes activities that support the organizations&amp;rsquo; main businesses, but their business logic complexity is not high, and they don&amp;rsquo;t really represent big selling points for the company.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s worth noting as well, that there may be subdomains where software solutions are not appropriate, even if they are highly complex core subdomains. They are still part of the business so it&amp;rsquo;s worth identifying and considering for high level architectural design decisions. If anything, to know what parts of the business the planned software system should and should not focus on. You could have a restaurant, for example, who prides itself in having the best desserts in the city. For their business, the recipe development activities constitute a core subdomain. This is dependent on the art and craftsmanship of the chefs. Not an area in which software solutions could help a whole lot.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s also worth noting that, just as organizations&amp;rsquo; business strategies are dynamic, so too can be their subdomain distribution. Today&amp;rsquo;s generic subdomain can be tomorrow&amp;rsquo;s core subdomain, and so on. For example, imagine a big retail store chain that, up until now, managed its inventory in an industry standard way. But it has grown so much that the standard way of doing things has become a bottleneck for them. So they design a new procedure for highly efficient inventory management, and that gives them an edge against competitors. Inventory management started as a generic subdomain for them, but due to an ever evolving business strategy, it became a core subdomain.&lt;/p&gt;
&lt;p&gt;The knowledge of subdomain types can also help the analysis when dividing the business and discovering subdomains. If you find yourself recursively finding more and more fine-grained subdomains from already identified ones, a good time to stop recursing is when you find that a generic or supporting subdomain, when broken down, reveals only new finer-grained generic or supporting subdomains. The reasoning for this is simple: this analysis activity is most valuable when trying to identify core subdomains. When there are no more core subdomains to be found within a particular area of the business, then it&amp;rsquo;s probably not worth it to keep digging deeper.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/high-level-system-analysis-and-design-ddd-part-1/decomposing-a-generic-subdomain.png&#34; alt=&#34;Decomposing a generic subdomain&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Here we have an example of a hypothetical help desk system subdomain, part of a customer service subdomain, being decomposed and revealing that it only contains generic subdomains.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;In summary, these are the main characteristics of the three types of subdomains:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Core subdomains&lt;/strong&gt; have high complexity, provide competitive advantage, and change and evolve frequently.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Generic subdomains&lt;/strong&gt; have high complexity, do not provide competitive advantage and change overtime, although at a slower pace than core subdomains.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Support subdomains&lt;/strong&gt; have low complexity, do not provide competitive advantage and are the slowest to change.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/high-level-system-analysis-and-design-ddd-part-1/subdomain-types-complexity-vs-differentiation.png&#34; alt=&#34;Subdomain types complexity vs differentiation&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This is where each type of subdomain fall when considering their business logic complexity and business differentiation.&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&#34;using-subdomains-to-make-strategic-decisions&#34;&gt;Using subdomains to make strategic decisions&lt;/h4&gt;
&lt;p&gt;Like I&amp;rsquo;ve already alluded to, armed with this knowledge, DDD practitioners are ready to start making some higher level architectural decisions. Depending on the type of subdomain that a problem belongs to, DDD has specific prescriptions on how to handle the implementation of software solutions for them.&lt;/p&gt;
&lt;p&gt;When working on &lt;strong&gt;core subdomains&lt;/strong&gt;, that&amp;rsquo;s where we want to make the biggest investments. We deploy the most advanced engineering tools, patterns and practices. This is to make sure that the software is efficient and high quality, and also easy to maintain and evolve. This is necessary because core subdomains have to evolve rapidly by nature, if the business is to maintain competitive advantage. Software solutions that operate within the context of core subdomains have to be implemented by high skill and high trust teams. Either in-house, or via trusted development partners, working hand in hand with domain experts.&lt;/p&gt;
&lt;p&gt;Problems in &lt;strong&gt;generic subdomains&lt;/strong&gt;, by nature of their business logic being very complex but also very common, are likely to have already been solved. For these types of problems, DDD recommends against implementing custom software, and instead buying and/or adopting tried and true, industry standard, off-the-shelf solutions. Their implementation and integration can be outsourced or handled by less specialized or skilled teams. The change management of these solutions is simple, as they get delivered generally via patches and updates.&lt;/p&gt;
&lt;p&gt;For &lt;strong&gt;support subdomains&lt;/strong&gt;, whose business logic is generally simple but uncommon, it is less likely that off-the-shelf solutions would be available. So software addressing problems in these subdomains will most likely have to be implemented as custom solutions. Due to their low complexity though, they can be easily outsourced, or handled by more junior team members. They can also be handled with &lt;a href=&#34;https://en.wikipedia.org/wiki/Rapid_application_development&#34;&gt;RAD&lt;/a&gt;, low-to-no-code technologies, since often times they are little more than ETL and pure CRUD applications.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a table to summarizes the differences between the types of subdomains:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Type of subdomain&lt;/th&gt;
          &lt;th&gt;Core&lt;/th&gt;
          &lt;th&gt;Generic&lt;/th&gt;
          &lt;th&gt;Supporting&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Competitive advantage&lt;/td&gt;
          &lt;td&gt;Yes&lt;/td&gt;
          &lt;td&gt;No&lt;/td&gt;
          &lt;td&gt;No&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Complexity&lt;/td&gt;
          &lt;td&gt;High&lt;/td&gt;
          &lt;td&gt;High&lt;/td&gt;
          &lt;td&gt;Low&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Rate of change&lt;/td&gt;
          &lt;td&gt;High&lt;/td&gt;
          &lt;td&gt;Medium&lt;/td&gt;
          &lt;td&gt;Low&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Implementation&lt;/td&gt;
          &lt;td&gt;Custom development&lt;/td&gt;
          &lt;td&gt;Buy/adopt off-the-shelf&lt;/td&gt;
          &lt;td&gt;Custom development&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Team composition&lt;/td&gt;
          &lt;td&gt;In-house/partners&lt;/td&gt;
          &lt;td&gt;Can outsource&lt;/td&gt;
          &lt;td&gt;Can outsource&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Skill level&lt;/td&gt;
          &lt;td&gt;High&lt;/td&gt;
          &lt;td&gt;High/regular&lt;/td&gt;
          &lt;td&gt;Low&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Investment&lt;/td&gt;
          &lt;td&gt;High&lt;/td&gt;
          &lt;td&gt;Medium&lt;/td&gt;
          &lt;td&gt;Low&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Problems&lt;/td&gt;
          &lt;td&gt;Interesting&lt;/td&gt;
          &lt;td&gt;Solved&lt;/td&gt;
          &lt;td&gt;Simple&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;section-2-discovering-domain-knowledge&#34;&gt;Section 2. Discovering domain knowledge&lt;/h3&gt;
&lt;p&gt;After identifying the business domain and categorizing the various subdomains that compose it, we have a bird&amp;rsquo;s-eye view of the business. This is good enough to get started and make high level architectural decisions of potential software solutions to problems within these domains. To actually build the software though, we need much more than that. In order to gain a thorough understanding of the business logic, and be able to eventually model and implement it in code, DDD proposes the &lt;strong&gt;ubiquitous language&lt;/strong&gt; as a tool.&lt;/p&gt;
&lt;h4 id=&#34;the-ubiquitous-language&#34;&gt;The ubiquitous language&lt;/h4&gt;
&lt;p&gt;The &lt;strong&gt;ubiquitous language&lt;/strong&gt; is DDD&amp;rsquo;s tool for knowledge sharing, effective communication and software modeling. In simple terms, the ubiquitous language is the language of the business. It&amp;rsquo;s the language that domain experts use on a day to day basis to talk and reason about the business.&lt;/p&gt;
&lt;p&gt;For a software project to be successful, it&amp;rsquo;s essential that engineers and stakeholders understand each other. They have to be aligned when it comes to the meanings of the core concepts of the business domain. That&amp;rsquo;s why it&amp;rsquo;s so important that everybody uses the ubiquitous language for all project related communications, requirements, documentation, face to face discussions, and even code itself. It all needs to share the same language.&lt;/p&gt;
&lt;p&gt;Engineers will need to interact with domain experts in order to learn about the business domain and its rules. They need to acquire the necessary knowledge that allows them to implement these rules in the software. During these interactions, they converse using the ubiquitous language. However, the knowledge transfer is not always strictly unidirectional. Yes, engineers have to learn from the domain experts. But also, through conversations and questioning, engineers can help domain experts deepen and flesh out their own understanding about their domains.&lt;/p&gt;
&lt;p&gt;A classic example of this is when domain experts focus too much on the &amp;ldquo;happy paths&amp;rdquo; of given business processes. Then, through discussions with developers, on account of the inherent precision that software demands, they are forced to consider more edge cases, better specify ambiguous terms and fill out gaps in their understanding.&lt;/p&gt;
&lt;p&gt;This deepening of knowledge feeds back into the ubiquitous language and improves it. Making it more insightful and more precise. This means that, throughout the life cycle of a project, the ubiquitous language should keep evolving, expanding and refining. This allows it to continuously improve as an effective model of the business domain, for the problem that it&amp;rsquo;s trying to solve. Indeed, just like code is a model of the business domain, so too is the ubiquitous language.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/high-level-system-analysis-and-design-ddd-part-1/traditional-knowledge-sharing-flow.png&#34; alt=&#34;Traditional knowledge sharing flow&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A classic pitfall in software projects is a manner of communication and knowledge sharing where developers are various steps removed from the domain experts. Consistent use of the ubiquitous language addresses this.&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&#34;the-ubiquitous-language-as-a-model-of-the-domain&#34;&gt;The ubiquitous language as a model of the domain&lt;/h4&gt;
&lt;p&gt;Through consistent use of the ubiquitous language, developers are able to obtain a deeper understanding of the domain, the problems we&amp;rsquo;re trying to solve, the reasoning behind the requirements and the mental model of the domain experts. This allows the construction of effective software solutions, with deep business insight, that go beyond simply translating requirements into code. If the implementation models the business domain effectively, there is potential for it to evolve with the business and better adapt as requirements change; and we reduce the risk of missing edge cases that may not be obvious to domain experts.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/high-level-system-analysis-and-design-ddd-part-1/knowledge-sharing-in-ddd.png&#34; alt=&#34;Knowledge sharing under DDD&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Under DDD, developers and domain experts develop the ubiquitous language, which then informs the implementation.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Effectively, when we build a ubiquitous language, we&amp;rsquo;re building a model of the domain. A model that reflects the relevant business entities, their behavior, and relationships. A model that will be eventually implemented into code, but also a model that needs to be understood by all stakeholders, regardless of their technical level. So it needs to be precise and rigorous, but also understandable. As such, in order to be useful, the ubiquitous language needs to respect certain restrictions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It must not include technical jargon like programming languages, constructs or frameworks; nor mention specific system structures like database tables, servers or programs. It is the language of the business and it needs to be understandable by non technical folks.&lt;/li&gt;
&lt;li&gt;It must avoid using the same term for different concepts. In conversation with humans, a lot of the meaning is extracted from the context. In software, not so much. Just like you can&amp;rsquo;t have two classes with the same name within the same namespace, the ubiquitous language cannot allow such ambiguities.&lt;/li&gt;
&lt;li&gt;It should also avoid using different words for the same concept. The classic example for this is words like users, accounts, customers, visitors. They all refer to closely related concepts which might actually have differences. The problem with these overlapping terms is that those differences are obscured by the notion of them &amp;ldquo;being the same&amp;rdquo; and being used interchangeably. For usage within the ubiquitous language, it&amp;rsquo;s best to be precise and clearly delineate terms and definitions. Eliminate what&amp;rsquo;s redundant, be strict with definitions.&lt;/li&gt;
&lt;li&gt;It should avoid including extraneous details. Just like code needs to include only what&amp;rsquo;s needed to solve the problem at hand and nothing else (in order to avoid accidental complexity); the ubiquitous language must not be polluted with details from outside of the area of business activity that it represents. That can create confusion and unnecessary cognitive load.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/high-level-system-analysis-and-design-ddd-part-1/model-translations.png&#34; alt=&#34;Classic model translations&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;The ubiquitous language represents a unified model of the domain. This is in contrast to a more traditional process where domain knowledge gets &amp;ldquo;translated&amp;rdquo; multiple times before turning into code.&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&#34;tools-for-capturing-the-ubiquitous-language&#34;&gt;Tools for capturing the ubiquitous language&lt;/h4&gt;
&lt;p&gt;The &lt;a href=&#34;https://agilemanifesto.org/&#34;&gt;Agile Manifesto&lt;/a&gt; declares: &amp;ldquo;Individuals and interactions over processes and tools&amp;rdquo;. And of course, when it comes to the ubiquitous language, direct interaction and conversations between engineers, domain experts and other stakeholders reigns supreme. That&amp;rsquo;s when it can be most useful. It can also be useful, however, to capture the ubiquitous language in documentation and even in runnable form.&lt;/p&gt;
&lt;p&gt;A glossary of terms is a great asset for keeping a ubiquitous language. A wiki is a good place to put this. Definitions for key concepts in the business like entities, processes and rules can be captured here. The only caveat is that documentation is static by nature, while the ubiquitous language is continually evolving. So, great care needs to be taken to keep the wiki updated at all times to reflect the latest and most complete understanding of the domain. This should not be relegated to or gate-keeped by only certain people; it should be a team effort where everyone contributes.&lt;/p&gt;
&lt;p&gt;An automated acceptance tests suite, written using &lt;a href=&#34;https://en.wikipedia.org/wiki/Behavior-driven_development&#34;&gt;Behavior Driven Development&lt;/a&gt; frameworks, like &lt;a href=&#34;https://cucumber.io/&#34;&gt;Cucumber&lt;/a&gt;, is also a great way of capturing the ubiquitous language. The advantage of these tests is that they are written in plain human-readable language, not code. And while it may be far fetched to think that non technical domain experts would be capable of writing and maintaining such tests, they certainly can read and understand them, which is a great boon. These tests speak the language they understand: the language of the business.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/high-level-system-analysis-and-design-ddd-part-1/cucumber.png&#34; alt=&#34;Cucumber tests&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;This is what Cucumber tests look like.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;By nature of being executable and tied closely to the implementation code, there is less chance that they become out of date. This can happen more easily with static documentation written in a wiki. The disadvantage is that they require much more effort. But for the right kind of project, one where business logic is very complex or the scope is very wide, they can be very well worth the cost.&lt;/p&gt;
&lt;h3 id=&#34;section-3-managing-domain-complexity&#34;&gt;Section 3. Managing domain complexity&lt;/h3&gt;
&lt;p&gt;We can attempt to model an entire business domain with one big ubiquitous language, but sometimes that&amp;rsquo;s impossible. Especially so for businesses of a certain size, you will inevitably find inconsistencies and conflicts between the mental models of different domain experts. The simplest example of this scenario is when different experts from different areas of the organization have the same word to describe different concepts. Or when they look at the same business entity with different levels of detail.&lt;/p&gt;
&lt;p&gt;When the different mental models are valid, and these inconsistencies are legitimate and cannot be reconciled, the solution is to follow the divide and conquer principle and decompose the language into separate ones, each one working within its own bounded context.&lt;/p&gt;
&lt;p&gt;Simply put, a &lt;strong&gt;bounded context&lt;/strong&gt; is the context within which a ubiquitous language, and the model it represents, operate, have meaning and are useful. Sure enough, just like a domain can be divided into subdomains, a ubiquitous language can be decomposed into smaller languages, each with its own context, to model different parts of the business. While we can attempt to capture an entire business domain with a single model, this is not advisable in the case of complex systems. It&amp;rsquo;s better to split up the model into smaller, more precise ones, tailored to work on specific problem domains.&lt;/p&gt;
&lt;h4 id=&#34;reasons-for-creating-bounded-contexts&#34;&gt;Reasons for creating bounded contexts&lt;/h4&gt;
&lt;p&gt;Like mentioned before, different domain experts having conflicting mental models is a clear indication that a division in the model needs to happen. However, that only tells us half of the story. There are other indicators that point to when and where further division should happen:&lt;/p&gt;
&lt;p&gt;First of all, there is of course, size. Size by itself is not a deciding factor, but it is something to consider and balance. Fewer, bigger models can help keep the overall environment simpler, but if they become too big they can become unmanageable and prone to corruption. A higher number of smaller models keep cognitive load low, but you run the risk of exploding integration and management complexity. The better principles to keep in mind here are coupling and cohesion. Beware of separating closely tied use cases, that deal with similar actors, entities and data.&lt;/p&gt;
&lt;p&gt;System-level nonfunctional requirements also play a role in dividing a model into separate bounded contexts. For example, you might need to decouple the life cycle of several software components. Have them be developed, evolved, versioned and deployed independently. You might also need them to scale separately. You might even need to use completely different technology stacks, whatever is appropriate for the task at hand. This means that bounded contexts generally get implemented as individual services and/or applications. That is, as individual runtime components.&lt;/p&gt;
&lt;p&gt;The organization&amp;rsquo;s composition also plays a role when designing bounded contexts. The general rule is that a given ubiquitous language, the model it represents and the bounded context is lives in, must be owned by a single team. In software, high ownership, cohesion and consistency are desirable traits. Having a single team own and maintain a particular component foments these. Single ownership also helps reduce communication overhead, and prevents people from stepping on one another&amp;rsquo;s toes. Of course, a single team can own multiple bounded contexts, what cannot happen is one bounded context being owned by multiple teams.&lt;/p&gt;
&lt;p&gt;Indeed, in bounded contexts, we have the tools we need to make strategic decisions related to the decomposition of software systems into architecturally significant components or modules. With bounded contexts, we are able to specify the physical and ownership boundaries of these components.&lt;/p&gt;
&lt;h4 id=&#34;bounded-contexts-vs-subdomains&#34;&gt;Bounded contexts vs subdomains&lt;/h4&gt;
&lt;p&gt;Bounded contexts and subdomains are closely related concepts, but there&amp;rsquo;s one key distinction: &lt;strong&gt;subdomains&lt;/strong&gt; are discovered, while &lt;strong&gt;bounded contexts&lt;/strong&gt; are designed. Subdomains are useful because they help us understand the business strategy. Splitting the business domain into smaller problem domains is useful because it can break down a complex whole into smaller and more manageable parts.&lt;/p&gt;
&lt;p&gt;Depending on the situation, it is certainly possible to end up with a set of bounded contexts that align one-to-one with the business subdomains. However, this is not mandatory. We can develop a solution with a single bounded context that spans multiple subdomains; the same way that we can decompose the problem into many bounded contexts, some of which operate within the same particular subdomain.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/high-level-system-analysis-and-design-ddd-part-1/subdomains-and-bounded-contexts.png&#34; alt=&#34;Subdomains and bounded contexts&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Bounded contexts are closely related to subdomains, but aren&amp;rsquo;t tied to them. They are flexible and can be organized in many ways.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Each bounded context becomes a separate major architectural component. That is, a (micro) service, a project, an application. When we have a component that spans multiple subdomains, programming language organizational structures like namespaces or modules can be used to logically separate the subdomains within.&lt;/p&gt;
&lt;h3 id=&#34;section-4-integrating-bounded-contexts&#34;&gt;Section 4. Integrating bounded contexts&lt;/h3&gt;
&lt;p&gt;For a system to function, its components need to interact with each other. So, once we have decomposed the problem domain into separate bounded contexts, we need to decide their relationship and integration strategies. This need for interaction between them implies that there are touch points between bounded contexts. We call them &lt;strong&gt;contracts&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;These contracts are necessary because each bounded context contains its own version of the world. That is, its own model and ubiquitous language. In order to integrate, some level of translation needs to happen. They need to be adapted to one another. The contracts define these adaptations.&lt;/p&gt;
&lt;p&gt;Domain-Driven Design offers various patterns that are useful for defining contracts between bounded contexts. The decision to use one pattern vs the other depends greatly on the nature of the teams tackling the project. Depending on the teams&amp;rsquo; relationship, we can put the patterns in one of three categories: cooperation, customer-supplier, and separate ways.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/high-level-system-analysis-and-design-ddd-part-1/collaboration-spectrum.png&#34; alt=&#34;Collaboration spectrum&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Integration patterns are determined mainly by the level of cooperation between the teams that own the interacting bounded contexts.&lt;/em&gt;&lt;/p&gt;
&lt;h4 id=&#34;cooperation-patterns&#34;&gt;Cooperation patterns&lt;/h4&gt;
&lt;p&gt;When the components that need to interact are owned by teams which are in close communication, work well together, and whose goals are aligned, &lt;strong&gt;cooperation&lt;/strong&gt; patterns can be applied.&lt;/p&gt;
&lt;p&gt;If the teams meet these criteria, a &lt;strong&gt;partnership&lt;/strong&gt; model can be implemented. This is where the integration is managed in an ad-hoc manner. Both teams work together to define the API through which their components interact and that&amp;rsquo;s that. Whenever changes happen on either side, the other team learns about it quickly and adjusts their code right away. Continuous integration is a great tool here, as breakages are immediately apparent, closing down the feedback loop.&lt;/p&gt;
&lt;p&gt;Sometimes, when different bounded contexts need to implement the same functionality, it makes more sense to develop this functionality once and package it as a reusable library, or a sub-module within a shared repository. This is what DDD calls a &lt;strong&gt;shared kernel&lt;/strong&gt;. A shared kernel is effectively a bounded context of its own, one that is statically linked to other components and implements the logic that others depend on.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/high-level-system-analysis-and-design-ddd-part-1/shared-kernel.png&#34; alt=&#34;Shared kernel&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A shared kernel is its own bounded context, but also &amp;ldquo;belongs&amp;rdquo; to multiple other bounded contexts.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;The shared kernel integration pattern needs to be used carefully though, as it creates tight coupling between the components that use it. If the involved bounded contexts are owned by different teams, then this shared kernel that emerges violates the DDD principle of bounded contexts having single-team ownership. This is something to watch out for, as bad team synergy can produce problems during development and maintenance.&lt;/p&gt;
&lt;p&gt;When using a shared kernel, the quality of the communication and the coordination between the teams has to strike the right balance: when they aren&amp;rsquo;t strong enough to support a partnership model, but not so weak that the shared kernel would become more trouble than it&amp;rsquo;s worth.&lt;/p&gt;
&lt;p&gt;The alternative to a shared kernel is the duplication of the logic across multiple components. When deciding whether to apply this pattern, the costs of duplication vs coordination need to be considered. That is, the cost of implementing the same logic within multiple components and its subsequent changes, vs the cost of changes in the shared kernel propagating into dependent components and coordinating with the owners of each. When it is cheaper to just duplicate the logic, shared kernel should not be applied.&lt;/p&gt;
&lt;p&gt;This means that complicated models that change frequently, like those of core subdomains, are good candidates for shared kernel. Shared kernels allow complexity to be encapsulated and exposed through stable contracts. This means that frequent changes are easier to contain. Frequent changes in logic that&amp;rsquo;s highly complex and also repeated in multiple places, quickly becomes much more expensive to maintain.&lt;/p&gt;
&lt;p&gt;Another good scenario for applying a shared kernel is when refactoring a legacy monolithic system into more separated modules. The legacy system can become the shared kernel during the time that modules are being extracted from it but not yet fully decoupled.&lt;/p&gt;
&lt;h4 id=&#34;customer-supplier-patterns&#34;&gt;Customer-supplier patterns&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Customer-supplier&lt;/strong&gt; patterns establish a relationship between components where one (the provider, who is &amp;ldquo;upstream&amp;rdquo;), provides a service to another (the consumer, who is &amp;ldquo;downstream&amp;rdquo;). These types of patterns emerge when the teams who own the involved bounded contexts are not in close collaboration and have their own independent goals.&lt;/p&gt;
&lt;p&gt;One form of customer-supplier integration is the &lt;strong&gt;conformist&lt;/strong&gt; pattern. This happens when the supplier defines the integration contract/API, using its own language, concepts and model, and the consumer accepts it. This can happen when the upstream service has a well established or industry-standard model. Or maybe the model is &amp;ldquo;good enough&amp;rdquo; for the consumer to interact with directly. Organizational politics may also be a reason for this type of integration, where there is an imbalance of power favoring the supplier&amp;rsquo;s team, and they get to impose their model or can&amp;rsquo;t be bothered to adapt it.&lt;/p&gt;
&lt;p&gt;Sometimes, the consumer won&amp;rsquo;t accept the supplier&amp;rsquo;s model. In that case, an &lt;strong&gt;anticorruption layer&lt;/strong&gt; can be created. An anticorruption layer is implemented by the consumer and translates the supplier&amp;rsquo;s model into its own. This allows the consumer to use the supplier&amp;rsquo;s service without polluting its model with extraneous concepts.&lt;/p&gt;
&lt;p&gt;Anticorruption layers can be good solutions for the following scenarios:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;When the consumer represents a core subdomain. The consumer is solving hard and interesting problems, so it&amp;rsquo;s best to protect its model with the ACL.&lt;/li&gt;
&lt;li&gt;When the supplier&amp;rsquo;s model is messy and inconvenient. The ACL can protect the consumer&amp;rsquo;s model from having to contend with a mess of extraneous concepts.&lt;/li&gt;
&lt;li&gt;When the supplier&amp;rsquo;s contract changes frequently. The ACL can protect the consumer from those changes, encapsulating them, allowing it to be less volatile.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;A third pattern represents an anticorruption layer of sorts, only built on the supplier&amp;rsquo;s side. DDD calls this the &lt;strong&gt;open-host service&lt;/strong&gt; pattern. Here, the upstream service comes up with a new language, separate from its own, tailored to its consumers&amp;rsquo; convenience, and exposes that as its contract. This public protocol is called a &amp;ldquo;published language&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/high-level-system-analysis-and-design-ddd-part-1/open-host-service.png&#34; alt=&#34;Open-host service&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;An open-host service provides a protocol for consumers to interact with. It can also support multiple versions of this protocol simultaneously.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;An open-host service applies to similar scenarios as an anticorruption layer. Decoupling a service&amp;rsquo;s internal model from its integration model frees it up for continuous evolution without fear of breaking its consumers. Another advantage of this is that multiple versions of the published language can be exposed, affording clients options on what to support and gradually migrate if they so choose.&lt;/p&gt;
&lt;h4 id=&#34;separate-ways&#34;&gt;Separate ways&lt;/h4&gt;
&lt;p&gt;Sometimes, the right decision is to not integrate at all. I alluded to this outcome back when discussing the pitfalls of the shared kernel. Sometimes duplication, &lt;a href=&#34;https://refactoring.guru/smells/duplicate-code&#34;&gt;in spite of how bad it smells&lt;/a&gt;, may be the most cost effective solution to a given situation. So the teams go their &lt;strong&gt;separate ways&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This can happen when the involved teams cannot collaborate effectively for whatever reason. It could be due to geographical, timezone, or organizational issues.&lt;/p&gt;
&lt;p&gt;This can also be a good solution when the repeated logic belongs in a generic subdomain, and it&amp;rsquo;s easy to integrate. For example consider a logging framework. Exposing that kind of functionality in a service for others to consume, in most cases would be much more trouble than just including some third party library or package.&lt;/p&gt;
&lt;p&gt;It may also be that the models being integrated are just so different that they are fundamentally incompatible. It may be cost-prohibitive for collaboration or customer-supplier patterns to be applied; and duplication again, is cheaper.&lt;/p&gt;
&lt;p&gt;Going separate ways can be dangerous when we&amp;rsquo;re talking about core subdomains though. So we have to tread carefully in those scenarios. Remember that models that represent core subdomain should be implemented in the most effective and efficient ways, with few shortcuts and minimized technical debt.&lt;/p&gt;
&lt;h4 id=&#34;the-context-map&#34;&gt;The context map&lt;/h4&gt;
&lt;p&gt;The &lt;strong&gt;context map&lt;/strong&gt; can be a useful tool for high level design as it plots all the major bounded contexts (i.e. modules, components, subsystems) that we&amp;rsquo;ve designed and their interaction patterns.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/high-level-system-analysis-and-design-ddd-part-1/context-map.png&#34; alt=&#34;Context map&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Here&amp;rsquo;s a context map which captures the various bounded contexts that compose a big system and their interactions. The arrows point to the upstream component in the relationship. &amp;ldquo;ACL&amp;rdquo; denotes an anticorruption layer and &amp;ldquo;OHS&amp;rdquo; represents an open-host service.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Of course, they can also offer valuable insight into organizational dynamics, as team composition and relationships with others are intrinsic parts of the discussion when talking about bounded contexts. For example, it can show teams that prefer to collaborate closely or at a healthy distance. I can also show problematic components, which are surrounded by anticorruption layers or have had their ties completely cut via a separate ways approach.&lt;/p&gt;
&lt;p&gt;As with any document, they run the risk of becoming stale as the system evolves. So it should be a team-wide responsibility to keep it up to date. Each team taking care of their own components and their integration points.&lt;/p&gt;
&lt;h3 id=&#34;the-ddd-high-level-design-concept-map&#34;&gt;The DDD high level design concept map&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2026/04/high-level-system-analysis-and-design-ddd-part-1/concept-map.png&#34; alt=&#34;The DDD high level design concept map&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;These are the main concepts that we&amp;rsquo;ve explored so far, and how they relate to each other.&lt;/em&gt;&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Containerizing Claude Code with Podman</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2026/03/claude-code-cli-in-container/"/>
      <id>https://www.endpointdev.com/blog/2026/03/claude-code-cli-in-container/</id>
      <published>2026-03-31T00:00:00+00:00</published>
      <author>
        <name>Seth Jensen</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2026/03/claude-code-cli-in-container/train-winter.webp&#34; alt=&#34;A long freight train points to the right, slightly toward the camera. The train cars extend off the image to the left. Above the tracks is a white snow-capped mountain and a deep blue sky.&#34;&gt;&lt;br&gt;
Photo by Seth Jensen, 2026.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been experimenting with many different AI tools, and my favorite is Claude Code. It provides the impressive performance of the Opus models without forcing me to use Visual Studio Code (or a fork of it).&lt;/p&gt;
&lt;p&gt;IDEs and fancy editors like IntelliJ and VS Code are great, but I often prefer the simplicity and low memory footprint of working directly in the terminal. Claude Code works well with my tmux-centered work environment.&lt;/p&gt;
&lt;p&gt;However, I don&amp;rsquo;t like giving AI agents access to all of my files, and I &lt;em&gt;really&lt;/em&gt; don&amp;rsquo;t like letting them run arbitrary commands in my shell. I&amp;rsquo;m already pretty cautious about running unvetted code on my machine (I&amp;rsquo;m looking at you, install.sh files I&amp;rsquo;m supposed to blindly &lt;code&gt;curl | bash&lt;/code&gt;), and the nondeterministic nature of LLMs takes this to the next level. It&amp;rsquo;s not just data-sniffing closed-source code or malware you need to worry about, it&amp;rsquo;s the product itself running commands and editing files in ways that, by design, are unique and untested.&lt;/p&gt;
&lt;p&gt;Claude Code has a &lt;a href=&#34;https://code.claude.com/docs/en/sandboxing&#34;&gt;sandbox mode&lt;/a&gt; which is supposed to limit filesystem access to the folder it&amp;rsquo;s run from, but since it&amp;rsquo;s closed-source (&lt;a href=&#34;https://arstechnica.com/ai/2026/03/entire-claude-code-cli-source-code-leaks-thanks-to-exposed-map-file/&#34;&gt;in theory&lt;/a&gt;), you can&amp;rsquo;t verify this isolation. Even in this mode, it can run commands outside the sandboxed folder (in my experience, it asks first, but I wouldn&amp;rsquo;t count on this).&lt;/p&gt;
&lt;p&gt;So, I wrote a little wrapper script to run Claude Code in a Podman container with access only to Claude&amp;rsquo;s config directory and the working directory you pass to the script. I also let it run with the &lt;code&gt;--dangerously-skip-permissions&lt;/code&gt; flag, which I would never do on my host machine (it didn&amp;rsquo;t take long to find a horror story of a typo in an &lt;code&gt;rm -rf&lt;/code&gt; command deleting half of the programs in &lt;code&gt;/Applications&lt;/code&gt; on one Redditor&amp;rsquo;s Mac).&lt;/p&gt;
&lt;h3 id=&#34;important-security-notes&#34;&gt;Important security notes&lt;/h3&gt;
&lt;p&gt;Claude has full access to the folder you pass to this script. That means it can run &lt;code&gt;rm -rf&lt;/code&gt; as much as it wants, and you won&amp;rsquo;t be prompted before it does so, especially when using the &lt;code&gt;--dangerously-skip-permissions&lt;/code&gt; flag.&lt;/p&gt;
&lt;p&gt;You should only run this script if you don&amp;rsquo;t mind everything in that folder getting deleted — for example, in a Git repository where everything has been pushed to a remote, or in a sandbox folder — it&amp;rsquo;s easy to create a copy of your working directory for Claude to play in!&lt;/p&gt;
&lt;p&gt;There is no constraint on outward network traffic, so Claude can send data anywhere it wants. If you&amp;rsquo;re working with sensitive data, you should set up a firewall so it only has access to the necessary Anthropic servers (and perhaps an allowlist you provide). &lt;strong&gt;Think carefully about what data you give to powerful AI agents.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;using-my-claude-container-script&#34;&gt;Using my claude-container script&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Make sure you have Podman and Podman Compose installed (this would also work with Docker &amp;amp; Docker Compose, just replace &lt;code&gt;podman compose&lt;/code&gt; with &lt;code&gt;docker compose&lt;/code&gt; in the &lt;code&gt;claude-container&lt;/code&gt; script)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Clone the repo from &lt;a href=&#34;https://github.com/sethjensen1/claude-container&#34;&gt;my GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;From the cloned folder, run &lt;code&gt;./claude-container /path/to/working/directory&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Optionally, add the script to your PATH. This is what I ran on macOS:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ln -s /Users/myuser/repos/claude-container/claude-container /Users/myuser/bin/claude-container&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can then run the script from anywhere: &lt;code&gt;claude-container /my/favorite/sandbox&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That&amp;rsquo;s all you need to run it! Keep reading if you&amp;rsquo;re interested in the technical details.&lt;/p&gt;
&lt;h3 id=&#34;what-the-script-is-doing&#34;&gt;What the script is doing&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;claude-container&lt;/code&gt; script is a lightweight wrapper around &lt;code&gt;podman compose&lt;/code&gt;, setting a couple of environment variables so the process is as simple as running Claude in the first place.&lt;/p&gt;
&lt;p&gt;The compose setup uses &lt;a href=&#34;https://docs.docker.com/engine/storage/bind-mounts/&#34;&gt;bind mounts&lt;/a&gt; to give Claude access to the folder you pass, along with your Claude config directory. This means you don&amp;rsquo;t have to reauthenticate every time, and that session history is shared between containers/​your local Claude install.&lt;/p&gt;
&lt;p&gt;Podman compose will automatically build the image if it doesn&amp;rsquo;t exist. If you want to trigger a rebuild, you can run it with the &lt;code&gt;-b&lt;/code&gt; flag: &lt;code&gt;claude-container -b /path/to/working/directory&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The first time you run the script, it will ask you if you&amp;rsquo;re okay bypassing permissions. Because we&amp;rsquo;re only mounting two directories, this should be much safer than it normally would be (provided you&amp;rsquo;re smart about which folders you pass).&lt;/p&gt;
&lt;h3 id=&#34;the-dockerfile&#34;&gt;The Dockerfile&lt;/h3&gt;
&lt;p&gt;The Dockerfile is based on the one from Claude&amp;rsquo;s devcontainer &lt;a href=&#34;https://code.claude.com/docs/en/devcontainer&#34;&gt;docs&lt;/a&gt;. I&amp;rsquo;ve stripped out some development helpers since I&amp;rsquo;m running Claude directly in the &lt;code&gt;CMD&lt;/code&gt;, not using the shell.&lt;/p&gt;
&lt;p&gt;I previously copied the working directory into the image, but have since moved to using bind mounts only. If I want to make sure that Claude doesn&amp;rsquo;t touch any files on my machine, I just create a copy of the working directory to bind mount to the container.&lt;/p&gt;
&lt;p&gt;Another important thing I removed from Claude&amp;rsquo;s devcontainer Dockerfile is the firewall setup with init-firewall.sh. Limiting outgoing traffic is a good idea, but the repos I was working with weren&amp;rsquo;t sensitive enough for me to spend the time getting this working (especially since it seems you need to run the container with the NET_ADMIN capability, which I don&amp;rsquo;t understand well enough to be using).&lt;/p&gt;
&lt;p&gt;Running this with no firewall is dangerous, but less so than running the agent outside of a container.&lt;/p&gt;
&lt;h3 id=&#34;no-container-ception-unfortunately&#34;&gt;No container-ception, unfortunately&lt;/h3&gt;
&lt;p&gt;At some point, I wanted to give Claude access to a backend dev server (it was running a frontend dev server in the container). But the backend dev server runs in a container itself, so I couldn&amp;rsquo;t just have Claude spin up the backend dev server in its container.&lt;/p&gt;
&lt;p&gt;After trying a few things, my workaround was to connect Claude&amp;rsquo;s container to the backend container&amp;rsquo;s Podman network. Once all your containers have been started, find the relevant container network with:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;podman network ls&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If you&amp;rsquo;re not sure which is the right network, you can check which containers are connected to a network using &lt;code&gt;podman network inspect &amp;lt;network_name&amp;gt;&lt;/code&gt;. To connect Claude to the desired network, run:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;podman network connect &amp;lt;relevant container&amp;#39;s network&amp;gt; &amp;lt;claude container&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And of course, if this container is connected to something important (like a database), this will allow Claude access to it. &lt;strong&gt;Always be careful before letting an LLM have access to data&lt;/strong&gt;. In my case, this was an empty database for development, so in terms of exposing data to Claude, it was similar to running the backend dev server within the container.&lt;/p&gt;
&lt;h3 id=&#34;a-few-notes&#34;&gt;A few notes&lt;/h3&gt;
&lt;p&gt;Claude won&amp;rsquo;t have access to some important developer tools in the container (e.g., Playwright). If you want to give it more tools, you could modify the Dockerfile to install them at build time. I tried installing Playwright at first, but found I actually prefer manually testing outside of the container &amp;amp; copying output back to Claude. I appreciate the extra time to slow down, check Claude&amp;rsquo;s decisions, and catch bugs.&lt;/p&gt;
&lt;p&gt;This setup intentionally doesn&amp;rsquo;t give your container SSH access — I don&amp;rsquo;t ever want an LLM to have access to my SSH key, and I would suggest thinking &lt;em&gt;very&lt;/em&gt; hard before giving it access to any servers. That&amp;rsquo;s one of the big reasons I don&amp;rsquo;t want to run Claude on my own machine, especially when using an SSH agent.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve enjoyed developing using Claude Code from within the confines of a single folder. I get the solid code analysis and generation without giving away an excessive amount of data.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Why I Am Focusing on Intelligent Document Processing</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2026/03/why-i-am-focusing-on-intelligent-document-processing/"/>
      <id>https://www.endpointdev.com/blog/2026/03/why-i-am-focusing-on-intelligent-document-processing/</id>
      <published>2026-03-27T00:00:00+00:00</published>
      <author>
        <name>Edgar Mlowe</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2026/03/why-i-am-focusing-on-intelligent-document-processing/mountains.webp&#34; alt=&#34;A snowy mountain towers over a clouded valley&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2026 --&gt;
&lt;p&gt;AI moves fast. A new model drops, a new framework launches, a new thing comes out, and suddenly what you learned last month feels old. LLMs, agents, fine-tuning, RAG, computer vision, multimodal models, prompt engineering, AI coding tools — the list keeps growing and it is hard to know where to focus.&lt;/p&gt;
&lt;p&gt;Recently I decided to stop trying to follow all of it and focus on one area. In this post, I will explain what that area is, how I found it, and why I think it is worth paying attention to.&lt;/p&gt;
&lt;h3 id=&#34;the-problem-with-being-an-ai-generalist&#34;&gt;The problem with being an AI generalist&lt;/h3&gt;
&lt;p&gt;When AI started becoming a practical tool for software engineers (not just researchers), I jumped in. I wrote about &lt;a href=&#34;/blog/2025/06/deploying-llms-efficiently-with-mixture-of-experts/&#34;&gt;deploying LLMs with Mixture of Experts&lt;/a&gt;, built an &lt;a href=&#34;/blog/2025/09/llm-expanded-vector-search/&#34;&gt;LLM-powered blog search&lt;/a&gt;, and worked on &lt;a href=&#34;/blog/2026/03/why-ai-extractor-fails-on-msg-emails-and-how-to-fix-decoding/&#34;&gt;AI extraction pipelines for document processing&lt;/a&gt;. At work, our team was building document processing systems with LLMs. On the side, I was reading about everything else.&lt;/p&gt;
&lt;p&gt;I was learning a lot, but I could not clearly say what I specialize in. If someone asked, &amp;ldquo;What is your thing in AI?&amp;rdquo; my answer was too broad to be useful.&lt;/p&gt;
&lt;h3 id=&#34;looking-at-the-pattern-in-the-projects&#34;&gt;Looking at the pattern in the projects&lt;/h3&gt;
&lt;p&gt;The turning point was stepping back and looking at the projects I had been involved in. Not the articles I had been reading, but the real systems I had been working on with the team.&lt;/p&gt;
&lt;p&gt;At End Point, one of the main projects our team has been working on is an AI order processing system. The problem was simple: a client receives purchase orders from dealers and resellers via email as PDFs, attachments, and sometimes just plain body text. Before our solution, someone had to open each email, read what was being ordered, and manually type the data into the ERP system. Invoicing addresses, delivery addresses, line items, quantities, all entered by hand.&lt;/p&gt;
&lt;p&gt;The solution is a pipeline that takes those raw email exports and PDF attachments, extracts structured data using LLMs with a defined schema, validates the output, handles exceptions through a human-in-the-loop review step, and produces a clean CSV that imports directly into the ERP. No more manual data entry.&lt;/p&gt;
&lt;p&gt;Looking at these projects, I started seeing the same pattern repeat across different industries, different source documents, and different target systems. But they shared a problematic pattern:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Unstructured data in → AI extraction and validation → Structured data out → Business system integration.&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;that-pattern-has-a-name&#34;&gt;That pattern has a name&lt;/h3&gt;
&lt;p&gt;I thought this was just a type of project that kept showing up. But when I researched it, I found out it is an established market with a formal name: &lt;strong&gt;Intelligent Document Processing (IDP)&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;IDP is about using AI (OCR, NLP, LLMs, computer vision) to automatically extract, classify, and transform data from unstructured documents like PDFs, emails, scanned forms, images, and legacy files into structured formats that plug into business systems.&lt;/p&gt;
&lt;p&gt;This is not something small. Major research firms track it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://www.gartner.com/reviews/market/intelligent-document-processing-solutions&#34;&gt;Gartner&lt;/a&gt; publishes a Market Guide for Intelligent Document Processing Solutions&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.processexcellencenetwork.com/tools-technologies/news/idc-rates-22-intelligent-document-processing-idp-vendors&#34;&gt;IDC&lt;/a&gt; rated 22 IDP vendors in one of their MarketScape assessments&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The vendor landscape includes names you already know: Google (&lt;a href=&#34;https://cloud.google.com/document-ai&#34;&gt;Document AI&lt;/a&gt;), Microsoft (&lt;a href=&#34;https://azure.microsoft.com/en-us/products/ai-foundry/tools/document-intelligence&#34;&gt;Azure Document Intelligence&lt;/a&gt;), Amazon (&lt;a href=&#34;https://aws.amazon.com/textract/&#34;&gt;Textract&lt;/a&gt;), ABBYY, UiPath, Automation Anywhere, and many more.&lt;/p&gt;
&lt;h3 id=&#34;the-market-is-real-and-growing-fast&#34;&gt;The market is real and growing fast&lt;/h3&gt;
&lt;p&gt;The numbers vary by research firm because they define the market scope differently, but they all point in the same direction:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Source&lt;/th&gt;
          &lt;th&gt;2024–2026 Estimate&lt;/th&gt;
          &lt;th&gt;Projected By 2034&lt;/th&gt;
          &lt;th&gt;Annual Growth&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;https://www.precedenceresearch.com/intelligent-document-processing-market&#34;&gt;Precedence Research&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;$4.3B (2026)&lt;/td&gt;
          &lt;td&gt;$43.9B&lt;/td&gt;
          &lt;td&gt;~34% per year&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;https://www.fortunebusinessinsights.com/intelligent-document-processing-market-108590&#34;&gt;Fortune Business Insights&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;$14.2B (2026)&lt;/td&gt;
          &lt;td&gt;$91.0B&lt;/td&gt;
          &lt;td&gt;~26% per year&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Growing at 26–34% per year makes IDP one of the fastest-growing areas in enterprise AI.&lt;/p&gt;
&lt;p&gt;This matters because market growth creates demand for people who can actually build these systems. Not just people who buy vendor platforms, but people who can design extraction schemas, debug LLM outputs, build validation pipelines, and connect everything to real business systems.&lt;/p&gt;
&lt;h3 id=&#34;what-skills-matter-in-idp&#34;&gt;What skills matter in IDP&lt;/h3&gt;
&lt;p&gt;Knowing the field has a name also made it clear what skills matter most. IDP work sits at the intersection of a few areas:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;LLM-based data extraction&lt;/strong&gt;: schema design, prompt engineering for structured output, few-shot examples for edge cases&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Document parsing&lt;/strong&gt;: handling PDFs, emails (.msg, .eml), scanned documents, HTML exports&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data validation and QA&lt;/strong&gt;: confidence scoring, exception routing, human-in-the-loop review workflows&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pipeline architecture&lt;/strong&gt;: from document intake to extraction, normalization, validation, and output&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Business system integration&lt;/strong&gt;: understanding ERP and CRM import formats, APIs, and data models&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;why-this-matters&#34;&gt;Why this matters&lt;/h3&gt;
&lt;p&gt;When a client has purchase orders stuck in email inboxes that aren&amp;rsquo;t processed fast enough, what they need is a reliable pipeline that extracts the right fields, handles the edge cases (like &lt;a href=&#34;/blog/2026/03/why-ai-extractor-fails-on-msg-emails-and-how-to-fix-decoding/&#34;&gt;character encoding issues in .msg files&lt;/a&gt;), validates the output, and delivers a clean import file.&lt;/p&gt;
&lt;p&gt;That is what IDP is about: turning data that businesses already have but cannot use into data that their existing systems can act on. Structured, validated output that goes into the ERP, the CRM, or the database, and saves real time and money.&lt;/p&gt;
&lt;h3 id=&#34;final-thoughts&#34;&gt;Final thoughts&lt;/h3&gt;
&lt;p&gt;AI is broad enough that no one person can master all of it. But looking at the projects I was actually working on, instead of trying to follow every new trend, made it much easier to find direction. In my case, that field turned out to be Intelligent Document Processing.&lt;/p&gt;
&lt;p&gt;If you need help parsing through legacy or unstructured data, we can help! Don&amp;rsquo;t hesitate to &lt;a href=&#34;/contact/&#34;&gt;send us a message&lt;/a&gt;.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Why Your AI Extractor Fails on .msg Emails (and How to Fix Decoding)</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2026/03/why-ai-extractor-fails-on-msg-emails-and-how-to-fix-decoding/"/>
      <id>https://www.endpointdev.com/blog/2026/03/why-ai-extractor-fails-on-msg-emails-and-how-to-fix-decoding/</id>
      <published>2026-03-13T00:00:00+00:00</published>
      <author>
        <name>Edgar Mlowe</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2026/03/why-ai-extractor-fails-on-msg-emails-and-how-to-fix-decoding/antenna-and-sky.webp&#34; alt=&#34;Against a blue sky with wispy white clouds, an old directional antenna points to the left of the camera atop a brick fireplace&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2025. --&gt;
&lt;p&gt;I want to share a debugging lesson that saved me from tuning the wrong layer in an AI extraction pipeline.&lt;/p&gt;
&lt;p&gt;It started with a familiar symptom: extraction output looked inconsistent. Some rows were fine, but some had extra characters, especially accents. My first instinct was the same one most of us have: maybe the model needs prompt tuning.&lt;/p&gt;
&lt;p&gt;It turned out not to be a model problem. The root cause was upstream data integrity: decoding &lt;code&gt;.msg&lt;/code&gt; email HTML with the wrong charset.&lt;/p&gt;
&lt;h3 id=&#34;the-pattern-that-gives-it-away&#34;&gt;The pattern that gives it away&lt;/h3&gt;
&lt;p&gt;If you see this mix, think decoding first:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;output is mostly correct, but certain names and addresses look garbled&lt;/li&gt;
&lt;li&gt;problems appear only for some senders or date ranges&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.eml&lt;/code&gt; looks stable, but &lt;code&gt;.msg&lt;/code&gt; is inconsistent&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A classic sign looks like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;expected: &lt;code&gt;Müller&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;corrupted: &lt;code&gt;MÃ¼ller&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By the time your extractor sees that text, the meaning is already damaged.&lt;/p&gt;
&lt;h3 id=&#34;why-msg-bites-harder-than-eml&#34;&gt;Why &lt;code&gt;.msg&lt;/code&gt; bites harder than &lt;code&gt;.eml&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;Quick definitions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;.eml&lt;/code&gt; is the standard MIME email format and usually includes charset metadata per part.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.msg&lt;/code&gt; is an Outlook container format (MAPI), where body bytes and encoding hints can be stored separately.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That difference matters.&lt;/p&gt;
&lt;p&gt;If your code assumes UTF-8 for &lt;code&gt;.msg&lt;/code&gt; HTML bytes, non-UTF messages can decode into garbage. Then downstream steps (HTML-to-PDF, OCR, LLM extraction, post-processing) just preserve and propagate bad text.&lt;/p&gt;
&lt;h3 id=&#34;the-fix-strict-explicit-controlled&#34;&gt;The fix: strict, explicit, controlled&lt;/h3&gt;
&lt;p&gt;You do not need a big rewrite. A small decode policy change can remove a whole class of silent failures. For &lt;code&gt;.msg&lt;/code&gt; HTML bytes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Read the encoding hint from message metadata.&lt;/li&gt;
&lt;li&gt;Map that hint to a decoder codec.&lt;/li&gt;
&lt;li&gt;Decode in strict mode.&lt;/li&gt;
&lt;li&gt;If needed, use one controlled strict fallback.&lt;/li&gt;
&lt;li&gt;If decode still fails, fail loud.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Minimal example in Python:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;extract_msg.encoding&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; lookupCodePage
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;PR_INTERNET_CODEPAGE = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;3FDE0003&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;decode_msg_html_bytes&lt;/span&gt;(html_bytes: &lt;span style=&#34;color:#038&#34;&gt;bytes&lt;/span&gt;, message) -&amp;gt; &lt;span style=&#34;color:#038&#34;&gt;str&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    codepage_id = message.getPropertyVal(PR_INTERNET_CODEPAGE)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    codec = (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        lookupCodePage(codepage_id)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#038&#34;&gt;isinstance&lt;/span&gt;(codepage_id, &lt;span style=&#34;color:#038&#34;&gt;int&lt;/span&gt;) &lt;span style=&#34;color:#080&#34;&gt;and&lt;/span&gt; codepage_id &amp;gt; &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; html_bytes.decode(codec, errors=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;strict&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;except&lt;/span&gt; (&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;LookupError&lt;/span&gt;, &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;UnicodeDecodeError&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; html_bytes.decode(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;, errors=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;strict&amp;#34;&lt;/span&gt;)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id=&#34;why-i-prefer-fail-loud-here&#34;&gt;Why I prefer fail-loud here&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;errors=&amp;quot;replace&amp;quot;&lt;/code&gt; keeps jobs moving, but it can hide real data corruption.&lt;/p&gt;
&lt;p&gt;For low-stakes preview features, that may be acceptable.
For transactional extraction (orders, invoices, legal, shipping), silent corruption is usually worse than an explicit failure.&lt;/p&gt;
&lt;p&gt;Use this decision rule:&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Use case&lt;/th&gt;
          &lt;th&gt;Policy&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Preview/search UX&lt;/td&gt;
          &lt;td&gt;Best-effort can be acceptable with clear flags&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Transactional extraction&lt;/td&gt;
          &lt;td&gt;Strict decode + fail loud&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Mixed systems&lt;/td&gt;
          &lt;td&gt;Strict on extraction path, best-effort on preview path&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id=&#34;how-to-roll-this-out-safely&#34;&gt;How to roll this out safely&lt;/h3&gt;
&lt;p&gt;Keep blast radius low:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Change only the failing decode path first.&lt;/li&gt;
&lt;li&gt;Validate on a representative dataset, not one sample file.&lt;/li&gt;
&lt;li&gt;Leave unrelated paths untouched until evidence says otherwise.&lt;/li&gt;
&lt;li&gt;Expand strict policy incrementally.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This gives reliability without destabilizing the rest of the ingestion stack.&lt;/p&gt;
&lt;h3 id=&#34;observability-that-makes-this-easier-next-time&#34;&gt;Observability that makes this easier next time&lt;/h3&gt;
&lt;p&gt;Log these fields per message:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Source file&lt;/li&gt;
&lt;li&gt;Content source used (HTML or plain text)&lt;/li&gt;
&lt;li&gt;Whether encoding hint was found&lt;/li&gt;
&lt;li&gt;Selected codec&lt;/li&gt;
&lt;li&gt;Whether fallback was used&lt;/li&gt;
&lt;li&gt;Result of decoding (success, fallback, manual review, fail)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With this, “random extraction quality” turns into a clear ingestion signal.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>How to Mount a BitLocker Drive on Linux at Login</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2026/02/how-to-mount-a-bitlocker-drive-on-linux-at-login/"/>
      <id>https://www.endpointdev.com/blog/2026/02/how-to-mount-a-bitlocker-drive-on-linux-at-login/</id>
      <published>2026-02-12T00:00:00+00:00</published>
      <author>
        <name>Seth Jensen</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2026/02/how-to-mount-a-bitlocker-drive-on-linux-at-login/leaf-in-water.webp&#34; alt=&#34;A single tree shoot with a few leaves pokes out from a lightly rippling water which reflects late afternoon sun&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2025. --&gt;
&lt;p&gt;I recently reinstalled Linux on my desktop machine alongside Windows on a separate SSD. I also have a 3TB hard drive for backups and slower storage, which I formatted on Windows. I didn&amp;rsquo;t manually enable any encryption, but I got the following error when trying to mount it directly:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mount: /mnt/&amp;lt;mydrive&amp;gt;: unknown filesystem type &amp;#39;BitLocker&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Looks encrypted to me! After some googling, I found a common solution is to use dislocker to decrypt the drive.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I was confused because my drive wasn&amp;rsquo;t encrypted, but the filesystem type was still &amp;ldquo;BitLocker&amp;rdquo;. My guess is that Windows encrypted the drive by default, though as you&amp;rsquo;ll see soon, it uses a &amp;ldquo;clear key,&amp;rdquo; meaning it&amp;rsquo;s technically encrypted but the key is stored unencrypted, meaning anyone can decrypt the drive. I suppose they want me to set up encryption somehow, but since this is a home computer, I don&amp;rsquo;t mind it being unencrypted for now.&lt;/p&gt;
&lt;p&gt;If your drive is encrypted, there are a few extra steps. There&amp;rsquo;s an excellent guide on std.rocks&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; for this. I&amp;rsquo;ll also annotate the fstab-specific section so you know where to put the recovery key there.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h3 id=&#34;installing-dislocker--fuse&#34;&gt;Installing dislocker &amp;amp; FUSE&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;From this point on, all of the commands need to be run as superuser. Add &lt;code&gt;sudo&lt;/code&gt; before the command or run them from the &lt;code&gt;root&lt;/code&gt; user. And, of course, proceed with caution and read your man pages before blindly trusting me :D&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;You can find dislocker packages in the default repos for most distros, or &lt;a href=&#34;https://github.com/Aorimn/dislocker/blob/master/INSTALL.md&#34;&gt;build it from source&lt;/a&gt;. I&amp;rsquo;m running Ubuntu right now, so I just had to run:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;apt-get install dislocker ntfs-3g&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;I was able to mount the filesystem as read-only with the default &lt;code&gt;ntfs&lt;/code&gt; driver, but to write I had to install &lt;code&gt;ntfs-3g&lt;/code&gt;.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h3 id=&#34;disabling-fast-startup-in-windows&#34;&gt;Disabling Fast Startup in Windows&lt;/h3&gt;
&lt;p&gt;If Fast Startup is enabled in Windows, the drive can get corrupted or be forced into read-only mode in Linux. To prevent this, disable Fast Startup by entering the following in an administrator PowerShell:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Set-ItemProperty -Path &amp;#34;HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Power\&amp;#34; -Name &amp;#34;HiberbootEnabled&amp;#34; -Value &amp;#34;0&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can also disable it from the control panel — just search &amp;ldquo;Fast Startup&amp;rdquo;.&lt;/p&gt;
&lt;h3 id=&#34;mounting-on-login-with-fstab&#34;&gt;Mounting on login with &lt;code&gt;fstab&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;It&amp;rsquo;s well and good to mount a drive one time, but since this is a persistent volume, I want it to be mounted consistently on boot. This is where &lt;code&gt;fstab&lt;/code&gt; comes in.&lt;/p&gt;
&lt;p&gt;To find a device, run &lt;code&gt;lsblk&lt;/code&gt;, find the partition you&amp;rsquo;re looking for, and note the device name. You should see something like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sda      8:0    0 232.9G  0 disk 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;├─sda1   8:1    0     1G  0 part /boot/efi
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;└─sda2   8:2    0 231.8G  0 part /
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sdb      8:16   0   2.7T  0 disk 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;├─sdb1   8:17   0    16M  0 part 
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;└─sdb2   8:18   0   2.7T  0 part &lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In my case, I want to mount &lt;code&gt;/dev/sdb2&lt;/code&gt;. First, though, we&amp;rsquo;ll need the device&amp;rsquo;s PARTUUID. Device names like &lt;code&gt;/dev/sdb2&lt;/code&gt; can change when you add or remove disks, so referencing a UUID is recommended in fstab&amp;rsquo;s man page to make sure you&amp;rsquo;re mounting the same disk every time. To get the PARTUUID, run the following:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;blkid | sort&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here&amp;rsquo;s my output:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;/dev/sda1: UUID=&amp;#34;A1A1-A1A1&amp;#34; BLOCK_SIZE=&amp;#34;512&amp;#34; TYPE=&amp;#34;vfat&amp;#34; PARTUUID=&amp;#34;1a1a1a1a-1a1a-1a1a-1a1a-1a1a1a1a1a1a&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;/dev/sda2: UUID=&amp;#34;1a1a1a1a-1a1a-1a1a-1a1a-1a1a1a1a1a1a&amp;#34; BLOCK_SIZE=&amp;#34;4096&amp;#34; TYPE=&amp;#34;ext4&amp;#34; PARTUUID=&amp;#34;1a1a1a1a-1a1a-1a1a-1a1a-1a1a1a1a1a1a&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;/dev/sdb1: PARTLABEL=&amp;#34;Microsoft reserved partition&amp;#34; PARTUUID=&amp;#34;1a1a1a1a-1a1a-1a1a-1a1a-1a1a1a1a1a1a&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;/dev/sdb2: TYPE=&amp;#34;BitLocker&amp;#34; PARTLABEL=&amp;#34;Basic data partition&amp;#34; PARTUUID=&amp;#34;&amp;lt;mydrive-part-uuid&amp;gt;&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;You should also be able to see the UUID after running &lt;code&gt;lsblk -f&lt;/code&gt;, but for some reason, that command didn&amp;rsquo;t show me any disk information for &lt;code&gt;/dev/sdb&lt;/code&gt;. It also didn&amp;rsquo;t show me the disk&amp;rsquo;s UUID, just the PARTUUID for each partition. That worked well enough for me, but please leave a comment if you know why that&amp;rsquo;s happening.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;Open &lt;code&gt;/etc/fstab&lt;/code&gt; in your favorite editor and add the following at the end:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;/dev/disk/by-partuuid/&amp;lt;mydrive-part-uuid&amp;gt; /mnt/bitlocker  fuse.dislocker  nofail,clear-key                              0   0
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;/mnt/bitlocker/dislocker-file             /mnt/&amp;lt;mydrive&amp;gt;  ntfs            nofail,x-gvfs-show,x-gvfs-name=bitlocker-raw  0   0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I specified the &lt;code&gt;clear-key&lt;/code&gt; option which is assumed in the &lt;code&gt;dislocker&lt;/code&gt; command — the &lt;code&gt;fuse.dislocker&lt;/code&gt; filesystem type takes dislocker&amp;rsquo;s long options. If your drive is encrypted, you&amp;rsquo;ll want to replace this with &lt;code&gt;recovery-password=000000-000000-000000-000000-000000-000000-000000-000000&lt;/code&gt; (recovery password is required if TPM is enabled) or &lt;code&gt;user-password=123456&lt;/code&gt; (a PIN code here works if TPM is disabled, according to std.rocks&lt;sup id=&#34;fnref1:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt;).&lt;/p&gt;
&lt;p&gt;Now, I recommend trying to mount the drive before rebooting to avoid any startup issues if there are syntax errors or similar. After saving the file, run the following to get your &lt;code&gt;fstab&lt;/code&gt; changes into systemd:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;systemctl daemon-reload&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now you can mount the drive, then mount the &lt;code&gt;dislocker-file&lt;/code&gt; just by naming the mount points:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mount /mnt/bitlocker
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mount /mnt/mydrive&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If you see files in &lt;code&gt;/mnt/mydrive&lt;/code&gt;, hooray, it worked! Your drive should be mounted on reboot now. If not, there may be an error in your fstab file. I&amp;rsquo;d recommend trying the manual mounting method in the next section and noting at what step the failure happens.&lt;/p&gt;
&lt;h3 id=&#34;troubleshooting-manual-mounting&#34;&gt;Troubleshooting: manual mounting&lt;/h3&gt;
&lt;p&gt;I found it easier to diagnose and fix issues when I mounted manually first:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mkdir /mnt/bitlocker
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;dislocker -V /dev/sdb2 -- /mnt/bitlocker&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If your drive &lt;em&gt;is&lt;/em&gt; encrypted, dislocker should prompt you for a user or recovery password. Since mine isn&amp;rsquo;t, I instead used the default &amp;ldquo;clear key&amp;rdquo; stored on the volume. The guide on std.rocks&lt;sup id=&#34;fnref2:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; has more info for this.&lt;/p&gt;
&lt;p&gt;If you check in the &lt;code&gt;/mnt/bitlocker&lt;/code&gt; folder, you can see that there&amp;rsquo;s now a file called &lt;code&gt;dislocker-file&lt;/code&gt; there. This file is what we mount:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mount -o loop -t ntfs /mnt/bitlocker/dislocker-file /mnt/&amp;lt;mydrive&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-o loop&lt;/code&gt; says that &lt;code&gt;dislocker-file&lt;/code&gt; is a normal file you&amp;rsquo;re mounting via the loop interface&lt;/li&gt;
&lt;li&gt;&lt;code&gt;-t ntfs&lt;/code&gt; specifies that this drive is an NTFS filesystem, the default on modern Windows (post-1993, that is 😉)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After running this, you should see your files in &lt;code&gt;/mnt/&amp;lt;mydrive&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&#34;references&#34;&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/Aorimn/dislocker&#34;&gt;https://github.com/Aorimn/dislocker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;man fstab&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;&lt;a href=&#34;https://std.rocks/gnulinux_bitlocker.html&#34;&gt;https://std.rocks/gnulinux_bitlocker.html&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref1:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref2:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;

      </content>
    </entry>
  
    <entry>
      <title>Running Ruby in Podman (When rbenv install Fails on Apple Silicon)</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2026/01/ruby-in-podman-apple-silicon/"/>
      <id>https://www.endpointdev.com/blog/2026/01/ruby-in-podman-apple-silicon/</id>
      <published>2026-01-12T00:00:00+00:00</published>
      <author>
        <name>Seth Jensen</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2026/01/ruby-in-podman-apple-silicon/newish-ruins.webp&#34; alt=&#34;Amidst dry, leafless brush, a tall structure of dilapidated bricks stands, with a small archway at the bottom in the center of the image&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2025 --&gt;
&lt;p&gt;I recently worked on a legacy Ruby backend app which hadn&amp;rsquo;t been changed for years. A Ruby development environment used to be provided by &lt;a href=&#34;/expertise/version-control-devcamps/&#34;&gt;DevCamps&lt;/a&gt; for this site, but over the years we stopped using Ruby other than this small backend, so we also stopped using camps.&lt;/p&gt;
&lt;p&gt;The frontend development now uses its own local dev server, so I decided to do the same for the backend by running the Unicorn server locally. I&amp;rsquo;m on a MacBook M2, so I should just be able to install &lt;code&gt;rbenv&lt;/code&gt; using Homebrew. Easy, right?&lt;/p&gt;
&lt;p&gt;Unfortunately not. I had a great deal of trouble getting any version of Ruby to run natively on my MacBook M2, so I eventually resorted to using Podman. You can &lt;a href=&#34;#third-try-podman&#34;&gt;skip to the end for my solution&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;first-try-native-rbenv&#34;&gt;First try: native rbenv&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;rbenv&lt;/code&gt; installation went just fine:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;brew install rbenv ruby-build&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And after adding &lt;code&gt;eval &amp;quot;$(rbenv init -)&amp;quot;&lt;/code&gt; to my &lt;code&gt;~/.zshrc&lt;/code&gt; file, I tried to install the old version of Ruby:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;rbenv install 2.4.10&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;However, this returned a &lt;code&gt;BUILD FAILED&lt;/code&gt; message. Strangely, it also failed with a new version of Ruby (3.3.10). I tried debugging for a while before giving up — I figured this project isn&amp;rsquo;t worth multiple hours of getting a local Ruby installation to work (despite my obstinate attempts to do so).&lt;/p&gt;
&lt;h3 id=&#34;second-try-mise&#34;&gt;Second try: mise&lt;/h3&gt;
&lt;p&gt;Several people on forums &amp;amp; blogs recommended using mise, a language-agnostic version manager. After installing it with Homebrew, I ran:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mise use --global ruby@2.4.10&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This failed too! There was a good explanation, though: older Ruby versions don&amp;rsquo;t run on ARM processors. Of course, that makes sense. So I just needed to install a newer version from after Apple silicon was supported:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mise use --global ruby@3.3.10&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;However, this failed as well, just like rbenv:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mise ERROR Failed to install core:ruby@3.3.10:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;   0: ~/Library/Caches/mise/ruby/ruby-build/bin/ruby-build exited with non-zero status: exit code 1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I&amp;rsquo;m sure there is a way to get Ruby running through a version manager on Apple silicon; I found several &lt;a href=&#34;https://gorails.com/setup/macos/15-sequoia&#34;&gt;tutorials&lt;/a&gt; claiming you can just run these commands as normal and it&amp;rsquo;ll work. However, after spending longer than I&amp;rsquo;d like to admit, I was completely unable to do so despite trying many fixes found online.&lt;/p&gt;
&lt;p&gt;This may be a unique problem on my machine, or I may be missing something. But the fact was, I&amp;rsquo;d waited through about a dozen failed Ruby installs (which take a long time!) and I just needed the app to work now. So I moved to a third option: run Ruby in a container.&lt;/p&gt;
&lt;h3 id=&#34;third-try-podman&#34;&gt;Third try: Podman&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://podman.io/&#34;&gt;Podman&lt;/a&gt; is a fully open-source containerization system which can run Dockerfiles and docker-compose.yml files. The app is a lightweight Sinatra backend with Unicorn as a server, and I just needed to add CloudFlare Turnstile to reduce bot traffic.&lt;/p&gt;
&lt;p&gt;The app uses two main files: &lt;code&gt;unicorn.rb&lt;/code&gt; (which holds the configuration for the Unicorn server) and &lt;code&gt;config.ru&lt;/code&gt; (which defines and runs the Sinatra app). The command to start the app is pretty simple: &lt;code&gt;bundle exec unicorn -c unicorn.rb config.ru&lt;/code&gt;. I also set up two files for the container: &lt;code&gt;Containerfile&lt;/code&gt; and &lt;code&gt;container-compose.yml&lt;/code&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: Podman does find files named Dockerfile and docker-compose.yml, but here I&amp;rsquo;m using Podman&amp;rsquo;s convention of Containerfile and container-compose.yml, which takes precedence if both are present in the folder.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;To run this, I just needed to set up the environment in my &lt;code&gt;Containerfile&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&#34;containerfile&#34;&gt;&lt;code&gt;Containerfile&lt;/code&gt;&lt;/h4&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-containerfile&#34; data-lang=&#34;containerfile&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;FROM&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;ruby:2.4.10&lt;/span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;WORKDIR&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;/usr/src/app&lt;/span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;COPY&lt;/span&gt; . .&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;RUN&lt;/span&gt; mkdir -p /var/log &amp;amp;&amp;amp; &lt;span style=&#34;color:#04d;background-color:#fff0f0&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#04d;background-color:#fff0f0&#34;&gt;&lt;/span&gt; gem i bundler &amp;amp;&amp;amp; &lt;span style=&#34;color:#04d;background-color:#fff0f0&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#04d;background-color:#fff0f0&#34;&gt;&lt;/span&gt; bundle install&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;EXPOSE&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;8080&lt;/span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;CMD&lt;/span&gt; [&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;bundle&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;exec&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;unicorn&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;-c&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;unicorn.rb&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;config.ru&amp;#34;&lt;/span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It&amp;rsquo;s a pretty simple setup:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;set the working directory&lt;/li&gt;
&lt;li&gt;copy files from the source directory into the container&lt;/li&gt;
&lt;li&gt;create the log directory defined in &lt;code&gt;unicorn.rb&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;install bundler&lt;/li&gt;
&lt;li&gt;install gems from Gemfile.lock&lt;/li&gt;
&lt;li&gt;expose the port defined in &lt;code&gt;unicorn.rb&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;define the server start command using a &lt;code&gt;CMD&lt;/code&gt; instruction&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;container-composeyml&#34;&gt;&lt;code&gt;container-compose.yml&lt;/code&gt;&lt;/h4&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;services&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;  &lt;/span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;backend&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;build&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;        &lt;/span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;context&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;.&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;        &lt;/span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;dockerfile&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;Containerfile&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;volumes&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;        &lt;/span&gt;- .:/usr/src/app&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;    &lt;/span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;ports&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;        &lt;/span&gt;- &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;8080:8080&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;volumes&lt;/code&gt; section means that changes on the host machine will be propogated to the container (and vice versa). This means you don&amp;rsquo;t need to rebuild the container when you change the app, instead you can use a change-watching Gem like &lt;a href=&#34;https://github.com/alexch/rerun&#34;&gt;rerun&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: You can run this without a compose file, the compose file just stores the volume &amp;amp; port information so you can run it more easily.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;podman run -d --name mybackend -v .:/usr/src/app -p 8080:8080 &amp;lt;image_id&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/blockquote&gt;
&lt;h4 id=&#34;minimal-unicornrb-configuration-file&#34;&gt;Minimal &lt;code&gt;unicorn.rb&lt;/code&gt; configuration file&lt;/h4&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-rb&#34; data-lang=&#34;rb&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;listen &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;0.0.0.0:8080&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;working_directory &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;/usr/src/app&amp;#34;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;0.0.0.0&lt;/code&gt; to connect to the container&amp;rsquo;s network interface&lt;/li&gt;
&lt;li&gt;Define our listening port&lt;/li&gt;
&lt;li&gt;Set the working directory to the same value as in our &lt;code&gt;Containerfile&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;Note: When we forward port 8080 to our Podman container, that traffic arrives on the container&amp;rsquo;s network interface, not on its internal loopback (localhost). If your app only listens on localhost, it&amp;rsquo;s essentially saying &amp;ldquo;I only accept connections from myself,&amp;rdquo; so the forwarded traffic gets rejected. By binding to 0.0.0.0, your app listens on all interfaces, which includes the one Podman uses to route external traffic into the container.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h4 id=&#34;simple-configru&#34;&gt;Simple &lt;code&gt;config.ru&lt;/code&gt;&lt;/h4&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;require &amp;#39;sinatra&amp;#39;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;get &amp;#39;/&amp;#39; do
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &amp;#34;Hello from Sinatra in Podman!\n&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;end
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;run Sinatra::Application&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is also very simple:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;After importing sinatra, define a GET route for the &lt;code&gt;/&lt;/code&gt; location (so this will be accessed from &lt;code&gt;localhost:8080/&lt;/code&gt; on the host machine)&lt;/li&gt;
&lt;/ul&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ podman build .
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ podman run -p 8080:8080 &amp;lt;image-id&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This returned a successful response on my host machine:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ curl localhost:8080
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Hello from Sinatra in Podman!&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When the app failed to run, it kept trying to restart until I moved a .env file into place and it worked. That shows that the volume is working correctly.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not a Ruby expert, but with this setup I was able to convert the JavaScript code for Turnstile to Ruby and get it working!&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Quick and Dirty Admin Pages with Rails scaffold_controller</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/12/quick-admin-pages-with-rails-scaffold/"/>
      <id>https://www.endpointdev.com/blog/2025/12/quick-admin-pages-with-rails-scaffold/</id>
      <published>2025-12-29T00:00:00+00:00</published>
      <author>
        <name>Couragyn Chretien</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2025/12/quick-admin-pages-with-rails-scaffold/fire-escape-looking-down.webp&#34; alt=&#34;Through the metal grid of a fire escape step, cars and a street far below are visible. On the step are two strong intersecting shadows from the top left, one going rightward and one going downward and to the left&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen --&gt;
&lt;p&gt;You&amp;rsquo;re building a Rails application, and you&amp;rsquo;ve reached that point where you or your team needs a simple way to manage data. You need an admin interface. You&amp;rsquo;ve heard of heavy-weight solutions like ActiveAdmin or Rails Admin, but they feel like overkill for adding a few new categories or moderating user posts. You don&amp;rsquo;t want to spend an afternoon configuring a gem. You need something now.&lt;/p&gt;
&lt;p&gt;Luckily Rails has a built-in lightning-fast way to generate a fully functional admin CRUD interface for any existing model. Meet the often-overlooked scaffold_controller generator. It&amp;rsquo;s the secret weapon for building &amp;ldquo;quick and dirty&amp;rdquo; internal tools.&lt;/p&gt;
&lt;h3 id=&#34;what-is-scaffold_controller-and-why-use-it&#34;&gt;What Is scaffold_controller and Why Use It?&lt;/h3&gt;
&lt;p&gt;When you run &lt;code&gt;rails generate scaffold Post&lt;/code&gt;, it creates everything: the model, migration, controller, and views. The scaffold_controller generator does only half of that, creating only the controller and views (as long as the model already exists).&lt;/p&gt;
&lt;p&gt;This is perfect for creating a separate admin namespace because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;It&amp;rsquo;s Non-Destructive:&lt;/strong&gt; Your existing, user-facing PostsController remains untouched&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It&amp;rsquo;s Fast:&lt;/strong&gt; One command gives you a complete set of CRUD actions and ERB views&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;It&amp;rsquo;s Simple:&lt;/strong&gt; No new gems, dependencies, or complex configurations to learn&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;lets-build-an-admin-interface-for-a-product-model&#34;&gt;Let&amp;rsquo;s Build an Admin Interface for a Product Model&lt;/h3&gt;
&lt;p&gt;Imagine you have a Product model in your application. You have a ProductsController for your main site, but now you need an admin area to create, edit, and delete products.&lt;/p&gt;
&lt;h4 id=&#34;step-1-create-the-admin-namespace-and-route&#34;&gt;Step 1: Create the Admin Namespace and Route&lt;/h4&gt;
&lt;p&gt;First, we&amp;rsquo;ll set up a routing namespace to keep our admin logic separate. This will put our URLs in this format: &lt;code&gt;/admin/products&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Open your &lt;code&gt;config/routes.rb&lt;/code&gt; file and add:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# config/routes.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Rails&lt;/span&gt;.application.routes.draw &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;# ... existing routes ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  namespace &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:admin&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;do&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    resources &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:products&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Running &lt;code&gt;rails routes&lt;/code&gt; will now show new routes like &lt;code&gt;admin_products_path&lt;/code&gt;, &lt;code&gt;edit_admin_product_path&lt;/code&gt;, etc.&lt;/p&gt;
&lt;h4 id=&#34;step-2-generate-the-scaffold-controller&#34;&gt;Step 2: Generate the Scaffold Controller&lt;/h4&gt;
&lt;p&gt;Now for the magic. In your terminal, run &lt;code&gt;rails generate scaffold_controller Admin::Product&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Important notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The namespace (Admin::) must match the one you used in your routes&lt;/li&gt;
&lt;li&gt;The model name (Product) must be singular, just like with a regular scaffold&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This one command generates several files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Controller:&lt;/strong&gt; &lt;code&gt;app/controllers/admin/products_controller.rb&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Views:&lt;/strong&gt; A full set of ERB templates in &lt;code&gt;app/views/admin/products/&lt;/code&gt; (&lt;code&gt;index.html.erb&lt;/code&gt;, &lt;code&gt;show.html.erb&lt;/code&gt;, &lt;code&gt;new.html.erb&lt;/code&gt;, &lt;code&gt;edit.html.erb&lt;/code&gt;, &lt;code&gt;_form.html.erb&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;step-3-update-the-generated-controller&#34;&gt;Step 3: Update the Generated Controller&lt;/h4&gt;
&lt;p&gt;The generated controller is a great start but it needs one crucial tweak. It doesn&amp;rsquo;t know about our namespace, so it will look for the Product model in the wrong place. The key change is ensuring every reference to the model is to Product, not Admin::Product.&lt;/p&gt;
&lt;p&gt;Open &lt;code&gt;products_controller.rb&lt;/code&gt; and change the class definition and any model references. Here&amp;rsquo;s a simplified example of what it should look like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# app/controllers/admin/products_controller.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;module&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Admin&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;ProductsController&lt;/span&gt; &amp;lt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ApplicationController&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    before_action &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:set_product&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;only&lt;/span&gt;: [&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:show&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:edit&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:update&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:destroy&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# GET /admin/products&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;index&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#33b&#34;&gt;@products&lt;/span&gt; = &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Product&lt;/span&gt;.all
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# ... other actions ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080&#34;&gt;private&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;set_product&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#33b&#34;&gt;@product&lt;/span&gt; = &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Product&lt;/span&gt;.find(params[&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:id&lt;/span&gt;])
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;product_params&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        params.require(&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:product&lt;/span&gt;).permit(&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:name&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:description&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:price&lt;/span&gt;, &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:stock&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id=&#34;step-4-add-a-link-and-access-control&#34;&gt;Step 4: Add a Link and Access Control&lt;/h4&gt;
&lt;p&gt;Your admin interface is now live at &lt;code&gt;http://localhost:3000/admin/products&lt;/code&gt;. Before you deploy you must add some form of access control. This can be as simple as a basic HTTP authentication check in your Admin::ProductsController.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# app/controllers/admin/products_controller.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;module&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Admin&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;ProductsController&lt;/span&gt; &amp;lt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ApplicationController&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    http_basic_authenticate_with &lt;span style=&#34;color:#038&#34;&gt;name&lt;/span&gt;: &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ENV&lt;/span&gt;[&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;ADMIN_USER&amp;#39;&lt;/span&gt;], &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;password&lt;/span&gt;: &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ENV&lt;/span&gt;[&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;ADMIN_PASSWORD&amp;#39;&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# ... rest of the controller code ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It would also be good to add a link in your navigation for easy access:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;%= link_to &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Admin Products&amp;#34;&lt;/span&gt;, admin_products_path &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Rails&lt;/span&gt;.env.development? %&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id=&#34;when-should-you-use-this-method&#34;&gt;When Should You Use This Method?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Ideal for:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Internal tools, administrative, and support functions&lt;/li&gt;
&lt;li&gt;Rapid prototyping&lt;/li&gt;
&lt;li&gt;Simple CRUD needs where a full-blown admin gem is overkill&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Not ideal for:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Customer-facing admin panels that need complex filtering, dashboards, or custom forms&lt;/li&gt;
&lt;li&gt;Applications where you need fine-grained, role-based permissions&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;The scaffold_controller generator is a fantastic piece of Rails tooling that is often overlooked. It embodies the Rails philosophy of providing quick solutions for common problems. In just a few minutes you can have a functional, separate admin area for any model without the overhead of a new gem or the risk of breaking your public-facing code. So next time you need a simple data management interface, skip the gem research and let scaffold_controller do the heavy lifting for you.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>End Point Is a Top Cloud Consulting Company on Techreviewer</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/12/techreviewer-co-recognition/"/>
      <id>https://www.endpointdev.com/blog/2025/12/techreviewer-co-recognition/</id>
      <published>2025-12-08T00:00:00+00:00</published>
      <author>
        <name>Juan Pablo Ventoso</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2025/12/techreviewer-co-recognition/clouds-with-badge.webp&#34; alt=&#34;View of deep, sunset-lit clouds over a mountain valley with a large lake, which also reflects the orange sunset.&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2025. --&gt;
&lt;p&gt;We recently received some good news: Techreviewer.co included us in their list of &lt;a href=&#34;https://techreviewer.co/top-cloud-consulting-companies&#34;&gt;top cloud migration consulting companies for 2025&lt;/a&gt;! While we don’t usually dwell on rankings, it’s always rewarding when thoughtful, steady work over many years gets noticed.&lt;/p&gt;
&lt;p&gt;Cloud adoption has been a major part of our engineering efforts since it became &amp;ldquo;a thing&amp;rdquo;: For many of our clients, moving to the cloud isn’t just a technology upgrade, but a strategic shift that affects performance, costs, security, and long-term maintainability. Helping organizations navigate that transition in a careful and grounded way is something we’ve been doing since long before “cloud migration” became a buzzword.&lt;/p&gt;
&lt;h3 id=&#34;our-approach-to-cloud-migration&#34;&gt;Our approach to cloud migration&lt;/h3&gt;
&lt;p&gt;At End Point Dev, we’ve always taken a pragmatic path: understand the real constraints, design strong systems that will age well, and keep things secure and predictable. Cloud work touches many layers, and affects DevOps, databases, applications, and ongoing operations. Our team of software engineering and infrastructure experts supports our clients through that entire lifecycle.&lt;/p&gt;
&lt;p&gt;Our day-to-day cloud work includes, but is not limited to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Planning and executing migrations to AWS, Azure, Google Cloud, and others&lt;/li&gt;
&lt;li&gt;Automating deployments and CI/CD pipelines for safer, faster releases&lt;/li&gt;
&lt;li&gt;Scaling and optimizing PostgreSQL and other databases&lt;/li&gt;
&lt;li&gt;Designing secure and resilient infrastructure that is ready to keep growing&lt;/li&gt;
&lt;li&gt;Building custom software that fits cleanly into modern cloud ecosystems&lt;/li&gt;
&lt;li&gt;Supporting and operating hosted environments around the clock&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Using containerization and orchestration tools like Docker and Kubernetes to improve reliability is key, when the project and client needs requires it.&lt;/p&gt;
&lt;h3 id=&#34;why-this-matters&#34;&gt;Why this matters&lt;/h3&gt;
&lt;p&gt;This acknowledgment isn’t something we chased, but it does capture what we care deeply about: Having a staff that brings a good mix of application knowledge and infrastructure engineering, and prioritizing client trust and long-term support.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Our cloud migration frameworks meet the highest standards of quality and reliability&lt;/li&gt;
&lt;li&gt;Our DevOps, infrastructure, and database expertise place us among industry leaders&lt;/li&gt;
&lt;li&gt;Our client-centered approach sets us apart in a rapidly evolving landscape&lt;/li&gt;
&lt;li&gt;Our long-term support model delivers value far beyond the initial project&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A lot of what we do happens behind the scenes, especially in maintenance, monitoring, and incident response. So it’s nice for us to see that steady reliability reflected in external evaluations.&lt;/p&gt;
&lt;h3 id=&#34;looking-ahead&#34;&gt;Looking ahead&lt;/h3&gt;
&lt;p&gt;The landscape of cloud computing moves quickly, and we like keeping pace. In the coming year, we will be focused on strengthening our multi-cloud capabilities, expanding our tooling around automation and observability, deepening AI integration within our daily processes, and exploring new AI-driven approaches to optimization and scaling.&lt;/p&gt;
&lt;p&gt;We extend our sincere gratitude to our clients, partners, and team members. Every project, migration, deployment, and late-night incident response has helped shape End Point Dev’s reputation for reliability and excellence. This recognition is a tribute to your trust and collaboration. We look forward to continuing to support organizations worldwide with solutions that are robust, secure, and thoughtfully engineered.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Using ActiveSupport::​ExecutionContext to improve Rails logging</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/12/execution-context-to-improve-logging/"/>
      <id>https://www.endpointdev.com/blog/2025/12/execution-context-to-improve-logging/</id>
      <published>2025-12-05T00:00:00+00:00</published>
      <author>
        <name>Couragyn Chretien</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2025/12/execution-context-to-improve-logging/northern-lights-over-utah-valley.webp&#34; alt=&#34;The northern lights glow red above a mountain valley filled with city lights&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2025. --&gt;
&lt;p&gt;If you’ve ever tried to debug a complex user workflow in a modern Rails application, you know how difficult it can be. A single web request can spawn multiple background jobs, Turbo Stream updates, and a flurry of database queries. Answering the  question &amp;ldquo;What was this user doing?&amp;rdquo; should be simple, but tracing the flow of events through a web of log files can be difficult and time consuming.&lt;/p&gt;
&lt;p&gt;You can add custom log tags but that gets tedious and messy fast. Fortunately, Rails has a powerful, under-documented feature designed specifically to address this problem: ActiveSupport::ExecutionContext.&lt;/p&gt;
&lt;p&gt;In this post, we&amp;rsquo;ll walk through what ExecutionContext is and how you can use it to add meaningful structure to your logs, making debugging a much more straightforward task.&lt;/p&gt;
&lt;h3 id=&#34;the-problem-a-tangled-web-of-logs&#34;&gt;The Problem: A Tangled Web of Logs&lt;/h3&gt;
&lt;p&gt;Imagine a user places an order on your site. The &lt;code&gt;OrdersController#create&lt;/code&gt; action fires, which then enqueues a &lt;code&gt;ReceiptJob&lt;/code&gt; and an &lt;code&gt;InventoryUpdateJob&lt;/code&gt;. The controller also renders a Turbo Stream to update the UI. You have at least four separate units of work: the HTTP request and three background tasks.&lt;/p&gt;
&lt;p&gt;Now, if the &lt;code&gt;InventoryUpdateJob&lt;/code&gt; fails, your log might show an exception, but it won&amp;rsquo;t immediately tell you which user&amp;rsquo;s order triggered it. You&amp;rsquo;re left grepping for job IDs or tracing timestamps. ExecutionContext solves this problem by providing a shared context that is automatically shared across these different units of work.&lt;/p&gt;
&lt;h3 id=&#34;how-executioncontext-works&#34;&gt;How ExecutionContext Works&lt;/h3&gt;
&lt;p&gt;Think of ActiveSupport::ExecutionContext as a container for data that automatically gets passed along. When you store something in it during a web request, that data is bundled up and made available in any background jobs you start from that request without you having to manually send it.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s important to note that while ActiveSupport::ExecutionContext values are available within the same process (e.g., during the web request), they don&amp;rsquo;t automatically serialize and propagate to background jobs. For the context to be available in jobs, you&amp;rsquo;ll need to explicitly pass relevant values as job arguments. In our example, since we&amp;rsquo;re already passing the order object to both jobs, we can access &lt;code&gt;order.user_id&lt;/code&gt; and &lt;code&gt;order.id&lt;/code&gt; directly in the job. For contexts without such natural carriers consider extracting the relevant values and passing them explicitly as additional job arguments.&lt;/p&gt;
&lt;p&gt;This is the magic that allows you to trace a chain of events.&lt;/p&gt;
&lt;h3 id=&#34;a-practical-example-tracing-an-order&#34;&gt;A Practical Example: Tracing an Order&lt;/h3&gt;
&lt;p&gt;Let&amp;rsquo;s implement a solution for the ordering scenario. Our goal is to tag every log line related to this specific order with the user&amp;rsquo;s ID and the order ID.&lt;/p&gt;
&lt;p&gt;First, we&amp;rsquo;ll set the context in an &lt;code&gt;around_action&lt;/code&gt; in our &lt;code&gt;ApplicationController&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# app/controllers/application_controller.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;ApplicationController&lt;/span&gt; &amp;lt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActionController&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Base&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  around_action &lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:set_execution_context&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080&#34;&gt;private&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;set_execution_context&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# We only set the context if a user is logged in.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; current_user
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActiveSupport&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ExecutionContext&lt;/span&gt;[&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:user_id&lt;/span&gt;] = current_user.id
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#888&#34;&gt;# We can also trace requests&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActiveSupport&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ExecutionContext&lt;/span&gt;[&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:request_id&lt;/span&gt;] = request.request_id
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;yield&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;ensure&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Ensure the context is cleared after the request is done.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActiveSupport&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ExecutionContext&lt;/span&gt;.clear
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# app/controllers/orders_controller.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;OrdersController&lt;/span&gt; &amp;lt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ApplicationController&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;create&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#33b&#34;&gt;@order&lt;/span&gt; = current_user.orders.create!(order_params)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Update the context with the real order_id&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActiveSupport&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ExecutionContext&lt;/span&gt;[&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:order_id&lt;/span&gt;] = &lt;span style=&#34;color:#33b&#34;&gt;@order&lt;/span&gt;.id
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ReceiptJob&lt;/span&gt;.perform_later(&lt;span style=&#34;color:#33b&#34;&gt;@order&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;InventoryUpdateJob&lt;/span&gt;.perform_later(&lt;span style=&#34;color:#33b&#34;&gt;@order&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    respond_to &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;do&lt;/span&gt; |&lt;span style=&#34;color:#038&#34;&gt;format&lt;/span&gt;|
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#038&#34;&gt;format&lt;/span&gt;.turbo_stream
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Because we used &lt;code&gt;perform_later&lt;/code&gt;, Active Job will automatically serialize our &lt;code&gt;ExecutionContext&lt;/code&gt; (containing &lt;code&gt;user_id&lt;/code&gt; and &lt;code&gt;order_id&lt;/code&gt;) and make it available when the job runs.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s look at the &lt;code&gt;InventoryUpdateJob&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# app/jobs/inventory_update_job.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;InventoryUpdateJob&lt;/span&gt; &amp;lt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ApplicationJob&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;perform&lt;/span&gt;(order)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    logger.info &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Updating inventory for order&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# ... your business logic ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id=&#34;making-the-context-visible-in-logs&#34;&gt;Making the Context Visible in Logs&lt;/h3&gt;
&lt;p&gt;Setting the context is only half the battle; we also need to see it in our logs. We can do this by customizing Rails&amp;rsquo;s log formatter.&lt;/p&gt;
&lt;p&gt;Here’s a simple formatter that appends the execution context to every log line:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# config/initializers/log_formatting.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;ContextAwareFormatter&lt;/span&gt; &amp;lt; &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Logger&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Formatter&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;call&lt;/span&gt;(severity, time, progname, msg)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    context = &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActiveSupport&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ExecutionContext&lt;/span&gt;.to_h
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    tags = context.map { |key, value| &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;key&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;value&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt; }.join(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34; &amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    tags = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;[&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;tags&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;] &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;unless&lt;/span&gt; tags.empty?
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;time.utc.iso8601(&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;3&lt;/span&gt;)&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;severity&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt; #&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Process&lt;/span&gt;.pid&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;tags&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}#{&lt;/span&gt;msg2str(msg)&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#04d;background-color:#fff0f0&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Apply it to the Rails logger&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Rails&lt;/span&gt;.logger.formatter = &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ContextAwareFormatter&lt;/span&gt;.new&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With this in place, a log line from our &lt;code&gt;InventoryUpdateJob&lt;/code&gt; might now look like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;2025-12-01T15:33:01.123 INFO #123 [user_id=1001 order_id=998842] Updating inventory for order&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If that job fails, the stack trace will be prefixed with the same &lt;code&gt;[user_id=1001 order_id=998842]&lt;/code&gt; context. You can instantly see which user and order were affected.&lt;/p&gt;
&lt;h3 id=&#34;going-beyond-tagging-database-queries&#34;&gt;Going Beyond: Tagging Database Queries&lt;/h3&gt;
&lt;p&gt;One of the most powerful applications is tagging database queries. This is incredibly useful for identifying expensive queries related to a specific user in a production environment.&lt;/p&gt;
&lt;p&gt;You can subscribe to the SQL event and include the context in the log:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# config/initializers/log_formatting.rb&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActiveSupport&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Notifications&lt;/span&gt;.subscribe(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;sql.active_record&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;do&lt;/span&gt; |event|
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  context = &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ActiveSupport&lt;/span&gt;::&lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;ExecutionContext&lt;/span&gt;.to_h
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;next&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; context.empty?
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  payload = event.payload
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  sql_with_context = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;/* &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;context.map &lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt; |k, v| &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;k&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;:&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;v&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;.join(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;, &amp;#39;&lt;/span&gt;)&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt; */ &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;payload[&lt;span style=&#34;color:#a60;background-color:#fff0f0&#34;&gt;:sql&lt;/span&gt;]&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#036;font-weight:bold&#34;&gt;Rails&lt;/span&gt;.logger.debug(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;SQL: &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;#{&lt;/span&gt;sql_with_context&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;end&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here is what that log line would look like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;DEBUG -- : SQL: /* user_id:1001, order_id:998842 */ SELECT &amp;#34;orders&amp;#34;.* FROM &amp;#34;orders&amp;#34; WHERE &amp;#34;orders&amp;#34;.&amp;#34;user_id&amp;#34; = $1 LIMIT $2  [[&amp;#34;user_id&amp;#34;, 1001], [&amp;#34;LIMIT&amp;#34;, 1]]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The SQL notification subscriber will fire on every database query which in production could introduce noticeable overhead. Consider wrapping this functionality in a &lt;code&gt;Rails.env.development?&lt;/code&gt; check or using feature flags to control its activation. Be mindful of this trade-off when adding context to high-volume SQL logging.&lt;/p&gt;
&lt;h3 id=&#34;a-stitch-in-time-saves-nine&#34;&gt;A Stitch in Time Saves Nine&lt;/h3&gt;
&lt;p&gt;ActiveSupport::ExecutionContext is a robust solution to a common problem in modern, event-driven Rails applications. It provides a clean, built-in mechanism for propagating context, which leads to more debuggable and observable systems.&lt;/p&gt;
&lt;p&gt;The next time you find yourself lost in a sea of log files, remember this tool. By adding a few lines of code to set the context and customizing your log formatter, you can transform a chaotic log file into a well-organized story of what your application is doing for each and every user.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Build a Smarter Telegram Bot: Integrating a RAG Pipeline for FAQ Answering</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/12/telegram-bot-rag-pipeline/"/>
      <id>https://www.endpointdev.com/blog/2025/12/telegram-bot-rag-pipeline/</id>
      <published>2025-12-01T00:00:00+00:00</published>
      <author>
        <name>Bimal Gharti Magar</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2025/12/telegram-bot-rag-pipeline/mountains-and-clouds.webp&#34; alt=&#34;A black-and-white image of clouds hanging over a sharp mountain, jutting from the bottom right of the image, and a longer mountain ridge further back in the bottom left of the image.&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2025. --&gt;
&lt;p&gt;In this post, we will show you how to build an intelligent FAQ bot using Python and the Telegram Bot API. We&amp;rsquo;ll go beyond simple commands by integrating a Retrieval-Augmented Generation (RAG) pipeline with LangChain.&lt;/p&gt;
&lt;p&gt;This RAG pipeline lets our bot pull information from a custom knowledge base (in our case, a simple &lt;code&gt;faqs.json&lt;/code&gt; file) and use a local Large Language Model (LLM) through Ollama to generate accurate answers. The best part? This approach (which works great with interfaces like Open WebUI) gives you full control over your models and data with zero API costs.&lt;/p&gt;
&lt;h3 id=&#34;what-is-telegram&#34;&gt;What is Telegram?&lt;/h3&gt;
&lt;p&gt;You&amp;rsquo;ve probably heard of &lt;a href=&#34;https://telegram.org/&#34;&gt;Telegram&lt;/a&gt;—it&amp;rsquo;s a popular, cloud-based instant messaging app. It’s fast, works everywhere (mobile, web, and desktop), and has powerful features like huge group chats and easy file sharing.&lt;/p&gt;
&lt;p&gt;One of its most powerful features for developers is the Telegram Bot API, an open platform that allows anyone to build and integrate automated applications (like ours!) directly into the chat interface.&lt;/p&gt;
&lt;h3 id=&#34;a-warning-on-privacy-and-encryption&#34;&gt;A Warning on Privacy and Encryption&lt;/h3&gt;
&lt;p&gt;Before we build our bot, it is critical to understand how Telegram handles encryption, as it directly impacts user privacy.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Cloud Chats (The Default)&lt;/strong&gt;: All standard chats, group chats, and all bot interactions are &amp;ldquo;Cloud Chats.&amp;rdquo; These use server-client encryption. This means your messages are encrypted between your device and Telegram&amp;rsquo;s servers, and then stored (encrypted) on their servers. This is what allows you to access your chat history from any device. However, Telegram itself holds the encryption keys and can access this data.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secret Chats (Manual)&lt;/strong&gt;: Telegram also offers &amp;ldquo;Secret Chats,&amp;rdquo; which are end-to-end encrypted (E2EE). In this mode, only you and the recipient can read the messages. Telegram has no access. However, bots cannot operate in Secret Chats.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This means that any message a user sends to our bot is a &amp;ldquo;Cloud Chat&amp;rdquo; and is &lt;em&gt;not end-to-end encrypted&lt;/em&gt;. The data is accessible to Telegram and will be processed in plain text by our bot.py script on our server.&lt;/p&gt;
&lt;p&gt;For this reason, you should never build a bot that asks for or encourages users to send sensitive private data such as passwords, financial information, or social security numbers. Always treat bot conversations as non-private.&lt;/p&gt;
&lt;h3 id=&#34;what-is-retrieval-augmented-generation-rag&#34;&gt;What is Retrieval-Augmented Generation (RAG)?&lt;/h3&gt;
&lt;p&gt;Retrieval-Augmented Generation (RAG) is a technique that makes Large Language Models (LLMs) smarter by connecting them to external, private knowledge.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;The Problem: An LLM like llama3 only knows the information it was trained on. It has no access to your company&amp;rsquo;s internal FAQs, new documents, or any private data.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;The Solution (RAG): RAG solves this in two steps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Retrieve: When you ask a question, the system first retrieves relevant information from your own knowledge base (for us, our faqs.json file).&lt;/li&gt;
&lt;li&gt;Augment: It then augments the LLM&amp;rsquo;s prompt by pasting that retrieved information in as context, along with your original question.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In short, instead of just asking the bot &amp;ldquo;What&amp;rsquo;s the shipping policy?&amp;rdquo;, we&amp;rsquo;re effectively asking, &amp;ldquo;Based on this specific text: &amp;lsquo;&amp;hellip;We offer standard shipping&amp;hellip;&amp;rsquo; — what is the shipping policy?&amp;rdquo; This forces the LLM to base its answer on our facts, not its own general knowledge, making the response accurate and reliable.&lt;/p&gt;
&lt;h3 id=&#34;what-youll-build&#34;&gt;What you’ll build&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Telegram bot&lt;/li&gt;
&lt;li&gt;faqs.json knowledge base&lt;/li&gt;
&lt;li&gt;RAG pipeline with local embeddings (FAISS) + LLM (Open WebUI)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;prerequisites&#34;&gt;Prerequisites&lt;/h3&gt;
&lt;p&gt;You’ll need:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Python 3.12+&lt;/li&gt;
&lt;li&gt;a Telegram bot token (from BotFather)&lt;/li&gt;
&lt;li&gt;access to an LLM via a locally hosted Open WebUI instance (OpenAI-compatible API)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;setting-up-the-project-for-telegram-bot&#34;&gt;Setting up the Project for Telegram Bot&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;uv&lt;/code&gt; is a high-performance Python package manager, so we&amp;rsquo;ll use it to set up our project. If you don&amp;rsquo;t have it installed, you can get it with or visit the &lt;a href=&#34;https://docs.astral.sh/uv/getting-started/installation/&#34;&gt;site&lt;/a&gt; for installation steps:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pip install uv&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Create a new project directory and navigate into it:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;mkdir telegram-rag-bot
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cd telegram-rag-bot&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Initialize a new Python project.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;uv init --bare&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This command creates a minimal pyproject.toml file. This file will track our project&amp;rsquo;s metadata and, most importantly, its dependencies.&lt;/p&gt;
&lt;p&gt;Create a virtual environment using uv:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;uv venv&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This will create a .venv directory. Activate it with the following:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;source .venv/bin/activate
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;# On Windows, use
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;#.venv\Scripts\activate&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Install the necessary Python packages using uv:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;uv add python-telegram-bot python-dotenv langchain langchain-openai langchain-community faiss-cpu jq sentence-transformers&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The key libraries are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;python-telegram-bot&lt;/code&gt;: For handling all Telegram communication&lt;/li&gt;
&lt;li&gt;&lt;code&gt;langchain&lt;/code&gt;: The primary framework for building the RAG pipeline&lt;/li&gt;
&lt;li&gt;&lt;code&gt;langchain-openai&lt;/code&gt;: Connector to Open WebUI’s OpenAI-compatible API&lt;/li&gt;
&lt;li&gt;&lt;code&gt;faiss-cpu&lt;/code&gt;: An efficient library for similarity search, used as a local vector store to quickly find relevant chunks of your FAQ data&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;environment-and-configuration&#34;&gt;Environment and configuration&lt;/h4&gt;
&lt;p&gt;The bot reads the Telegram token from the environment variable &lt;code&gt;BOT_TOKEN&lt;/code&gt;. We can store it in a .env file as &lt;code&gt;BOT_TOKEN=your-token-here&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# .env (Open WebUI)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Open WebUI_URL must end with /v1 (e.g., http://localhost:3000/v1).&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#369&#34;&gt;BOT_TOKEN&lt;/span&gt;=123456:abcdefg
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Open &lt;span style=&#34;color:#369&#34;&gt;WebUI_URL&lt;/span&gt;=http://localhost:3000/v1
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Open &lt;span style=&#34;color:#369&#34;&gt;WebUI_API_KEY&lt;/span&gt;=your_key_here&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&#34;https://core.telegram.org/bots/features#inline-requests&#34;&gt;Inline mode&lt;/a&gt; requires enabling inline for the bot via BotFather.&lt;/p&gt;
&lt;p&gt;Create a new file named &lt;code&gt;bot.py&lt;/code&gt; and add the following code to set up and add message handlers for the Telegram bot.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;logging&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;os&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;uuid&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; uuid4
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;telegram&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; Update, InlineQueryResultArticle, InputTextMessageContent
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;telegram.ext&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; filters, MessageHandler, ApplicationBuilder, CommandHandler, ContextTypes, InlineQueryHandler
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;dotenv&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; load_dotenv
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# load .env variables&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;load_dotenv()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;bot_token = os.getenv(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;BOT_TOKEN&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# Setup logging&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;logging.basicConfig(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#038&#34;&gt;format&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;%(asctime)s&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt; - &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;%(name)s&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt; - &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;%(levelname)s&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt; - &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;%(message)s&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    level=logging.INFO
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;logger = logging.getLogger(&lt;span style=&#34;color:#369&#34;&gt;__name__&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;start&lt;/span&gt;(update: Update, context: ContextTypes.DEFAULT_TYPE):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; context.bot.send_message(chat_id=update.effective_chat.id, text=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;I&amp;#39;m a bot, please talk to me!&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;echo&lt;/span&gt;(update: Update, context: ContextTypes.DEFAULT_TYPE):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; context.bot.send_message(chat_id=update.effective_chat.id, text=update.message.text)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;caps&lt;/span&gt;(update: Update, context: ContextTypes.DEFAULT_TYPE):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    text_caps = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39; &amp;#39;&lt;/span&gt;.join(context.args).upper()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; context.bot.send_message(chat_id=update.effective_chat.id, text=text_caps)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;inline_caps&lt;/span&gt;(update: Update, context: ContextTypes.DEFAULT_TYPE):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    query = update.inline_query.query
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#080&#34;&gt;not&lt;/span&gt; query:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    results = []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    results.append(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        InlineQueryResultArticle(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#038&#34;&gt;id&lt;/span&gt;=&lt;span style=&#34;color:#038&#34;&gt;str&lt;/span&gt;(uuid4()),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            title=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Caps&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            input_message_content=InputTextMessageContent(query.upper())
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; context.bot.answer_inline_query(update.inline_query.id, results)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;unknown&lt;/span&gt;(update: Update, context: ContextTypes.DEFAULT_TYPE):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; context.bot.send_message(chat_id=update.effective_chat.id, text=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Sorry, I didn&amp;#39;t understand that command.&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;async&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;document&lt;/span&gt;(update: Update, context: ContextTypes.DEFAULT_TYPE):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; update.message.document:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        file = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; update.message.document.get_file()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        file_name = update.message.document.file_name
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; file.download_to_drive(file_name)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;elif&lt;/span&gt; update.message.photo:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#888&#34;&gt;# Get the largest photo size&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        file = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; update.message.photo[-&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt;].get_file()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        file_name = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;photo_&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;file.file_unique_id&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;.jpg&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#888&#34;&gt;# Create a unique name for photos&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; file.download_to_drive(file_name)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;elif&lt;/span&gt; update.message.video:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        file = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; update.message.video.get_file()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        file_name = update.message.video.file_name
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; file.download_to_drive(file_name)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;else&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; update.message.reply_text(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Please send a document, photo, or video.&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;main&lt;/span&gt;() -&amp;gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;None&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    start_handler = CommandHandler(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;start&amp;#39;&lt;/span&gt;, start)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    echo_handler = MessageHandler(filters.TEXT &amp;amp; (~filters.COMMAND), echo)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    caps_handler = CommandHandler(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;caps&amp;#39;&lt;/span&gt;, caps)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    inline_caps_handler = InlineQueryHandler(inline_caps)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    document_handler = MessageHandler(filters.PHOTO | filters.Document.PDF | filters.VIDEO, document)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    unknown_handler = MessageHandler(filters.COMMAND, unknown)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    application = ApplicationBuilder().token(bot_token).build()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    application.add_handler(start_handler)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    application.add_handler(echo_handler)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    application.add_handler(caps_handler)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    application.add_handler(inline_caps_handler)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    application.add_handler(document_handler)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    application.add_handler(unknown_handler)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# Run the bot&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    logger.info(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Starting bot polling...&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    application.run_polling()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;__name__&lt;/span&gt; == &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;__main__&amp;#39;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    main()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id=&#34;try-it-out&#34;&gt;Try it out&lt;/h4&gt;
&lt;p&gt;To run the application, simply run:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;uv run bot.py&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Search for your bot name in Telegram and send the bot a message or command like &lt;code&gt;/start&lt;/code&gt; or &lt;code&gt;/caps&lt;/code&gt;.&lt;/p&gt;
&lt;h4 id=&#34;what-this-bot-does&#34;&gt;What this bot does&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;Responds to /start with a greeting&lt;/li&gt;
&lt;li&gt;Echoes back any plain text message (that isn’t a command)&lt;/li&gt;
&lt;li&gt;Converts text to uppercase via /caps or inline mode&lt;/li&gt;
&lt;li&gt;Downloads files users send (photos, PDFs, and videos) to local storage&lt;/li&gt;
&lt;li&gt;Politely handles unknown commands&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;core-structure&#34;&gt;Core structure&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;start()&lt;/code&gt;: Sends a simple welcome message when the user runs /start&lt;/li&gt;
&lt;li&gt;&lt;code&gt;echo()&lt;/code&gt;: Replies with the exact same text the user sent (only for non-commands)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;caps()&lt;/code&gt;: Turns the arguments after /caps into uppercase and sends them back&lt;/li&gt;
&lt;li&gt;&lt;code&gt;inline_caps()&lt;/code&gt;: Provides an inline result that uppercases whatever users type after @YourBotName in any chat&lt;/li&gt;
&lt;li&gt;&lt;code&gt;document()&lt;/code&gt;: Saves received media to disk:
&lt;ul&gt;
&lt;li&gt;Photos: Downloads the largest size, naming it photo_&amp;lt;unique_id&amp;gt;.jpg&lt;/li&gt;
&lt;li&gt;PDFs: Downloads using the document’s file name. Note: The filter only accepts PDFs as documents&lt;/li&gt;
&lt;li&gt;Videos: Downloads using the video’s file name&lt;/li&gt;
&lt;li&gt;If none of these are present, it prompts the user to send a supported file&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unknown()&lt;/code&gt;: Catches any unrecognized commands and replies with a friendly error&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;handlers-and-filters&#34;&gt;Handlers and filters&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;CommandHandler(&#39;start&#39;, start)&lt;/code&gt; and &lt;code&gt;CommandHandler(&#39;caps&#39;, caps)&lt;/code&gt; handle &lt;a href=&#34;https://core.telegram.org/bots/features#commands&#34;&gt;commands&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MessageHandler(filters.TEXT &amp;amp; (~filters.COMMAND), echo)&lt;/code&gt; ensures normal text (not commands) is echoed&lt;/li&gt;
&lt;li&gt;&lt;code&gt;InlineQueryHandler(inline_caps)&lt;/code&gt; answers inline queries&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MessageHandler(filters.PHOTO | filters.Document.PDF | filters.VIDEO, document)&lt;/code&gt; restricts downloads to photos, PDFs, and videos&lt;/li&gt;
&lt;li&gt;&lt;code&gt;MessageHandler(filters.COMMAND, unknown)&lt;/code&gt; is added last to catch all other commands&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;running-the-bot&#34;&gt;Running the bot&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;main()&lt;/code&gt; wires up the handlers, builds the Application with the token, logs a startup message, and starts long polling via &lt;code&gt;application.run_polling()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This script is a clean, async-first Telegram bot scaffold that demonstrates commands, inline mode, message filtering, and media downloads—ready to extend for more sophisticated behaviors.&lt;/p&gt;
&lt;p&gt;Now that we have our bot ready, we will extend the code to add a RAG pipeline to the bot.&lt;/p&gt;
&lt;h3 id=&#34;setting-up-the-knowledge-base&#34;&gt;Setting up the knowledge base&lt;/h3&gt;
&lt;p&gt;Let’s set up a knowledge base by creating a file named &lt;code&gt;faqs.json&lt;/code&gt; to hold our data. The RAG pipeline will load and search this content. An example structure is shown below.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;category&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;General&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;question&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;What are your operating hours?&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;answer&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Monday to Friday, 9:00 AM–5:00 PM (local time).&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;category&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Accounts&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;question&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;How do I reset my password?&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;answer&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Go to our website, click Login, then Forgot Password. Check your email for the reset link.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id=&#34;setting-up-the-rag-pipeline&#34;&gt;Setting up the RAG Pipeline&lt;/h3&gt;
&lt;p&gt;The RAG pipeline is the engine that converts our static JSON file into a searchable brain for our bot. This part initializes once and creates a vector database. In simple steps,&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Load faqs.json&lt;/li&gt;
&lt;li&gt;Create embeddings&lt;/li&gt;
&lt;li&gt;Store them in FAISS&lt;/li&gt;
&lt;li&gt;When a user asks a question, find similar answers and ask the LLM to write a reply based only on those&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&#34;data-ingestion-indexing-the-faqs-handled-by-setup_rag_chain-method&#34;&gt;Data Ingestion (Indexing the FAQs) handled by &lt;code&gt;setup_rag_chain()&lt;/code&gt; method&lt;/h5&gt;
&lt;p&gt;This part happens once when the bot starts. We load the &lt;code&gt;faqs.json&lt;/code&gt; file, create vector embeddings, and store them in a searchable database (FAISS).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Load Data: Read the faqs.json file&lt;/li&gt;
&lt;li&gt;Embeddings: Use an embedding model (like &lt;a href=&#34;https://huggingface.co/sentence-transformers/all-MiniLM-L6-v2&#34;&gt;&lt;code&gt;HuggingFace (Sentence-Transformers) all‑MiniLM‑L6‑v2.&lt;/code&gt;&lt;/a&gt;) to convert the text into numerical vectors&lt;/li&gt;
&lt;li&gt;Vector Store: Store these vectors in a FAISS index for fast retrieval&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&#34;the-rag-retrieval-logic-handled-by-handle_message-method&#34;&gt;The RAG Retrieval Logic handled by &lt;code&gt;handle_message()&lt;/code&gt; method&lt;/h5&gt;
&lt;p&gt;When a user asks a question:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Embed Query: The user&amp;rsquo;s question is converted into an embedding vector&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Retrieve Context: The query vector is used to perform a similarity search against the FAISS index. This returns the top K most relevant FAQs (question and answer pairs)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Construct Prompt: A final prompt is built, containing the user&amp;rsquo;s question and the retrieved relevant context&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Example Prompt Template:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;You are an expert FAQ assistant. Use the following context to answer
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;the user&amp;#39;s question. If the context does not contain the answer,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;state that you cannot help with this specific question. Context:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[Retrieved FAQs] Question: [User&amp;#39;s message]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Generate Response: The complete prompt is sent to our Open WebUI model via the OpenAI-compatible API, e.g. &lt;code&gt;gpt-5&lt;/code&gt; or another model exposed by Open WebUI, which generates a coherent, context-grounded final answer&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Send to Telegram: The bot sends the LLM&amp;rsquo;s final response back to the user&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&#34;new-capabilities-added-to-botpy&#34;&gt;New capabilities added to &lt;code&gt;bot.py&lt;/code&gt;&lt;/h5&gt;
&lt;ul&gt;
&lt;li&gt;RAG pipeline: Loads FAQs from a local JSON file, embeds them with HuggingFace, retrieves the most relevant entries via FAISS, and drafts answers with an LLM served by Open WebUI.&lt;/li&gt;
&lt;li&gt;Inline UX polish: Sends a “typing…” chat action while the model thinks.&lt;/li&gt;
&lt;li&gt;Persisted chain: The RAG chain is built once at startup and stored in bot_data for reuse across messages.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The core logic of our bot will revolve around an update to the standard message handler. When a user sends a question, the bot no longer looks for a simple command; instead, it passes the question to the RAG pipeline.&lt;/p&gt;
&lt;h4 id=&#34;try-it-out-1&#34;&gt;Try it out&lt;/h4&gt;
&lt;p&gt;To run the application, simply run&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;uv run bot.py&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Search for your bot name in telegram and send the bot a message like &lt;code&gt;What are your operating hours?&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Changes to the RAG pipeline setup are available &lt;a href=&#34;https://github.com/bimalghartimagar/telegram-rag-bot/commit/4afac21e085c2782f98fffc66bb2cca27e6c7f50&#34;&gt;here&lt;/a&gt;. The source code is available &lt;a href=&#34;https://github.com/bimalghartimagar/telegram-rag-bot&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;By integrating a RAG pipeline, we&amp;rsquo;ve leveled up our Telegram bot from a simple command processor to a knowledge-aware assistant. This approach ensures our bot&amp;rsquo;s answers are accurate, grounded in our provided faqs.json data, and remain consistent, dramatically reducing the chance of &amp;ldquo;hallucinations&amp;rdquo; from the underlying LLM.&lt;/p&gt;
&lt;p&gt;This architecture is powerful and scalable. To expand its capabilities, we only need to update the faqs.json file and re-run the indexing step—no need to retrain or modify the core LLM!&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Migrating from Legacy Networking to systemd-networkd on Ubuntu 24.04</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/11/legacy-networking-to-systemd-networkd/"/>
      <id>https://www.endpointdev.com/blog/2025/11/legacy-networking-to-systemd-networkd/</id>
      <published>2025-11-25T00:00:00+00:00</published>
      <author>
        <name>Bharathi Ponnusamy</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2025/11/legacy-networking-to-systemd-networkd/banner.webp&#34; alt=&#34;Apartment buildings and a domed white Indian building in Leh city, India sit among trees in front of a layered mountain range, capped with clouds.&#34;&gt;&lt;/p&gt;
&lt;!-- photo by Bharathi Ponnusamy --&gt;
&lt;p&gt;During a recent Ubuntu server upgrade, I migrated a production system from the legacy ifupdown networking stack to the modern systemd-networkd service. This migration was part of preparing several remote production servers for a smooth upgrade to Ubuntu 24.04, which relies heavily on systemd-managed components.&lt;/p&gt;
&lt;p&gt;In this post, I’ll walk through the full migration process, how we automated it safely across multiple remote servers, and the lessons learned from implementing rollback and watchdog mechanisms for zero-downtime transitions.&lt;/p&gt;
&lt;h3 id=&#34;why-migrate-to-systemd-networkd&#34;&gt;Why migrate to systemd-networkd?&lt;/h3&gt;
&lt;p&gt;Ubuntu 24.04 uses systemd-networkd as the default backend for network management. The traditional ifupdown scripts (/etc/network/interfaces) are no longer the recommended way to configure networking. Some of the modern features enabled by systemd-networkd include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Predictable network interface naming (e.g., ens3, eth0, etc.)&lt;/li&gt;
&lt;li&gt;Built-in support for bridges, VLANs, and bonds&lt;/li&gt;
&lt;li&gt;Faster boot times with asynchronous network handling&lt;/li&gt;
&lt;li&gt;Unified configuration under /etc/systemd/network&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Migrating now also avoids future compatibility issues and takes advantage of systemd’s integrated logging and management.&lt;/p&gt;
&lt;h3 id=&#34;step-1-check-the-current-setup&#34;&gt;Step 1: Check the current setup&lt;/h3&gt;
&lt;p&gt;Before the migration, I confirmed the system was still using ifupdown:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ls /etc/network/interfaces
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;cat /etc/network/interfaces&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Output showed legacy configuration similar to:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;auto eth0
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;iface eth0 inet static
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    address 10.42.41.1
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    netmask 255.255.0.0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id=&#34;step-2-install-and-enable-systemd-networkd&#34;&gt;Step 2: Install and enable systemd-networkd&lt;/h3&gt;
&lt;p&gt;Since systemd-networkd wasn’t already active, I installed and enabled it:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-plain&#34; data-lang=&#34;plain&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo apt install systemd-networkd systemd-resolved -y
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo systemctl enable systemd-networkd
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo systemctl enable systemd-resolved&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then, I stopped ifupdown to prevent conflicts.&lt;/p&gt;
&lt;h3 id=&#34;step-3-create-a-network-configuration-file&#34;&gt;Step 3: Create a network configuration file&lt;/h3&gt;
&lt;p&gt;Each interface now has its own .network file under &lt;code&gt;/etc/systemd/network/&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;DHCP-based interface:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;# /etc/systemd/network/10-eth0.network
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[Match]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Name=eth0
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[Network]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;DHCP=yes&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Static interface:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;# /etc/systemd/network/20-eth1.network
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[Match]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Name=eth1
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;[Network]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Address=10.42.41.1/16
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Gateway=10.42.0.1
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;DNS=8.8.8.8
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;DNS=1.1.1.1&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id=&#34;step-4-configure-systemd-resolved&#34;&gt;Step 4: Configure systemd-resolved&lt;/h3&gt;
&lt;p&gt;This step ensures your system’s DNS resolver works correctly with systemd-networkd.&lt;/p&gt;
&lt;p&gt;systemd-resolved listens locally on 127.0.0.53 and handles DNS resolution, caching, and per-interface settings.&lt;/p&gt;
&lt;p&gt;Link your system’s &lt;code&gt;/etc/resolv.conf&lt;/code&gt; to use it:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo ln -sf /run/systemd/resolve/stub-resolv.conf /etc/resolv.conf&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then to verify:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;resolvectl status&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If we skip this, the system might lose DNS resolution after reboot. Applications like apt, curl, and ping may fail to resolve hostnames even though interfaces are up.&lt;/p&gt;
&lt;h3 id=&#34;restart-and-verify&#34;&gt;Restart and verify&lt;/h3&gt;
&lt;p&gt;Restart both services:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo systemctl restart systemd-networkd
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;sudo systemctl restart systemd-resolved&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Check active links:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;networkctl list&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Detailed view for one interface:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;networkctl status eth0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Sample output:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;● 2: eth0
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;       Type: ether
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      State: routable (configured)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     Address: 10.42.41.1/16
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     Gateway: 10.42.0.1
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;     DNS: 8.8.8.8&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Test:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ping -c 3 8.8.8.8
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;ping -c 3 google.com&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id=&#34;step-6-reboot-and-confirm&#34;&gt;Step 6: Reboot and confirm&lt;/h3&gt;
&lt;p&gt;Reboot once to ensure everything persists. After the reboot,&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;systemctl status systemd-networkd
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;systemctl status systemd-resolved&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and confirm interface and DNS are still functional.&lt;/p&gt;
&lt;h3 id=&#34;handling-remote-migration-and-downtime-risks&#34;&gt;Handling remote migration and downtime risks&lt;/h3&gt;
&lt;p&gt;Performing this migration remotely on production systems without physical console access required special care. I prepared several layers of protection to prevent accidental lockouts.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Console or IPMI verification&lt;/strong&gt;: Ensured each node had out-of-band access in case SSH connectivity was lost.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Timed rollback safety script&lt;/strong&gt;: A background process automatically reverted to legacy networking if the new configuration didn’t come up within a few minutes.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c00;font-weight:bold&#34;&gt;#!/bin/bash
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#c00;font-weight:bold&#34;&gt;&lt;/span&gt;sleep &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;300&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; ! ping -c1 8.8.8.8 &amp;amp;&amp;gt;/dev/null; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;then&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    systemctl disable systemd-networkd
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    systemctl &lt;span style=&#34;color:#038&#34;&gt;enable&lt;/span&gt; networking
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    reboot
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;fi&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;automating-the-migration-and-rollback-process&#34;&gt;Automating the migration and rollback process&lt;/h3&gt;
&lt;p&gt;To streamline upgrades across multiple servers, I created automation scripts that handle migration, rollback, and ongoing monitoring.&lt;/p&gt;
&lt;h4 id=&#34;migrate_to_systemd_networkdsh&#34;&gt;migrate_to_systemd_networkd.sh&lt;/h4&gt;
&lt;p&gt;This script performs a complete, logged migration with built-in validation and rollback.
It checks for valid .network files, disables legacy services, enables systemd-networkd, tests connectivity, and if all retries fail, restores the old configuration automatically.&lt;/p&gt;
&lt;p&gt;Key features&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Validates configuration syntax using systemd-analyze verify&lt;/li&gt;
&lt;li&gt;Masks conflicting services (&lt;code&gt;ifup@eth*&lt;/code&gt;, NetworkManager)&lt;/li&gt;
&lt;li&gt;Performs up to three connectivity tests&lt;/li&gt;
&lt;li&gt;Restarts SSH and dependent services&lt;/li&gt;
&lt;li&gt;Schedules a controlled reboot or user-confirmed one&lt;/li&gt;
&lt;li&gt;Rolls back cleanly if connectivity fails&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;upgrade_watchdogsh&#34;&gt;upgrade_watchdog.sh&lt;/h4&gt;
&lt;p&gt;During do-release-upgrade, networking may restart multiple times.
This watchdog script runs in the background to ensure systemd-networkd remains active and services like SSH come back automatically.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Executes every 10 minutes&lt;/li&gt;
&lt;li&gt;Verifies /etc/systemd/network/*.network exists and services are running as expected if it fails, this script restarts the service&lt;/li&gt;
&lt;li&gt;Restarts systemd-networkd, ssh, and dependent services&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Together, both scripts allowed us to perform fully remote OS upgrades with zero manual downtime and consistent recovery paths.&lt;/p&gt;
&lt;h3 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Migrating to systemd-networkd modernizes Ubuntu servers and simplifies long-term maintenance.
With the automation scripts and safety mechanisms in place, we successfully migrated multiple remote servers with no downtime and automatic rollback protection.&lt;/p&gt;
&lt;p&gt;This upgrade not only prepares infrastructure for Ubuntu 24.04 but also integrates cleanly with our automation and monitoring systems, ensuring reliable networking for years ahead.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Adventures in Vibe-Coding with Replit</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/10/adventures-in-vibe-coding-with-replit/"/>
      <id>https://www.endpointdev.com/blog/2025/10/adventures-in-vibe-coding-with-replit/</id>
      <published>2025-10-29T00:00:00+00:00</published>
      <author>
        <name>Seth Jensen</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2025/10/adventures-in-vibe-coding-with-replit/nyc-streets-from-above.webp&#34; alt=&#34;A New York City intersection viewed from above, at a 40 degree angle&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2025, shot on Kodak 200 with a Nikon FE --&gt;
&lt;p&gt;A few weeks back, I tried out Replit Agent to see how viable it is as a development tool. I built two apps from scratch, which took 10–15 hours total, with a decent amount of human input along the way to clarify the agent&amp;rsquo;s questions.&lt;/p&gt;
&lt;h3 id=&#34;project-1-multi-llm-code-review-api&#34;&gt;Project 1: Multi-LLM code review API&lt;/h3&gt;
&lt;p&gt;The idea for the first project was to have a multi-LLM code review app, where you would pass a repository on GitHub or locally to an API, and it would run several full-repo code reviews, ranking and deduplicating them before returning a response. This setup should allow me to have a web frontend or CLI without much added complexity.&lt;/p&gt;
&lt;h4 id=&#34;the-prompt&#34;&gt;The prompt&lt;/h4&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Please build an app based on this app specification: app_spec.yaml (460 lines)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id=&#34;the-results&#34;&gt;The results&lt;/h4&gt;
&lt;p&gt;After much back and forth (and about 40 agent-generated Git commits), I got this message:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;🎉 SUCCESS! The Multi-LLM Code Review Assistant is now fully working!&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Well, I actually got this message several times after sending just about any command to the agent. It&amp;rsquo;s very excited about its own work, even when it doesn&amp;rsquo;t work! In this case, the API connections were working, and testing with curl gave output, but I couldn&amp;rsquo;t get it to give more than one code review tip for a large codebase — not a very effective code-review app.&lt;/p&gt;
&lt;p&gt;I tried asking the agent to diagnose and fix this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;So, this seems to not be returning very many code review suggestions. If I go
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;look at the repos I&amp;#39;m putting in, there are plenty of code issues that could be
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;returned, especially with documentation. How can we make this system find more
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;issues?&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In response, Replit&amp;rsquo;s agent created a full React frontend for the API! Neat, but far from what I asked — the app still doesn&amp;rsquo;t return substantial code reviews.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/10/adventures-in-vibe-coding-with-replit/code-review-unwanted-frontend.webp&#34; alt=&#34;Code Review AI Multi-LLM Assistant interface showing dark-themed navigation sidebar with options for Submit Code, Dashboard, Analytics, and Settings. Main content area displays &amp;lsquo;Multi-LLM Code Review Assistant&amp;rsquo; title with description and two buttons: GitHub Repository and Upload ZIP File.&#34;&gt;&lt;/p&gt;
&lt;p&gt;So after ~$100 of agent credits, I found that Replit was not the tool for this app — I&amp;rsquo;m now trying to build a &lt;a href=&#34;/blog/2025/10/creating-agentic-ai-apps/&#34;&gt;CrewAI&lt;/a&gt; app to accomplish the same thing (stay tuned for a post about that).&lt;/p&gt;
&lt;h3 id=&#34;project-2-end-point-ecommerce--hugo-site&#34;&gt;Project 2: End Point Ecommerce + Hugo site&lt;/h3&gt;
&lt;p&gt;A common theme in Replit apps we&amp;rsquo;ve experimented with is that it prefers to make everything a web app with a React frontend. I was curious how it&amp;rsquo;d do with a different web stack: The &lt;a href=&#34;https://gohugo.io/&#34;&gt;Hugo&lt;/a&gt; static site generator for the frontend and our recently launched &lt;a href=&#34;/expertise/end-point-ecommerce/&#34;&gt;End Point Ecommerce&lt;/a&gt; for the backend.&lt;/p&gt;
&lt;p&gt;This is an interesting problem for an LLM, since Hugo is widely used (including on &lt;a href=&#34;/blog/2021/08/converting-to-hugo/&#34;&gt;this site&lt;/a&gt;), but still small compared to React, Vue, and similar frameworks, and since End Point Ecommerce is well-documented but too new for any online discussion. However, it&amp;rsquo;s a modern .NET ecommerce framework and the code is readable and straightforward.&lt;/p&gt;
&lt;h4 id=&#34;the-prompt-1&#34;&gt;The prompt&lt;/h4&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Build me a hugo website frontend to interact with end point ecommerce:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;https://ecommerce.endpointdev.com/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;For the UI, make a halloween-themed storefront with orange text, black and grey
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;backgrounds, and halloween-themed test products. Use the Ballast font for
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;headers, and charter for body text for now.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After providing this initial prompt, I followed up with these:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Here is the github link for end point ecommerce, which this site will connect to:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;https://github.com/EndPointCorp/end-point-ecommerce
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Please make the app as simple as possible, while still working
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Oh, and please use this demo API to connect to for now, so you don&amp;#39;t have to
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;build any backend at all, just interface with this:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;https://demo.ecommerce.endpointdev.com/swagger/index.html&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id=&#34;the-results-version-1&#34;&gt;The results (version 1)&lt;/h4&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/10/adventures-in-vibe-coding-with-replit/site-v1.png&#34; alt=&#34;Spooky Store Halloween ecommerce website showing three product cards featuring jack-o&amp;rsquo;-lantern pumpkin carving designs. Products listed are: Pumpkin Spice Treats with pumpkin emoji ($3.99), Spider Web Candy with spider emoji ($2.49), and Ghost Marshmallows with skull emoji ($4.99). Dark background with orange Halloween-themed branding.&#34;&gt;&lt;/p&gt;
&lt;p&gt;It generated a nice, simple ecommerce storefront, using Hugo and End Point Ecommerce. Impressive! However, there&amp;rsquo;s one red flag: if you look at the products &lt;a href=&#34;https://demo.ecommerce.endpointdev.com/&#34;&gt;in the demo API&lt;/a&gt; we&amp;rsquo;re using, they are simple groceries — Apple, Banana, etc. So where did &amp;ldquo;Pumpkin spice treats,&amp;rdquo; &amp;ldquo;Spider Web Candy,&amp;rdquo; and &amp;ldquo;Ghost Marshmallows&amp;rdquo; come from?&lt;/p&gt;
&lt;p&gt;It turns out, these are &amp;ldquo;fallback products&amp;rdquo; Replit had made when it failed to connect to the End Point Ecommerce demo API. Not great for an ecommerce site! Your users would see bogus but real-looking products when they&amp;rsquo;re unable to see or buy real products (I didn&amp;rsquo;t test this far, but the site may have even accepted payment info for fake products).&lt;/p&gt;
&lt;p&gt;So I followed up:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Please remove the fallback products&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id=&#34;the-results-version-2&#34;&gt;The results (version 2)&lt;/h4&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/10/adventures-in-vibe-coding-with-replit/site-v2.png&#34; alt=&#34;Spooky Store Halloween ecommerce website with jack-o&amp;rsquo;-lantern decorations. Features welcome banner with spider icons and orange border. Error message displayed stating &amp;lsquo;Something spooky happened! Could not load products&amp;rsquo; with skull icons, indicating products failed to load in the Haunting Collection section.&#34;&gt;&lt;/p&gt;
&lt;p&gt;This looks more promising! We get an error message written by the frontend. In production we might want to display more specific messages based on the response, but for this demo, I&amp;rsquo;m happy with this way of displaying errors.&lt;/p&gt;
&lt;p&gt;In true &amp;ldquo;vibe-coding&amp;rdquo; fashion, I asked the Replit agent to diagnose the network error:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;There is a network error while loading products, what is the issue?&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It responded with a couple options, the main one being assume it&amp;rsquo;s a CORS issue.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Possible Solutions
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Option 1: Server-Side Proxy (Recommended)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- Create a simple server endpoint that fetches the products
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- Your frontend calls your server, your server calls the external API
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;- No CORS issues since server-to-server requests aren&amp;#39;t restricted&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I was suspicious of it being a CORS issue since everything was hosted in the Replit container, so after poking around myself, I wrote this prompt:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Please use the web api to sign up for an account by POSTing to the /api/User
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;end point and getting an API key&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;However, after reviewing the project for this post, I found that I misunderstood the issue in this prompt — no authentication is needed to GET &lt;code&gt;/api/products&lt;/code&gt; on the demo API of End Point Ecommerce, the site was just disallowing this cross-origin request. Replit&amp;rsquo;s agent went ahead and implemented the CORS change anyway, though it didn&amp;rsquo;t correct my prompt.&lt;/p&gt;
&lt;p&gt;After a few more followup prompts&amp;hellip;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Please use the &amp;#34;basePrice&amp;#34;, &amp;#34;discountAmount&amp;#34;, and &amp;#34;discountedPrice&amp;#34; keys that
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;are returned from the API to show the real prices of the items
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Now, please fetch the images from the API as well&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&amp;hellip;we have a decent working Hugo site connected with the End Point Ecommerce demo API!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/10/adventures-in-vibe-coding-with-replit/site-v3.png&#34; alt=&#34;Spooky Store Halloween ecommerce website displaying three product cards in a row: red apple for $1.99, banana bunch on yellow background for $0.25, and raw beef for $7.99. Shopping cart button visible in top right corner. Dark background with orange Halloween-themed header.&#34;&gt;&lt;/p&gt;
&lt;h4 id=&#34;feature-addition-cart&#34;&gt;Feature addition: cart&lt;/h4&gt;
&lt;p&gt;I wrote this prompt next, without having done my research on the backend (which made for an interesting unintentional experiment):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Please add a lightweight cart management system which uses localStorage, with
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;options to add to, remove from, or update quantity of items in the cart. Add a
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;simple checkout form with contact information, shipping address, and billing
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;address. For now, instead of payment information, just have a button which adds
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;a commonly used dummy credit card number.&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There&amp;rsquo;s an issue here: I specified using &lt;code&gt;localStorage&lt;/code&gt;, which is a fine way to set up a frontend cart system, but it ignores the existing system End Point Ecommerce already supplies. I hadn&amp;rsquo;t checked the documentation before making this prompt, and neither did Replit; it happily obliged and created a cart management system on the frontend. It&amp;rsquo;s nice that the agent did what I asked, but if I were trying to work with the agent, without cross-referencing documentation myself, I would be disappointed.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s the interesting part about using a non-deterministic system like AI agents: responding to one prompt, it might ignore my specific (and misguided) requests, but in another it might blindly execute the tasks it&amp;rsquo;s given. By the nature of LLMs, you can&amp;rsquo;t predict what you&amp;rsquo;ll get.&lt;/p&gt;
&lt;h4 id=&#34;bonus-a-few-extra-notes&#34;&gt;Bonus: a few extra notes&lt;/h4&gt;
&lt;p&gt;A couple final notes:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No .gitignore was generated, so the Hugo-generated &lt;code&gt;public/&lt;/code&gt; folder is tracked in Git&lt;/li&gt;
&lt;li&gt;The agent used plain CSS, without SCSS or a framework — I&amp;rsquo;d recommend specifying the stack further in the prompt if you want an app that&amp;rsquo;ll be nice to maintain. You don&amp;rsquo;t want to assume the agent will make smart coding decisions on its own.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;when-id-use-replit-and-similar-do-it-all-ai-agents&#34;&gt;When I&amp;rsquo;d use Replit (and similar do-it-all AI agents)&lt;/h4&gt;
&lt;p&gt;I would use Replit for very quick-and-dirty web development, when sites which shouldn&amp;rsquo;t last a long time — although I&amp;rsquo;ve seen too many projects originally intended to be short-lived become important for many years&amp;hellip;&lt;/p&gt;
&lt;p&gt;Replit handles its containerized deployments well; if I needed to share a working demo site within a few hours, Replit would be a good pick. Especially if you like React frontends, even when you might not have asked for one! But if I was dealing with customer data, orders, or my own data I cared about, I would want to be heavily involved throughout the development process.&lt;/p&gt;
&lt;p&gt;For projects where performance and accuracy matter (read: most projects), I will be taking a more directed approach. I&amp;rsquo;ve had some good results using Continue or GitHub CoPilot as a coding assistant for small tasks in existing repos, rather than letting AI taking the wheel completely.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>A Preact Web App Without npm Build</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/10/preact-web-app-without-npm-build/"/>
      <id>https://www.endpointdev.com/blog/2025/10/preact-web-app-without-npm-build/</id>
      <published>2025-10-15T00:00:00+00:00</published>
      <author>
        <name>Dmitry Kiselev</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2025/10/preact-web-app-without-npm-build/nyc-skyline-orange-sunset.webp&#34; alt=&#34;An orange sun sets over New York City, viewed from the top of a building in Manhattan. A water tower is visible in front of buildings which reflect bright light on the left side, with shadows on the right side.&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2025. --&gt;
&lt;p&gt;Modern JavaScript frameworks such as React and Preact usually require a full “build pipeline” with Node.js, npm, and bundlers like Webpack or Vite. That’s fine for large web apps, but sometimes you just need a small, self-contained browser interface inside a project that isn’t mainly about the web — say, a Java program that processes transit data and simply needs a page to display results.&lt;/p&gt;
&lt;p&gt;This post explores how to use Preact and its companion library @preact/signals &lt;em&gt;without any npm build process at all&lt;/em&gt;. By taking advantage of browser-native import maps and writing a bit of code directly in JavaScript instead of JSX, it shows how to build a lightweight, modern UI that runs straight from static HTML, CSS, and JS files — no build tools required.&lt;/p&gt;
&lt;h3 id=&#34;the-scenario&#34;&gt;The Scenario&lt;/h3&gt;
&lt;p&gt;This might look like a strange problem to solve but sometimes the web UI part just isn’t a main character in your project.&lt;/p&gt;
&lt;p&gt;For instance, I have a GTFS data processor whose main goal is to preprocess public transport stop timetables. It’s written in Java and I want a simple browser UI to check its output. So I want the web UI part of the project to be just static HTML, CSS, and JavaScript files.&lt;/p&gt;
&lt;p&gt;I also don’t want to run npm or yarn inside what would otherwise be a pure Maven-managed project. But at the same time, I would like to use a modern JS UI framework such as Preact.&lt;/p&gt;
&lt;h3 id=&#34;the-main-issues&#34;&gt;The Main Issues&lt;/h3&gt;
&lt;p&gt;To pull off this trick, there are two main challenges:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;JSX&lt;/li&gt;
&lt;li&gt;Imports&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;jsx&#34;&gt;JSX&lt;/h3&gt;
&lt;p&gt;JSX is basically JavaScript templates. When you write something like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;function&lt;/span&gt; Greeting() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;div className=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;greeting&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      Hello world
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &amp;lt;&lt;span style=&#34;color:#a61717;background-color:#e3d2d2&#34;&gt;/div&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;function&lt;/span&gt; App() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; (&amp;lt;Greeting/&amp;gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;During project build Babel will transform it into:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;function&lt;/span&gt; Greeting() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;// Particular function for creating elements may vary.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; h(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;div&amp;#39;&lt;/span&gt;, {className: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;greeting&amp;#34;&lt;/span&gt;}, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Hello world&amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;function&lt;/span&gt; App() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; h(Greeting, &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;null&lt;/span&gt;);
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is a pretty straightforward transformation, and no one is stopping you from importing the element creation function (h in the example above) and composing what would be Babel output yourself.&lt;/p&gt;
&lt;h3 id=&#34;imports&#34;&gt;Imports&lt;/h3&gt;
&lt;p&gt;Now for the more interesting issue: imports.&lt;/p&gt;
&lt;p&gt;Normally, with a build process, you could just write:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; { h } from &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;preact&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; { signal } from &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;@preact/signals&amp;#39;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;What’s more, some packages will try to resolve their own dependencies by importing them by global name. For instance, &lt;code&gt;@preact/signals&lt;/code&gt; will try to import &lt;code&gt;@preact/signals-core&lt;/code&gt;, and there isn’t a ready-made UMD (Universal Module Definition) version of &lt;code&gt;@preact/signals-core&lt;/code&gt; available.&lt;/p&gt;
&lt;h3 id=&#34;the-solution-browser-import-maps&#34;&gt;The Solution: Browser Import Maps&lt;/h3&gt;
&lt;p&gt;I would like to be able to just map library names to URLs of ES modules.&lt;/p&gt;
&lt;p&gt;There is a relatively new browser feature that does exactly this: an &lt;em&gt;import map&lt;/em&gt;. It’s a special type of &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; block containing a JSON object with your mapping.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-react&#34; data-lang=&#34;react&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;script&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;type&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;importmap&amp;#34;&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;imports&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;preact&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;https://unpkg.com/preact@latest/dist/preact.mjs&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;preact/hooks&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;https://unpkg.com/preact@latest/hooks/dist/hooks.mjs&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;preact/compat&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;https://unpkg.com/preact@latest/compat/dist/compat.mjs&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;@preact/signals-core&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;https://unpkg.com/@preact/signals-core@latest/dist/signals-core.mjs&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;      &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;@preact/signals&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;https://unpkg.com/@preact/signals@latest/dist/signals.mjs&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;script&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And that’s it!&lt;/p&gt;
&lt;p&gt;With the import map in place, your browser can resolve all those imports directly from a CDN like &lt;a href=&#34;https://unpkg.com/&#34;&gt;unpkg.com&lt;/a&gt;. You can then use Preact, hooks, and signals exactly as if you were running a fully bundled app.&lt;/p&gt;
&lt;h3 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;This approach is ideal for small, embedded, or hybrid projects where a full JavaScript toolchain would be overkill. It lets you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Build modern UI components with Preact&lt;/li&gt;
&lt;li&gt;Avoid npm, yarn, Babel, and bundlers entirely&lt;/li&gt;
&lt;li&gt;Keep your project clean and language-native (e.g., Maven for Java, without extra layers)&lt;/li&gt;
&lt;li&gt;Run everything as plain static assets, right from your browser&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Everything works, and nothing extra gets in the way.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Creating Agentic AI Applications</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/10/creating-agentic-ai-apps/"/>
      <id>https://www.endpointdev.com/blog/2025/10/creating-agentic-ai-apps/</id>
      <published>2025-10-14T00:00:00+00:00</published>
      <author>
        <name>Kürşat Kutlu Aydemir</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2025/10/creating-agentic-ai-apps/futuristic-maze.webp&#34; alt=&#34;Futuristic Abstract Maze with Colorful Lights&#34;&gt;&lt;br&gt;
Photo by &lt;a href=&#34;https://www.pexels.com/photo/futuristic-abstract-maze-with-colorful-lights-28494630/&#34;&gt;Steve Johnson&lt;/a&gt; on Pexels&lt;/p&gt;
&lt;p&gt;In the rapidly evolving world of AI, agentic AI is emerging as a game-changer. These systems go beyond simple chatbots or predictive models: they&amp;rsquo;re designed to act autonomously, make decisions, and interact with the real world to accomplish goals. In this blog post, we&amp;rsquo;ll dive into what agents are, explore agentic applications and orchestration, and walk through how to build your own agentic applications with open-source examples.&lt;/p&gt;
&lt;h3 id=&#34;what-are-agents&#34;&gt;What Are Agents?&lt;/h3&gt;
&lt;p&gt;AI agents are autonomous entities that can perceive their environment, reason about it, and take actions to achieve specific objectives. Think of an AI agent as more than just a chatbot, it’s like a digital teammate that can look around, make decisions, and take action on its own. Unlike traditional AI models that respond passively to inputs, agents are proactive: they can break down goals into sub-tasks, use tools (like APIs or databases), maintain memory across interactions, and adapt based on feedback. This makes AI agents ideal for applications requiring independence, such as automation, research, or problem-solving.&lt;/p&gt;
&lt;p&gt;At their core, agents typically integrate large language models (LLMs) like GPT-4 or Llama for reasoning, combined with mechanisms for tool invocation and state management. They can operate in loops, iteratively refining their approach until the goal is met.&lt;/p&gt;
&lt;h4 id=&#34;open-source-examples-of-ai-agents&#34;&gt;Open-Source Examples of AI Agents&lt;/h4&gt;
&lt;p&gt;If you want to get hands-on with agents, there are already plenty of open-source projects you can experiment with and build on. Here are a couple of the most notable:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&#34;https://github.com/Significant-Gravitas/AutoGPT&#34;&gt;AutoGPT&lt;/a&gt;&lt;/strong&gt;: One of the earliest and best-known autonomous agents. AutoGPT takes a high-level goal, breaks it down into steps, and tries to complete them—whether that means researching a topic, generating code, or managing a workflow. It comes with a simple frontend for building agents, plus a library of ready-made ones (including some quirky ones, like creating viral videos from Reddit trends).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&#34;https://github.com/reworkd/AgentGPT&#34;&gt;AgentGPT&lt;/a&gt;&lt;/strong&gt;: A browser-based platform that lets you spin up and deploy agents without extra setup. Just type in a goal and watch the agent go—browsing the web, running code, or connecting with external services to move toward the objective.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For a longer list of projects, check out this &lt;a href=&#34;https://huggingface.co/blog/tegridydev/open-source-ai-agents-directory&#34;&gt;open-source AI agents directory on Hugging Face&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;what-are-agentic-applications-and-agent-orchestration&#34;&gt;What Are Agentic Applications and Agent Orchestration?&lt;/h3&gt;
&lt;p&gt;An agentic application is simply an app that uses one or more AI agents to take on complex tasks by itself. Instead of coding every step by hand, you can plug agents together like modules to tackle real-world problems, whether that’s analyzing data, generating content, or building software. The word &amp;ldquo;agentic&amp;rdquo; really just highlights one thing: these systems can act on their own with little human input, following the prompts they are given.&lt;/p&gt;
&lt;p&gt;When you bring more than one agent into the mix, you need a way to coordinate them. This is where orchestration comes in—getting multiple agents to work together smoothly. That might mean giving them specific roles (say, a researcher and a writer), setting up how they communicate, keeping track of what they’ve learned, and making sure they stay on task.&lt;/p&gt;
&lt;p&gt;To make this manageable, orchestration frameworks provide ready-made structures, like graphs, crews, or conversation flows, that organize the collaboration. With these in place, it becomes much easier to grow from a single helpful agent to an entire team working together.&lt;/p&gt;
&lt;h4 id=&#34;open-source-examples-of-agentic-applications-and-orchestration&#34;&gt;Open-Source Examples of Agentic Applications and Orchestration&lt;/h4&gt;
&lt;p&gt;If you want to start building agentic applications yourself, there are already a number of open-source frameworks that can help. Here are some of the most popular:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&#34;https://github.com/crewaiinc/crewai&#34;&gt;CrewAI&lt;/a&gt;&lt;/strong&gt;: A lightweight Python framework for orchestrating agents that work together in teams. You can assign roles, define tasks, and let the agents collaborate—without the bulk of larger frameworks like LangChain. It’s simple, fast, and flexible enough for everything from small experiments to enterprise automations.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&#34;https://github.com/microsoft/autogen&#34;&gt;AutoGen&lt;/a&gt;&lt;/strong&gt;: Created by Microsoft, AutoGen is all about multi-agent conversations. Agents can chat with each other, work independently, or take direction from a human. It also supports tool use like code execution and web browsing, making it handy for more specialized domains like math or chemistry.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;a href=&#34;https://github.com/langchain-ai/langgraph&#34;&gt;LangGraph&lt;/a&gt;&lt;/strong&gt;: Part of the LangChain ecosystem, LangGraph lets you design agents as graphs, which makes them resilient and stateful. It’s well-suited for long-running workflows, offering built-in memory, human-in-the-loop controls, and debugging support through LangSmith—perfect for production-grade projects.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Other frameworks worth exploring include OpenAI’s Agents SDK (built and evolved upon Swarm) for multi-agent workflows, and MetaGPT, which simulates role-based teams of agents.&lt;/p&gt;
&lt;h3 id=&#34;writing-agentic-applications&#34;&gt;Writing Agentic Applications&lt;/h3&gt;
&lt;p&gt;To build an agentic application, the first step is choosing the right framework for your needs, for instance, CrewAI if you want simplicity and lightweight orchestration, or AutoGen if you need extensibility and layered APIs.&lt;/p&gt;
&lt;p&gt;Once you’ve selected a framework, you’ll need to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Define agent roles, e.g., researcher, writer, reviewer. Each agent is an actor to complete one or more tasks.&lt;/li&gt;
&lt;li&gt;Equip agents with tools, like search APIs, code interpreters, or custom functions. As you can see, theoretically there is no limit on what you can do with agents.&lt;/li&gt;
&lt;li&gt;Assign tasks and orchestration rules, deciding how agents will interact, share state, and collaborate.&lt;/li&gt;
&lt;li&gt;Add safeguards, error handling, memory management, and human oversight to ensure reliability and trustworthiness.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;With those elements in place, you can start assembling workflows. Below, we’ll look at how to get started with one of the most widely used frameworks (CrewAI).&lt;/p&gt;
&lt;h5 id=&#34;building-with-crewai&#34;&gt;Building with CrewAI&lt;/h5&gt;
&lt;p&gt;First, install CrewAI and ddgs (duckduckgo-search) which is a free search tool that I used for this example.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;pip install crewai &amp;#39;crewai[tools]&amp;#39; ddgs&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;CrewAI uses decorators for agents and tasks. Below is a simple example of a crew for researching and reporting on a given topic. It defines two agents (a &lt;code&gt;researcher&lt;/code&gt; and a &lt;code&gt;reporting analyst&lt;/code&gt;) and one task for each agent (&lt;code&gt;research task&lt;/code&gt; and &lt;code&gt;reporting task&lt;/code&gt;). This code creates a sequential workflow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The researcher gathers data&lt;/li&gt;
&lt;li&gt;Then, the analyst compiles a report&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can extend it with more agents with more tasks or parallel processes.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;To run this script, you need to set your LLM API key. See how to do that &lt;a href=&#34;https://docs.crewai.com/en/concepts/llms#setting-up-your-llm&#34;&gt;in the crewAI docs&lt;/a&gt;.&lt;/p&gt;&lt;/blockquote&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;typing&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; Type, Optional
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;json&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;crewai&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; Agent, Crew, Process, Task
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;crewai.project&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; CrewBase, agent, crew, task
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;crewai.tools&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; BaseTool
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;pydantic&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; BaseModel, Field
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;ddgs&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; DDGS
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;DDGSearchInput&lt;/span&gt;(BaseModel):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    query: &lt;span style=&#34;color:#038&#34;&gt;str&lt;/span&gt; = Field(..., description=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Search query&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;DDGSearchTool&lt;/span&gt;(BaseTool):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    name: &lt;span style=&#34;color:#038&#34;&gt;str&lt;/span&gt; = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;DuckDuckGo Search&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    description: &lt;span style=&#34;color:#038&#34;&gt;str&lt;/span&gt; = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Web search via DuckDuckGo. Returns a JSON list of results.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    args_schema: Type[BaseModel] = DDGSearchInput
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;# declare this as pydantic field&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    max_results: &lt;span style=&#34;color:#038&#34;&gt;int&lt;/span&gt; = Field(&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;8&lt;/span&gt;, description=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Maximum number of results to return&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;_run&lt;/span&gt;(&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;, query: Optional[&lt;span style=&#34;color:#038&#34;&gt;str&lt;/span&gt;] = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;None&lt;/span&gt;, **kwargs) -&amp;gt; &lt;span style=&#34;color:#038&#34;&gt;str&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; query &lt;span style=&#34;color:#080&#34;&gt;is&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;None&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            query = kwargs.get(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;query&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#080&#34;&gt;not&lt;/span&gt; query:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; json.dumps([], ensure_ascii=&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;False&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        results = []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;with&lt;/span&gt; DDGS() &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;as&lt;/span&gt; ddgs:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;for&lt;/span&gt; r &lt;span style=&#34;color:#080&#34;&gt;in&lt;/span&gt; ddgs.text(query, max_results=&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.max_results):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                results.append({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;title&amp;#34;&lt;/span&gt;: r.get(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;title&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;url&amp;#34;&lt;/span&gt;: r.get(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;href&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                    &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;snippet&amp;#34;&lt;/span&gt;: r.get(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;body&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; json.dumps(results, ensure_ascii=&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;False&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#555&#34;&gt;@CrewBase&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;ResearchCrew&lt;/span&gt;():
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;    Crew of agents for researching and reporting on given topic.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;    &amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;__init__&lt;/span&gt;(&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;, topic):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.topic = topic
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#555&#34;&gt;@agent&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;researcher&lt;/span&gt;(&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;) -&amp;gt; Agent:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; Agent(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            role=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Senior Data Researcher&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            goal=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Uncover cutting-edge developments in &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.topic&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            backstory=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Seasoned researcher skilled in finding relevant information about &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.topic&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;.&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            verbose=&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;True&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            tools=[DDGSearchTool(max_results=&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;10&lt;/span&gt;)]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#555&#34;&gt;@agent&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;reporting_analyst&lt;/span&gt;(&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;) -&amp;gt; Agent:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; Agent(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            role=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Reporting Analyst&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            goal=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Create detailed reports from &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.topic&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt; research findings&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            backstory=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Meticulous analyst who turns &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.topic&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt; data into clear reports.&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            verbose=&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#555&#34;&gt;@task&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;research_task&lt;/span&gt;(&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;) -&amp;gt; Task:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; Task(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            description=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Conduct thorough research on &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.topic&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;.&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            expected_output=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;A list of 10 bullet points with key findings.&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            agent=&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.researcher()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#555&#34;&gt;@task&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;reporting_task&lt;/span&gt;(&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;) -&amp;gt; Task:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; Task(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            description=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Expand the research on &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.topic&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt; into a full report.&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            expected_output=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;A markdown-formatted report with detailed sections.&amp;#39;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            agent=&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.reporting_analyst(),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            output_file=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;ai_report-&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.topic&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;.md&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#555&#34;&gt;@crew&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;crew&lt;/span&gt;(&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;) -&amp;gt; Crew:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&amp;#34;&amp;#34;Assembles the crew.&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; Crew(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            agents=[&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.researcher(), &lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.reporting_analyst()],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            tasks=[&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.research_task(), &lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.reporting_task()],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            process=Process.sequential,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            verbose=&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;True&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;## run the crew of agents&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;__name__&lt;/span&gt; == &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;__main__&amp;#39;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    topic = &lt;span style=&#34;color:#038&#34;&gt;input&lt;/span&gt;(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;Topic: &amp;#39;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    crew = ResearchCrew(topic).crew()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    result = crew.kickoff(inputs={&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;topic&amp;#39;&lt;/span&gt;: topic})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#038&#34;&gt;print&lt;/span&gt;(result)&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id=&#34;building-a-dynamic-agent&#34;&gt;Building A Dynamic Agent&lt;/h4&gt;
&lt;p&gt;We can follow a dynamic approach to create an agentic AI application as well. In this approach, without other 3rd party agent orchestration frameworks we can dynamically create task and agentic processes. Here I present a simple dynamic agent which can work on any given goal until the goal is met. I used OpenAI as the LLM in this example. Using the dynamic approach, the agent crew extracts sub-goals from the given main goal. Then it works on tasks and decides if the goals are met.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;os&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;json&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;openai&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; OpenAI
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;DynamicAgent&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;__init__&lt;/span&gt;(&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;, goal: &lt;span style=&#34;color:#038&#34;&gt;str&lt;/span&gt;, model: &lt;span style=&#34;color:#038&#34;&gt;str&lt;/span&gt; = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;gpt-4o&amp;#34;&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.goal = goal
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.model = model
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.llm = OpenAI(api_key=os.getenv(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;OPENAI_API_KEY&amp;#34;&lt;/span&gt;))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.memory = []  &lt;span style=&#34;color:#888&#34;&gt;# store context and past decisions&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.max_iterations = &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;5&lt;/span&gt;  &lt;span style=&#34;color:#888&#34;&gt;# prevent infinite loops&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;get_llm_response&lt;/span&gt;(&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;, prompt: &lt;span style=&#34;color:#038&#34;&gt;str&lt;/span&gt;) -&amp;gt; &lt;span style=&#34;color:#038&#34;&gt;str&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        resp = &lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.llm.chat.completions.create(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            model=&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.model,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            messages=[
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                {&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;role&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;system&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;content&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;You are an autonomous agent that creates plans and conditions to achieve goals.&amp;#34;&lt;/span&gt;},
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                {&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;role&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;user&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;content&amp;#34;&lt;/span&gt;: prompt}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            ],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            temperature=&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0.2&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        result = &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;None&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;try&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            result = resp.choices[&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0&lt;/span&gt;].message.content
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;except&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;Exception&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#888&#34;&gt;# fallback&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            result = &lt;span style=&#34;color:#038&#34;&gt;str&lt;/span&gt;(resp)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#038&#34;&gt;print&lt;/span&gt;(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;llm result: &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;result&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; result
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;generate_plan&lt;/span&gt;(&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;) -&amp;gt; &lt;span style=&#34;color:#038&#34;&gt;list&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        prompt = (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Given the goal &amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.goal&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;, generate a pure JSON list of actionable steps with conditions for success.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Don&amp;#39;t use any formatting like markdown outside of the JSON content.&lt;/span&gt;&lt;span style=&#34;color:#04d;background-color:#fff0f0&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Each step should include: {&amp;#39;step&amp;#39;: &amp;#39;description&amp;#39;, &amp;#39;condition&amp;#39;: &amp;#39;success criteria&amp;#39;}.&lt;/span&gt;&lt;span style=&#34;color:#04d;background-color:#fff0f0&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Do not rely on predefined rules; infer the steps and conditions from the goal.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        plan = json.loads(&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.get_llm_response(prompt))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.memory.append({&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;action&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;planning&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;output&amp;#34;&lt;/span&gt;: plan})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; plan
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;execute_step&lt;/span&gt;(&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;, step: &lt;span style=&#34;color:#038&#34;&gt;dict&lt;/span&gt;) -&amp;gt; &lt;span style=&#34;color:#038&#34;&gt;str&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        prompt = (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Execute this step: &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;step[&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;step&amp;#39;&lt;/span&gt;]&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#04d;background-color:#fff0f0&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Success condition: &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;step[&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;condition&amp;#39;&lt;/span&gt;]&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#04d;background-color:#fff0f0&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Provide the result and whether the condition was met.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        result = &lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.get_llm_response(prompt)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.memory.append({&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;step&amp;#34;&lt;/span&gt;: step[&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;step&amp;#39;&lt;/span&gt;], &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;result&amp;#34;&lt;/span&gt;: result})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; result
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;run&lt;/span&gt;(&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#038&#34;&gt;print&lt;/span&gt;(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Starting agent with goal: &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.goal&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        plan = &lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.generate_plan()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        results = []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;for&lt;/span&gt; i, step &lt;span style=&#34;color:#080&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;color:#038&#34;&gt;enumerate&lt;/span&gt;(plan[:&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.max_iterations]):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#038&#34;&gt;print&lt;/span&gt;(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Executing step &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;i+&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;: &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;step[&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;step&amp;#39;&lt;/span&gt;]&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            result = &lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.execute_step(step)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            results.append(result)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#038&#34;&gt;print&lt;/span&gt;(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Result: &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;result&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#888&#34;&gt;# check if goal met&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Goal achieved&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#080&#34;&gt;in&lt;/span&gt; result:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;break&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.save_results_to_markdown(results)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;save_results_to_markdown&lt;/span&gt;(&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;, results: &lt;span style=&#34;color:#038&#34;&gt;list&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        filename = &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;dynamic_agent_report-&lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;&lt;span style=&#34;color:#038&#34;&gt;self&lt;/span&gt;.goal&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;.md&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;with&lt;/span&gt; &lt;span style=&#34;color:#038&#34;&gt;open&lt;/span&gt;(filename, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;w&amp;#34;&lt;/span&gt;, encoding=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;utf-8&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;as&lt;/span&gt; f:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            f.write(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#04d;background-color:#fff0f0&#34;&gt;\n&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;.join(results))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        &lt;span style=&#34;color:#038&#34;&gt;print&lt;/span&gt;(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;f&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Results saved to &lt;/span&gt;&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;{&lt;/span&gt;filename&lt;span style=&#34;color:#33b;background-color:#fff0f0&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;__name__&lt;/span&gt; == &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;__main__&amp;#34;&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    goal = &lt;span style=&#34;color:#038&#34;&gt;input&lt;/span&gt;(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Goal: &amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    agent = DynamicAgent(goal=goal)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    agent.run()&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id=&#34;use-cases-of-agentic-ai-applications&#34;&gt;Use Cases of Agentic AI Applications&lt;/h3&gt;
&lt;p&gt;Agentic AI applications are already finding their way into practical scenarios. By combining autonomy with orchestration, these systems go beyond demos and research projects to deliver measurable impact.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Customer Support Automation&lt;/strong&gt;: Instead of static chatbots, agentic systems can act as full-service assistants—resolving customer queries, escalating complex cases, and even initiating refunds or ticket creation. They maintain context across long interactions, providing a more human-like experience.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Research and Analysis&lt;/strong&gt;: Enterprises and individuals use research agents to autonomously scan news sources, academic databases, or financial reports. For example, a financial analyst could deploy agents to track company earnings, summarize investor calls, and produce actionable insights daily.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Content Creation Pipelines&lt;/strong&gt;: Marketing teams benefit from multi-agent setups where one agent researches a topic, another drafts a blog post, and another optimizes it for SEO. The result is faster, higher-quality content production with less manual effort.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Software Development Assistants&lt;/strong&gt;: Multi-agent workflows can handle bug triaging, code generation, testing, and documentation. For instance, one agent detects issues, another suggests fixes, while another runs automated tests and updates docs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Healthcare and Biomedicine&lt;/strong&gt;: Specialized agents can search the latest medical literature, cross-reference patient data, and generate preliminary diagnostic reports, supporting clinicians in decision-making while reducing information overload.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;At End Point, we use agents to speed up development, using human review to keep our company&amp;rsquo;s long-standing commitment to high-quality products. We also build agentic AI solutions for clients, speeding up and modernizing legacy workflows.&lt;/p&gt;
&lt;h3 id=&#34;challenges-ahead&#34;&gt;Challenges Ahead&lt;/h3&gt;
&lt;p&gt;While agentic AI holds promise, several challenges remain before widespread adoption becomes seamless:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reliability and Hallucination&lt;/strong&gt;: LLM-based agents can still generate incorrect or fabricated outputs. Ensuring trustworthiness through validation, feedback loops, and human-in-the-loop oversight is crucial. But, personally I am not against hallucination — We humans hallucinate too.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Security and Safety&lt;/strong&gt;: Autonomous agents that can browse the web, execute code, or control external systems pose risks if misconfigured or exploited. Sandboxing, permissions, and guardrails are essential to prevent harmful actions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Evaluation and Benchmarking&lt;/strong&gt;: Unlike traditional ML models, measuring the success of agentic systems is difficult. Metrics must capture not only accuracy but also task completion, collaboration quality, and user satisfaction.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Ethics and Alignment&lt;/strong&gt;: As agents gain autonomy, aligning them with human values, legal frameworks, and ethical principles becomes critical. Misaligned objectives could cause unintended consequences.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Integration with Legacy Systems&lt;/strong&gt;: Many organizations still rely on older infrastructure. Seamlessly embedding agentic AI into these environments can be a non-trivial engineering challenge.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;wrapping-up&#34;&gt;Wrapping up&lt;/h3&gt;
&lt;p&gt;Agentic AI applications represent the future of intelligent software, blending autonomy with collaboration. By leveraging open-source tools like CrewAI and LangGraph or specialized custom agents, you can create powerful solutions tailored to your needs. As AI evolves, we might expect even more sophisticated orchestration capabilities.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>.NET 10 is Coming: What&#39;s New, and Why It Matters</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/09/dotnet-10-is-coming/"/>
      <id>https://www.endpointdev.com/blog/2025/09/dotnet-10-is-coming/</id>
      <published>2025-09-29T00:00:00+00:00</published>
      <author>
        <name>Juan Pablo Ventoso</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2025/09/dotnet-10-is-coming/palms-and-sunset.webp&#34; alt=&#34;Two palm trees are silhouetted against a blue and orange sunset&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Juan Pablo Ventoso, 2024 --&gt;
&lt;p&gt;Microsoft has everything almost ready for the upcoming release of &lt;a href=&#34;https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-10/overview&#34;&gt;.NET 10&lt;/a&gt; in November this year, and it promises to be a major milestone for developers. Currently in beta, the previews released so far have shown improvements in performance, security, compatibility, and more. Let&amp;rsquo;s take a look and what&amp;rsquo;s coming, and why it matters.&lt;/p&gt;
&lt;h3 id=&#34;performance-and-runtime-improvements&#34;&gt;Performance and runtime improvements&lt;/h3&gt;
&lt;p&gt;One of the core aspects of .NET 10 will be the improved efficiency in different areas. To mention a few of them:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Optimized compression streams such as &lt;code&gt;GZipStream&lt;/code&gt; and async implementations for &lt;code&gt;ZipArchive&lt;/code&gt; methods&lt;/li&gt;
&lt;li&gt;Devirtualization improvements for interface methods, especially when arrays are involved, resulting in faster code execution&lt;/li&gt;
&lt;li&gt;Faster stack allocation for objects through the expanded use of scape analysis&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All of these features (and others) contribute to lower latency and more efficient resource usage. There is a &lt;a href=&#34;https://devblogs.microsoft.com/dotnet/performance-improvements-in-net-10/&#34;&gt;comprehensive blog post&lt;/a&gt; by Stephen Toub, .NET team developer, where he runs several benchmarks proving how these improvements result in faster responses and smaller allocations.&lt;/p&gt;
&lt;h3 id=&#34;security-and-increased-cryptographic-support&#34;&gt;Security and increased cryptographic support&lt;/h3&gt;
&lt;p&gt;.NET 10 also introduces enhancements to encryption and authentication, increasing application security with support for new encryption policies and ways of authentication.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enhanced certificate thumbprint support adds the ability to search certificates by thumbprint using more secure hash algorithms (not just SHA-1)&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://webauthn.io/&#34;&gt;WebAuthn&lt;/a&gt; support enables passwordless logins with secure authentication methods like biometrics or physical security keys&lt;/li&gt;
&lt;li&gt;More cryptographic APIs: Better support for post-quantum cryptography APIs&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;api--web-enhancements&#34;&gt;API &amp;amp; web enhancements&lt;/h3&gt;
&lt;p&gt;For Web APIs, services, and Blazor apps, .NET 10 has several useful features aiming to increase productivity, security, and cloud capabilities.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Minimal APIs now support built-in validation through Data Annotations&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://spec.openapis.org/oas/v3.1.0.html&#34;&gt;OpenAPI 3.1&lt;/a&gt; support: ASP.NET Core in .NET 10 will generate OpenAPI 3.1 documents with the latest JSON Schema specifications&lt;/li&gt;
&lt;li&gt;Several improvements to Blazor assets delivery and environment configuration. One key change is that Blazor scripts will be served as a static web asset&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;ai-cloud-and-devops&#34;&gt;AI, cloud, and DevOps&lt;/h3&gt;
&lt;p&gt;AI developers will also benefit from the new .NET, as well as applications residing in the cloud and serverless environments.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The &lt;a href=&#34;https://dotnet.microsoft.com/en-us/apps/ai/ml-dotnet&#34;&gt;ML.NET Framework&lt;/a&gt; will keep evolving with expanded capabilities&lt;/li&gt;
&lt;li&gt;Cloud Native Development: .NET 10 is designed to be fundamental to cloud native deployments, thanks to smaller base images and the automatic trimming of unused bits&lt;/li&gt;
&lt;li&gt;The &lt;a href=&#34;https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid?view=aspnetcore-9.0&#34;&gt;HybridCache library&lt;/a&gt; unifies in-memory and distributed caching, with tagging for smarter invalidation&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;c-14&#34;&gt;C# 14&lt;/h3&gt;
&lt;p&gt;With .NET 10, Microsoft is also rolling out &lt;a href=&#34;https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-14&#34;&gt;a new version of the C# language&lt;/a&gt;, including several improvements that will result in cleaner syntax and better performance. Let&amp;rsquo;s mention some key features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;User-defined compound assignment operators: We will be able to define our own operators for types like &lt;code&gt;+=&lt;/code&gt;, &lt;code&gt;-=&lt;/code&gt;, etc.&lt;/li&gt;
&lt;li&gt;Using &lt;code&gt;nameof(List&amp;lt;&amp;gt;)&lt;/code&gt; without specifying type arguments if we only need the name, avoiding boilerplate in generic programming and logging&lt;/li&gt;
&lt;li&gt;Partial constructors and partial events, expanding features like partial methods and properties from the previous language versions&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can find more details about all these upcoming changes on the &lt;a href=&#34;https://dotnet.microsoft.com/&#34;&gt;official .NET website&lt;/a&gt;. Based on what we&amp;rsquo;ve seen above, .NET 10 promises to be one of the most significant releases to the .NET ecosystem, consolidating many incremental improvements that will result in measurable gains in performance, security, and productivity.&lt;/p&gt;
&lt;p&gt;I encourage you to explore the &lt;a href=&#34;https://devblogs.microsoft.com/dotnet/dotnet-10-rc-1/&#34;&gt;.NET 10 Release Candidate 1&lt;/a&gt; to try things yourself and get an early look into what&amp;rsquo;s about to come out in the .NET world!&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Making Blog Search Smarter with LLMs and Open WebUI</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/09/llm-expanded-vector-search/"/>
      <id>https://www.endpointdev.com/blog/2025/09/llm-expanded-vector-search/</id>
      <published>2025-09-29T00:00:00+00:00</published>
      <author>
        <name>Edgar Mlowe</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2025/09/llm-expanded-vector-search/stained-glass-flowering.webp&#34; alt=&#34;An ornate pattern flowers out from a circular window in the center of the image, framing plant-shaped stained glass depicting European church images&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2024. --&gt;
&lt;p&gt;We recently released LLM Expanded Search for our blog&amp;rsquo;s vector search. It builds on what we covered in our earlier posts about &lt;a href=&#34;/blog/2025/08/vector-search-for-the-end-point-blog/&#34;&gt;AI-powered search&lt;/a&gt; and &lt;a href=&#34;/blog/2025/07/vector-search/&#34;&gt;vector search basics&lt;/a&gt;. Here&amp;rsquo;s how we built it with our internal AI setup (Open WebUI running an OpenAI-compatible API), why it makes search better, and what&amp;rsquo;s coming next.&lt;/p&gt;
&lt;h3 id=&#34;what-llm-expanded-search-actually-does&#34;&gt;What &amp;ldquo;LLM Expanded Search&amp;rdquo; actually does&lt;/h3&gt;
&lt;p&gt;Here&amp;rsquo;s the basic idea: when you search for something, we first ask an LLM to come up with related terms and phrases. Then we search for all of those terms, not just your original query.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your search gets expanded by an open-source LLM through our AI portal (Open WebUI with an OpenAI-compatible API)&lt;/li&gt;
&lt;li&gt;Those extra terms give our vector index more ways to find posts that match what you&amp;rsquo;re looking for&lt;/li&gt;
&lt;li&gt;We combine the results, remove duplicates, and sort by relevance before showing the best matches with snippets and links&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This really helps with short or vague searches where regular vector search might miss the relevant context — for example, &amp;ldquo;S3&amp;rdquo; refers to Amazon S3, which is a cloud object storage system, so whereas &amp;ldquo;S3&amp;rdquo; doesn&amp;rsquo;t provide enough context for a useful vector search. An LLM can expand this short search and include context about cloud object storage in general, as well as give enough context to return results about S3.&lt;/p&gt;
&lt;h3 id=&#34;how-it-works&#34;&gt;How it works&lt;/h3&gt;
&lt;p&gt;The frontend is pretty straightforward: our search bar has two options, &amp;ldquo;Search&amp;rdquo; (just hit Enter) and &amp;ldquo;LLM Expanded Search&amp;rdquo; (Shift/​Ctrl/​Command+Enter).&lt;/p&gt;
&lt;p&gt;When you use expanded search, here&amp;rsquo;s what happens:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We call our Open WebUI endpoint with a prompt that asks for 8–15 related terms&lt;/li&gt;
&lt;li&gt;We turn both your original query and the expanded terms into embeddings&lt;/li&gt;
&lt;li&gt;We search our vector store with all these terms and combine the results&lt;/li&gt;
&lt;li&gt;Caching and rate limiting keep things fast and cheap&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here&amp;rsquo;s a simple example of how we expand queries:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;from&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;openai&lt;/span&gt; &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; OpenAI
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;os&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;client = OpenAI(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    base_url=os.getenv(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;OPENAI_BASE_URL&amp;#34;&lt;/span&gt;),   &lt;span style=&#34;color:#888&#34;&gt;# e.g., http://openwebui.local/api/v1&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    api_key=os.getenv(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;OPENAI_API_KEY&amp;#34;&lt;/span&gt;)      &lt;span style=&#34;color:#888&#34;&gt;# token managed in your environment&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;def&lt;/span&gt; &lt;span style=&#34;color:#06b;font-weight:bold&#34;&gt;expand_query&lt;/span&gt;(raw_query: &lt;span style=&#34;color:#038&#34;&gt;str&lt;/span&gt;) -&amp;gt; &lt;span style=&#34;color:#038&#34;&gt;list&lt;/span&gt;[&lt;span style=&#34;color:#038&#34;&gt;str&lt;/span&gt;]:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    messages = [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;role&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;system&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;content&amp;#34;&lt;/span&gt;: (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;You expand a short search query into a concise, comma-separated list of &amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;synonyms and closely related phrases (8–15 items). No explanations.&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;            )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        {&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;role&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;user&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;content&amp;#34;&lt;/span&gt;: raw_query}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    ]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    res = client.chat.completions.create(
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        model=os.getenv(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;OPENAI_MODEL&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;local-llm&amp;#34;&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        messages=messages,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        temperature=&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0.2&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        max_tokens=&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;200&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    )
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    text = res.choices[&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0&lt;/span&gt;].message.content
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;return&lt;/span&gt; [t.strip() &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;for&lt;/span&gt; t &lt;span style=&#34;color:#080&#34;&gt;in&lt;/span&gt; text.split(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;,&amp;#34;&lt;/span&gt;) &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; t.strip()]&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After that, we embed the original query and the expanded terms, search the vector index, then sort by score and drop duplicates so each post appears once. Finally, we render concise snippets.&lt;/p&gt;
&lt;p&gt;For example, after a similarity search you can rank and de-duplicate like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# given: results = [(doc, score), ...]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;valid = [(d, &lt;span style=&#34;color:#038&#34;&gt;float&lt;/span&gt;(s)) &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;for&lt;/span&gt; d, s &lt;span style=&#34;color:#080&#34;&gt;in&lt;/span&gt; results &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#038&#34;&gt;float&lt;/span&gt;(s) &amp;gt; &lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;0.05&lt;/span&gt;]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;valid.sort(key=&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;lambda&lt;/span&gt; x: x[&lt;span style=&#34;color:#00d;font-weight:bold&#34;&gt;1&lt;/span&gt;], reverse=&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;True&lt;/span&gt;)  &lt;span style=&#34;color:#888&#34;&gt;# highest score first&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;seen = &lt;span style=&#34;color:#038&#34;&gt;set&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;unique = []
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;for&lt;/span&gt; doc, score &lt;span style=&#34;color:#080&#34;&gt;in&lt;/span&gt; valid:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    src = doc.metadata.get(&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;source&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;if&lt;/span&gt; src &lt;span style=&#34;color:#080&#34;&gt;not&lt;/span&gt; &lt;span style=&#34;color:#080&#34;&gt;in&lt;/span&gt; seen:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        unique.append((doc, score))
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        seen.add(src)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# unique now holds top ranked, de‑duplicated posts&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id=&#34;why-we-chose-open-webui&#34;&gt;Why we chose Open WebUI&lt;/h3&gt;
&lt;p&gt;A few reasons made Open WebUI the right choice:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It&amp;rsquo;s open source and works great self-hosted&lt;/li&gt;
&lt;li&gt;The OpenAI-compatible API means we can drop it into existing code&lt;/li&gt;
&lt;li&gt;We can use whatever models and inference backends we want&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s easy to experiment with different prompts and workflows&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;whats-next-moving-more-into-open-webui&#34;&gt;What&amp;rsquo;s next: Moving more into Open WebUI&lt;/h3&gt;
&lt;p&gt;We&amp;rsquo;re looking into moving more of the search pipeline directly into Open WebUI workflows:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Query expansion (LLM)&lt;/li&gt;
&lt;li&gt;Vector retrieval (custom tool that hits our index)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This would give us tighter integration, fewer network calls, and simpler deployment, and make it easier to try new approaches.&lt;/p&gt;
&lt;h3 id=&#34;what-youll-notice-when-using-it&#34;&gt;What you&amp;rsquo;ll notice when using it&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Short searches work way better, you get more relevant results and fewer dead ends&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s still experimental, so sometimes results might drift into related topics. Stick with regular &amp;ldquo;Search&amp;rdquo; if you want more exact matches&lt;/li&gt;
&lt;li&gt;We cache common terms to keep things smooth&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Give it a try at &lt;a href=&#34;/blog/&#34;&gt;our blog&lt;/a&gt;. Just use the search bar in our header: press Enter for regular search, or Shift/​Ctrl/​Command+Enter for LLM Expanded Search.&lt;/p&gt;
&lt;p&gt;Want to know more about why we built this? Check out the announcement and vector search posts linked above.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re interested in setting up LLM-expanded vector search or running something similar self-hosted with Open WebUI, we&amp;rsquo;d love to &lt;a href=&#34;/contact/&#34;&gt;help out&lt;/a&gt;.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>How to Download All Videos from a YouTube Channel with yt-dlp</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/09/how-to-download-youtube-channel/"/>
      <id>https://www.endpointdev.com/blog/2025/09/how-to-download-youtube-channel/</id>
      <published>2025-09-11T00:00:00+00:00</published>
      <author>
        <name>Seth Jensen</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2025/09/how-to-download-youtube-channel/peak-in-clouds.webp&#34; alt=&#34;A tall peak coated in brown and green short shrubbery, broken up only by sharp faces of rocky cliffs, sits behind wispy clouds, against an overcast backdrop. The ridgeline descends from the top left downward to the right, splitting the image in half&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2023 --&gt;
&lt;p&gt;Recently, one of our clients temporarily lost access to their YouTube account to a potentially bad actor. As we met with them to regain access, we wanted to back up all their videos as quickly as possible to guarantee they retained access to over a decade of videos. Their channel had over 1000 videos, many of which were over an hour long.&lt;/p&gt;
&lt;p&gt;The Google-approved solution here is Google Takeout, but this requires access to the Google account, meaning it wasn&amp;rsquo;t an option. What&amp;rsquo;s more, it only downloads in up to 720p quality (or as low as 360p, &amp;ldquo;depending on the video size&amp;rdquo;)! This was unacceptable for proper archiving. It also told me I need to improve my personal Google backup process, which currently &lt;a href=&#34;/blog/2022/05/backing-up-your-saas-data-with-google-takeout/&#34;&gt;relies on Takeout&lt;/a&gt; 🙃.&lt;/p&gt;
&lt;p&gt;I have used &lt;a href=&#34;https://github.com/yt-dlp/yt-dlp&#34;&gt;yt-dlp&lt;/a&gt; for years, so I knew it would be a good option. It&amp;rsquo;s an open-source command-line tool which is perfect for this situation: you provide the URL of a YouTube video, playlist, or channel and it downloads the corresponding video(s). It&amp;rsquo;s easy to automate and repeat, and is important for archival purposes, personal backups, and situations like ours. The final command we used was:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;yt-dlp &amp;lt;channel URL&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;By default, yt-dlp will download the best video and audio options available — the actual best quality available, not capped at 720p — but you can list the formats with &lt;code&gt;-F&lt;/code&gt;/&lt;code&gt;--list-formats&lt;/code&gt; and manually select formats with &lt;code&gt;-f&lt;/code&gt;/&lt;code&gt;--format&lt;/code&gt;. We first tried downloading only in mp4 format, but since it&amp;rsquo;s a less storage-efficient format than webm, YouTube compresses mp4 more heavily, resulting in a lower-quality video. So we let yt-dlp choose the best option, and usually this meant webm files.&lt;/p&gt;
&lt;p&gt;You can provide command line options via a config file in &lt;code&gt;~/.config/yt-dlp/config&lt;/code&gt;, which makes it easy to automate or to run multiple times with consistent results. Here&amp;rsquo;s what we set up for our download:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;--output &amp;#34;%(upload_date)s - %(title)s.%(ext)s&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;--download-archive archive.txt
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;--embed-metadata
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;--embed-thumbnail
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;--embed-subs
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;--all-subs&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;We included a timestamp in the output filenames, making sorting by time easy&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--download-archive&lt;/code&gt; writes each completed file to the specified archive file (we chose &lt;code&gt;archive.txt&lt;/code&gt; in whatever folder &lt;code&gt;yt-dlp&lt;/code&gt; is run from) so that if you have to rerun the command it efficiently skips these videos&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--embed-metadata&lt;/code&gt; embeds metadata in the file, which is good for archiving&lt;/li&gt;
&lt;li&gt;&lt;code&gt;--embed-thumbnail&lt;/code&gt; and &lt;code&gt;--embed-subs&lt;/code&gt; will change the output muxer to something that supports embedding, like &lt;code&gt;mkv&lt;/code&gt;. &lt;code&gt;--all-subs&lt;/code&gt; makes sure you get all languages and implies the &lt;code&gt;--write-subs&lt;/code&gt; flag&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;You can also save comments using the &lt;code&gt;--write-comments&lt;/code&gt; flag. They will be saved to a JSON info file for each video. You can limit the maximum number of comments to fetch with the &lt;code&gt;max_comments&lt;/code&gt; argument (passed using the &lt;code&gt;--extractor-args&lt;/code&gt; flag).&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;We were running low on disk space on the server we used, so we periodically copied all of the video files to Google Drive using &lt;a href=&#34;https://rclone.org/drive/&#34;&gt;rclone&lt;/a&gt; with the &lt;a href=&#34;https://rclone.org/commands/rclone_move/&#34;&gt;move&lt;/a&gt; subcommand.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;rclone move --exclude=&amp;#39;*.part&amp;#39; --exclude=&amp;#39;*.txt&amp;#39; --progress channel-download/ gdrive:channel-drive-folder/&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If you need to back up your YouTube videos, especially if you don&amp;rsquo;t have access to your account anymore, consider using yt-dlp!&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Using esbuild and pnpm to Set Up Frontend Asset Bundling in an ASP.NET Core Web App</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/08/using-esbuild-to-set-up-frontend-asset-bundling-in-an-asp-net-core-web-app/"/>
      <id>https://www.endpointdev.com/blog/2025/08/using-esbuild-to-set-up-frontend-asset-bundling-in-an-asp-net-core-web-app/</id>
      <published>2025-08-25T00:00:00+00:00</published>
      <author>
        <name>Kevin Campusano</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2025/08/using-esbuild-to-set-up-frontend-asset-bundling-in-an-asp-net-core-web-app/mountains.webp&#34; alt=&#34;Layers of mountains are exposed by light with a red tint&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2025. --&gt;
&lt;p&gt;Recently we had to build an admin dashboard on ASP.NET Core. We decided to hit the ground running by looking for a template online. Luckily, we found a very cool one from Start Bootstrap that &lt;a href=&#34;https://startbootstrap.com/template/sb-admin&#34;&gt;met our requirements&lt;/a&gt;. It was built using &lt;a href=&#34;https://getbootstrap.com/&#34;&gt;Bootstrap&lt;/a&gt;, had nice styling, and included examples of graphs, tables, and basic common pages like login and registration.&lt;/p&gt;
&lt;p&gt;However, integrating the template into an ASP.NET Core web app was a bit involved. The template uses tools like &lt;a href=&#34;https://pugjs.org/api/getting-started.html&#34;&gt;Pug&lt;/a&gt; and &lt;a href=&#34;https://sass-lang.com/documentation/syntax/&#34;&gt;Sass/SCSS&lt;/a&gt;, which are not supported out of the box by ASP.NET. So some work had to be done to properly integrate it.&lt;/p&gt;
&lt;p&gt;We ended up with a nice approach for handling frontend asset bundling using &lt;a href=&#34;https://pnpm.io/&#34;&gt;pnpm&lt;/a&gt; and &lt;a href=&#34;https://esbuild.github.io/&#34;&gt;esbuild&lt;/a&gt;, with support for JavaScript and Sass.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You can find a codebase that uses the setup we&amp;rsquo;ll be discussing here on &lt;a href=&#34;https://github.com/megakevin/razor-start-bootstrap-admin&#34;&gt;Github&lt;/a&gt;. It&amp;rsquo;s an empty ASP.NET Core Razor Pages project that implements Start Bootrap&amp;rsquo;s admin template. Feel free to review it alongside this article and/or use it for your own projects.&lt;/p&gt;&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;We&amp;rsquo;ve also &lt;a href=&#34;https://www.nuget.org/packages/EndPointDev.RazorEsbuildProjectTemplate&#34;&gt;uploaded to NuGet&lt;/a&gt; a template for an empty ASP.NET Core Razor Pages web app that has implemented the esbuild-based frontend asset bundling.&lt;/p&gt;&lt;/blockquote&gt;
&lt;h3 id=&#34;installing-pnpm-and-the-necessary-packages&#34;&gt;Installing pnpm and the necessary packages&lt;/h3&gt;
&lt;p&gt;First of all we need to have the &lt;a href=&#34;https://dotnet.microsoft.com/en-us/download&#34;&gt;.NET framework installed&lt;/a&gt;. We also need to &lt;a href=&#34;https://nodejs.org/en/download&#34;&gt;install Node.js&lt;/a&gt; and &lt;a href=&#34;https://pnpm.io/installation&#34;&gt;the pnpm package manager&lt;/a&gt;.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-text&#34; data-lang=&#34;text&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ dotnet --version
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;9.0.302
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ node --version
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;v22.18.0
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ pnpm --version
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;10.14.0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Once we have those, we need a Razor Pages project to apply the changes to. For our purposes, I&amp;rsquo;m going to assume we&amp;rsquo;re starting off with a fresh project, created using something like &lt;code&gt;dotnet new webapp&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;With that out of the way, we can create a &lt;code&gt;package.json&lt;/code&gt; file in the root of our project:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ pnpm init
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Wrote to /path/to/code/package.json
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;demo&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;version&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;1.0.0&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;description&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;main&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;index.js&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;scripts&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;test&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;echo \&amp;#34;Error: no test specified\&amp;#34; &amp;amp;&amp;amp; exit 1&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;keywords&amp;#34;&lt;/span&gt;: [],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;author&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;license&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;ISC&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;packageManager&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;pnpm@10.14.0&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We can adjust the resulting &lt;code&gt;package.json&lt;/code&gt; to better reflect our project. Maybe remove the &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;scripts.test&lt;/code&gt; settings, which we don&amp;rsquo;t need. Maybe also set a &lt;code&gt;name&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In any case, now that we have this, we can install the Node.js packages that we need. I mentioned that we were going to use esbuild and SCSS so let&amp;rsquo;s install those as dev dependencies:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ pnpm add esbuild esbuild-sass-plugin sass@1.78.0 --save-dev
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Packages: +41
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+++++++++++++++++++++++++++++++++++++++++
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Progress: resolved 103, reused 50, downloaded 0, added 41, &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;devDependencies:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+ esbuild 0.25.9
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+ esbuild-sass-plugin 3.3.1
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+ sass 1.78.0 (1.90.0 is available)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;╭ Warning ───────────────────────────────────────────────────────────────────────────────────╮
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;│                                                                                            │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;│   Ignored build scripts: esbuild.                                                          │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;│   Run &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;pnpm approve-builds&amp;#34;&lt;/span&gt; to pick which dependencies should be allowed to run scripts.   │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;│                                                                                            │
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;╰────────────────────────────────────────────────────────────────────────────────────────────╯
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Done in 2.5s using pnpm v10.14.0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;I&amp;rsquo;ve pinned the version of the &lt;code&gt;sass&lt;/code&gt; package to &lt;code&gt;1.78.0&lt;/code&gt; in order to work around some compatibility warnings with Bootstrap&amp;rsquo;s stylesheets. You may or may not need to do this in your own projects.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;We have to address the warning message and allow &lt;code&gt;esbuild&lt;/code&gt; to run scripts. Running &lt;code&gt;pnpm approve-builds&lt;/code&gt; and selecting &lt;code&gt;esbuild&lt;/code&gt; (by pressing either the &amp;ldquo;Space&amp;rdquo; or &amp;ldquo;a&amp;rdquo; keys) takes care of that:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ pnpm approve-builds
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;✔ Choose which packages to build (Press &amp;lt;space&amp;gt; to &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;select&lt;/span&gt;, &amp;lt;a&amp;gt; to toggle all, &amp;lt;i&amp;gt; to invert selection) · esbuild
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;✔ The next packages will now be built: esbuild.
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Do you approve? (y/N) · &lt;span style=&#34;color:#038&#34;&gt;true&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;node_modules/.pnpm/esbuild@0.25.9/node_modules/esbuild: Running postinstall script, &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;done&lt;/span&gt; in 40ms&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With that, the &lt;code&gt;package.json&lt;/code&gt; file should be looking like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// ./package.json
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;demo&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;version&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;1.0.0&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;description&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;scripts&amp;#34;&lt;/span&gt;: {},
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;keywords&amp;#34;&lt;/span&gt;: [],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;author&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;license&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;ISC&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;packageManager&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;pnpm@10.14.0&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;devDependencies&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;esbuild&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;^0.25.9&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;esbuild-sass-plugin&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;^3.3.1&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;sass&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;1.78.0&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There should also be a new &lt;code&gt;pnpm-workspace.yaml&lt;/code&gt; file that contains the setting for the approval we gave for &lt;code&gt;esbuild&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;# ./pnpm-workspace.yaml&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;onlyBuiltDependencies&lt;/span&gt;:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;  &lt;/span&gt;- esbuild&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In this particular example, we&amp;rsquo;re adapting Start Bootrap&amp;rsquo;s admin template, so we&amp;rsquo;re going to install the packages that it needs:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-sh&#34; data-lang=&#34;sh&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ pnpm add @fortawesome/fontawesome-free bootstrap chart.js jquery simple-datatables
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Packages: +9
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+++++++++
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Progress: resolved 112, reused 59, downloaded 0, added 9, &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;dependencies:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+ @fortawesome/fontawesome-free 7.0.0
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+ bootstrap 5.3.7
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+ chart.js 4.5.0
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+ jquery 3.7.1
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+ simple-datatables 10.0.0
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;Done in 2.9s using pnpm v10.14.0&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;These are the ones we need for this project but of course, in your own projects you can install whatever you want.&lt;/p&gt;
&lt;h3 id=&#34;authoring-javascript-and-scss-source-files&#34;&gt;Authoring JavaScript and SCSS source files&lt;/h3&gt;
&lt;p&gt;Now that we have the Node.js part of the project set up, we need a place where we can put our JavaScript and SCSS files. For that, we created two new directories: &lt;code&gt;JavaScript&lt;/code&gt; and &lt;code&gt;Stylesheets&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Since we&amp;rsquo;re adapting Start Bootrap&amp;rsquo;s admin template, we copied all its SCSS files into the &lt;code&gt;Stylesheets&lt;/code&gt; directory and put its JavaScript into the &lt;code&gt;JavaScript&lt;/code&gt; directory. It all ended up looking like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; .
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+├── JavaScript
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+│   └── site.js
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;&lt;/span&gt; ├── Pages
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+├── Stylesheets
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+│   ├── layout
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+│   ├── navigation
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+│   ├── plugins
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+│   ├── variables
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+│   ├── _global.scss
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+│   ├── site.scss
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+│   └── _variables.scss
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;&lt;/span&gt; ├── wwwroot
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; │   ├── css
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; │   ├── js
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; │   └── favicon.ico
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; ├── appsettings.Development.json
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; ├── appsettings.json
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+├── package.json
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+├── pnpm-lock.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;+├── pnpm-workspace.yaml
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#000;background-color:#dfd&#34;&gt;&lt;/span&gt; ├── Program.cs
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; ├── RazorStartBootstrapAdmin.csproj
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt; └── README.md
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Of course, the specific file contents will be different in your own projects, but the take home message is that these two new directories are meant for the source files of your frontend assets.&lt;/p&gt;
&lt;h3 id=&#34;bundling-frontend-assets-with-esbuild&#34;&gt;Bundling frontend assets with esbuild&lt;/h3&gt;
&lt;p&gt;With that, we have the NodeJS package management ready, the tools we need, and a project directory structure that supports writing JavaScript and SCSS.&lt;/p&gt;
&lt;p&gt;Now we need to put together an &lt;code&gt;esbuild.config.mjs&lt;/code&gt; file that processes these files and produces the bundles. Here it is:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// ./esbuild.config.mjs
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; * as esbuild from &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;esbuild&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; { sassPlugin } from &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;esbuild-sass-plugin&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// SCSS
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// This is a list of all the SCSS files that we want to bundle and their
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// respective output files. In this project&amp;#39;s case, we have only one.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// We put the resulting file under ./wwwroot/css. &amp;#34;wwwroot&amp;#34; is the default
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// location used to expose files to the public in ASP.NET Core Razor Pages
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// projects.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; scssFiles = [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  { entry: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;./Stylesheets/site.scss&amp;#39;&lt;/span&gt;, outfile: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;./wwwroot/css/site.css&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// Here we tell esbuild to take the entry SCSS files as input and produce CSS
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// files ready to be interpreted by a browser. Since the source files are SCSS,
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// we use the esbuild-sass-plugin plugin to translate them into CSS.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;scssFiles.forEach(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;async&lt;/span&gt; file =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; esbuild.build({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    entryPoints: [file.entry],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    bundle: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    sourcemap: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// Some libraries, like Bootstrap, include icons as woff and woff2 font
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// files. To handle this, we specify this loader setting. Instructing
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// esbuild to use the &amp;#34;file&amp;#34; loader, when it encounters such files. The
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// &amp;#34;file&amp;#34; loader takes care of copying the files into the output
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// location.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// More info: https://esbuild.github.io/content-types/#file
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;    loader: { &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;.woff&amp;#39;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;file&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;.woff2&amp;#39;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;file&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    outfile: file.outfile,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    plugins: [sassPlugin()]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// JavaScript
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// Similar to the stylesheet files, for JavaScript we also declare a list of the
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// files that we want to bundle.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; jsFiles = [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  { entry: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;./JavaScript/site.js&amp;#39;&lt;/span&gt;, outfile: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;./wwwroot/js/site.js&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// Bundling here is simpler than SCSS because we don&amp;#39;t need custom loaders or
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// plugins. Just specify entry points and output files and esbuild knows what to
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// do.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;jsFiles.forEach(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;async&lt;/span&gt; file =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; esbuild.build({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    entryPoints: [file.entry],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    bundle: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    sourcemap: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    outfile: file.outfile,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;One important thing to note here is that with &lt;code&gt;esbuild&lt;/code&gt;, we can safely use JavaScript modules and &lt;code&gt;import&lt;/code&gt; statements for both JavaScript and SCSS. This means that our frontend logic and styling rules can be broken up into any number of files, forming a tree of dependencies through &lt;code&gt;import&lt;/code&gt; statements. We need only to specify the root files, or entry points, and &lt;code&gt;esbuild&lt;/code&gt; takes care of building a complete and self-contained bundle. Not really a revolutionary idea in the grand scheme of things, but not always a given when working with JavaScript on the browser.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p&gt;With this, we can trigger the bundling process: &lt;code&gt;node esbuild.config.mjs&lt;/code&gt;. However, it&amp;rsquo;d be nice to have it better integrated with a typical .NET development flow. For that, we can add the following under &lt;code&gt;scripts&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// ./package.json
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;scripts&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;build&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;node esbuild.config.mjs&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This allows us to run &lt;code&gt;pnpm run build&lt;/code&gt; instead.&lt;/p&gt;
&lt;p&gt;Finally, we can have this command run automatically as part of &lt;code&gt;dotnet build&lt;/code&gt; if we add the following to our &lt;code&gt;.csproj&lt;/code&gt; file:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-xml&#34; data-lang=&#34;xml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&amp;lt;!-- ./RazorStartBootstrapAdmin.csproj --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;Project&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;Sdk=&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Microsoft.NET.Sdk.Web&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;Target&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;Name=&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;BundleFrontendAssets&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;BeforeTargets=&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Build&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;Exec&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;Command=&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;pnpm install&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;Exec&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;Command=&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;pnpm run build&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/Target&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/Project&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;All this will result in our build process producing &lt;code&gt;./wwwroot/css/site.css&lt;/code&gt;, and &lt;code&gt;./wwwroot/js/site.js&lt;/code&gt; bundles. Which we can include in our &lt;code&gt;.cshtml&lt;/code&gt; files like we would any other &lt;code&gt;.js&lt;/code&gt; or &lt;code&gt;.css&lt;/code&gt; file:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&amp;lt;!-- ./Pages/Shared/_Layout.cshtml --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;link&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;rel&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;href&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;~/css/site.css&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;asp-append-version&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;script&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;src&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;~/js/site.js&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;asp-append-version&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;script&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id=&#34;importing-libraries-as-modules&#34;&gt;Importing libraries as modules&lt;/h3&gt;
&lt;p&gt;One interesting aspect to note is that, with &lt;code&gt;esbuild&lt;/code&gt;, we are using &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules&#34;&gt;JavaScript modules&lt;/a&gt; with &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#importing_features_into_your_script&#34;&gt;&lt;code&gt;import&lt;/code&gt; statements&lt;/a&gt;. This is particularly important for libraries, since some of them won&amp;rsquo;t necessarily support being included as JavaScript modules by default or in an obvious manner.&lt;/p&gt;
&lt;p&gt;Our &lt;code&gt;site.js&lt;/code&gt; demonstrates this. For example, it loads Bootstrap and the Font Awesome icons using the following statements:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// ./JavaScript/site.js
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;bootstrap&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;@fortawesome/fontawesome-free/js/all&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This makes it so any page that includes &lt;code&gt;site.js&lt;/code&gt; will also have Bootstrap and the Font Awesome icons available.&lt;/p&gt;
&lt;p&gt;To get the same for something like JQuery, for example, the strategy is a little different:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// ./JavaScript/site.js
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; jQuery from &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;jquery&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#038&#34;&gt;window&lt;/span&gt;.$ = &lt;span style=&#34;color:#038&#34;&gt;window&lt;/span&gt;.jQuery = jQuery;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A similar scenario happens with the stylesheets. For SCSS, we have to use the &lt;code&gt;@import&lt;/code&gt; statement. Our &lt;code&gt;site.scss&lt;/code&gt; contains examples of this. Here&amp;rsquo;s how we load Bootstrap&amp;rsquo;s CSS, for instance:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-scss&#34; data-lang=&#34;scss&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// ./Stylesheets/site.scss
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// ...
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;@import&lt;/span&gt; &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;bootstrap/scss/bootstrap.scss&amp;#34;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;// &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;...&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In your projects, you&amp;rsquo;ll encounter libraries with varying levels of support for JavaScript modules, and you will have to adapt accordingly, depending on the library itself and how you want to use it.&lt;/p&gt;
&lt;h3 id=&#34;importing-libraries-via-html-tags&#34;&gt;Importing libraries via HTML tags&lt;/h3&gt;
&lt;p&gt;There are also some libraries that only work via traditional &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; tags. For these, you might want to have &lt;code&gt;esbuild&lt;/code&gt; copy files directly from the &lt;code&gt;node_modules&lt;/code&gt; directory into &lt;code&gt;wwwroot&lt;/code&gt;, without any preprocessing. You could use something like this in your &lt;code&gt;esbuild.config.mjs&lt;/code&gt; file for that:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// ./esbuild.config.mjs
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// Raw files
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; rawFiles = [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  { entry: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;./node_modules/path/to/package.js&amp;#39;&lt;/span&gt;, outfile: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;./wwwroot/lib/package.js&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  { entry: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;./node_modules/path/to/package.css&amp;#39;&lt;/span&gt;, outfile: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;./wwwroot/lib/package.css&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;rawFiles.forEach(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;async&lt;/span&gt; file =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; esbuild.build({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    entryPoints: [file.entry],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    bundle: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;false&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    sourcemap: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;false&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    loader: { &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;.*&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;copy&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    outfile: file.outfile
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then you can include the files from &lt;code&gt;wwwroot&lt;/code&gt; using the appropriate &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;link rel=&amp;quot;stylesheet&amp;quot;&amp;gt;&lt;/code&gt; tags.&lt;/p&gt;
&lt;h3 id=&#34;summary&#34;&gt;Summary&lt;/h3&gt;
&lt;p&gt;So, in the end, using &lt;code&gt;esbuild&lt;/code&gt; and &lt;code&gt;pnpm&lt;/code&gt; to set up frontend asset bundling in an ASP.NET Core web app is perfectly doable and can be summarized like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Install NodeJS and &lt;code&gt;pnpm&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create a &lt;code&gt;package.json&lt;/code&gt; file with &lt;code&gt;pnpm init&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Install &lt;code&gt;esbuild&lt;/code&gt; as a dev dependency with &lt;code&gt;pnpm add esbuild --save-dev&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Also make sure to run &lt;code&gt;pnpm approve-builds&lt;/code&gt; to allow &lt;code&gt;esbuild&lt;/code&gt; to run scripts&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Install any additional preprocessor that you might need like &lt;code&gt;sass&lt;/code&gt; with &lt;code&gt;esbuild-sass-plugin&lt;/code&gt;, etc.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Create &lt;code&gt;JavaScript&lt;/code&gt; and &lt;code&gt;Stylesheets&lt;/code&gt; directories and put your source files in them&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add an &lt;code&gt;esbuild.config.mjs&lt;/code&gt; file that can bundle your assets&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Include the asset bundling step as part of the &lt;code&gt;dotnet build&lt;/code&gt; command&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Your package.json should end up looking something like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-json&#34; data-lang=&#34;json&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;version&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;1.0.0&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;description&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;scripts&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// Register this so that esbuild can be called with &amp;#34;pnpm run build&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;build&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;node esbuild.config.mjs&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;keywords&amp;#34;&lt;/span&gt;: [],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;author&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;license&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;ISC&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;dependencies&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// Add your dependencies here.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;  },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;devDependencies&amp;#34;&lt;/span&gt;: {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;esbuild&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;0.25.9&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#888&#34;&gt;// Add other preprocessors and plugins here, like:
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;esbuild-sass-plugin&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;3.3.1&amp;#34;&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;#34;sass&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;1.78.0&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  }
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;}&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here&amp;rsquo;s an example of an &lt;code&gt;esbuild.config.mjs&lt;/code&gt; that covers the common scenarios:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-javascript&#34; data-lang=&#34;javascript&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; * as esbuild from &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;esbuild&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;import&lt;/span&gt; { sassPlugin } from &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;esbuild-sass-plugin&amp;#39;&lt;/span&gt;;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// SCSS
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; scssFiles = [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  { entry: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;./Stylesheets/site.scss&amp;#39;&lt;/span&gt;, outfile: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;./wwwroot/css/site.css&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;// Add other stylesheet files here.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;scssFiles.forEach(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;async&lt;/span&gt; file =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; esbuild.build({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    entryPoints: [file.entry],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    bundle: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    sourcemap: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    loader: { &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;.woff&amp;#39;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;file&amp;#39;&lt;/span&gt;, &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;.woff2&amp;#39;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;file&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    outfile: file.outfile,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    plugins: [sassPlugin()]
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// JavaScript
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; jsFiles = [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  { entry: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;./JavaScript/site.js&amp;#39;&lt;/span&gt;, outfile: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;./wwwroot/js/site.js&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;// Add other JavaScript files here.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;jsFiles.forEach(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;async&lt;/span&gt; file =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; esbuild.build({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    entryPoints: [file.entry],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    bundle: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    sourcemap: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;true&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    outfile: file.outfile,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;// Raw files
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;const&lt;/span&gt; rawFiles = [
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  { entry: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;./node_modules/path/to/package.js&amp;#39;&lt;/span&gt;, outfile: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;./wwwroot/lib/package.js&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  { entry: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;./node_modules/path/to/package.css&amp;#39;&lt;/span&gt;, outfile: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;./wwwroot/lib/package.css&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;// Add other library files here.
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#888&#34;&gt;&lt;/span&gt;];
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;rawFiles.forEach(&lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;async&lt;/span&gt; file =&amp;gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;await&lt;/span&gt; esbuild.build({
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    entryPoints: [file.entry],
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    bundle: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;false&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    sourcemap: &lt;span style=&#34;color:#080;font-weight:bold&#34;&gt;false&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    loader: { &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;.*&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#39;copy&amp;#39;&lt;/span&gt; },
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    outfile: file.outfile
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  });
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is the addition that the &lt;code&gt;.csproj&lt;/code&gt; file needs in order to bundle the frontend assets during &lt;code&gt;dotnet build&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-xml&#34; data-lang=&#34;xml&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;Project&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;Sdk=&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Microsoft.NET.Sdk.Web&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#888&#34;&gt;&amp;lt;!-- ... --&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;Target&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;Name=&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;BundleFrontendAssets&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;BeforeTargets=&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;Build&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;Exec&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;Command=&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;pnpm install&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;Exec&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;Command=&lt;/span&gt;&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;pnpm run build&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/Target&amp;gt;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;&amp;lt;/Project&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With that, you&amp;rsquo;re free to add the bundles and raw files in your pages using traditional methods like:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;link&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;rel&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;href&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;~/css/site.css&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;asp-append-version&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt; /&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;link&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;rel&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;stylesheet&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;href&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;~/lib/package.css&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;asp-append-version&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt; /&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and&amp;hellip;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;background-color:#fff;-moz-tab-size:4;-o-tab-size:4;tab-size:4;&#34;&gt;&lt;code class=&#34;language-html&#34; data-lang=&#34;html&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;script&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;src&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;~/js/site.js&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;asp-append-version&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;script&lt;/span&gt;&amp;gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&amp;lt;&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;script&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;src&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;~/lib/package.js&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#369&#34;&gt;asp-append-version&lt;/span&gt;=&lt;span style=&#34;color:#d20;background-color:#fff0f0&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;&amp;gt;&amp;lt;/&lt;span style=&#34;color:#b06;font-weight:bold&#34;&gt;script&lt;/span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;As you create more entrypoint JavaScript and SCSS files, you have to remember to add them to their respective arrays in &lt;code&gt;esbuild.config.mjs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;You can add more packages using &lt;code&gt;pnpm add &amp;lt;package_name&amp;gt;&lt;/code&gt;. Just remember that they have to be imported by your JavaScript files as modules. If you need to add raw files, especially from libraries that don&amp;rsquo;t support being imported as JavaScript modules, the &lt;code&gt;esbuild.config.mjs&lt;/code&gt; file also supports that thanks to the &amp;ldquo;raw files&amp;rdquo; section. These raw files can be loaded normally via &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; HTML tags.&lt;/p&gt;
&lt;p&gt;All right, that&amp;rsquo;s it for now. We&amp;rsquo;ve seen how to integrate &lt;code&gt;esbuild&lt;/code&gt; with ASP.NET Core in order to setup a basic frontend asset bundling process. We did that through adapting Smart Bootstrap&amp;rsquo;s free admin template into an ASP.NET Core Razor Pages web app project — a project that&amp;rsquo;s up on &lt;a href=&#34;https://github.com/megakevin/razor-start-bootstrap-admin&#34;&gt;GitHub&lt;/a&gt;. We saw how to organize source files in our repo, a basic &lt;code&gt;esbuild&lt;/code&gt; config script that handles the most common scenarios, and how to handle a few different cases when it comes to including NodeJS packages into our apps.&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Announcing End Point Ecommerce, Our New Open Source Project</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/08/announcing-end-point-ecommerce-our-new-open-source-project/"/>
      <id>https://www.endpointdev.com/blog/2025/08/announcing-end-point-ecommerce-our-new-open-source-project/</id>
      <published>2025-08-21T00:00:00+00:00</published>
      <author>
        <name>Kevin Campusano</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2025/08/announcing-end-point-ecommerce-our-new-open-source-project/railing-blue-sky.webp&#34; alt=&#34;A white plastered railing from a low angle, behind which is a pure blue sky. The railing comes down and to the right from the top left corner of the image, then has a right angle in the center of the image, coming back up and to the right for another quarter of the image before stopping.&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2025. --&gt;
&lt;p&gt;Today we&amp;rsquo;re pleased to announce a new open source project: End Point Ecommerce.&lt;/p&gt;
&lt;p&gt;End Point Ecommerce is a minimalist ecommerce system that&amp;rsquo;s quick to set up and easy to understand. It is meant for developers to own, adapt, customize, and extend.&lt;/p&gt;
&lt;p&gt;You can learn more about it on &lt;a href=&#34;https://ecommerce.endpointdev.com/&#34;&gt;our landing page&lt;/a&gt;. You can also fork it today on &lt;a href=&#34;https://github.com/EndPointCorp/end-point-ecommerce&#34;&gt;GitHub&lt;/a&gt;. There are running demos here:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://demo.ecommerce.endpointdev.com/swagger/index.html&#34;&gt;REST API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://admin.demo.ecommerce.endpointdev.com&#34;&gt;Admin Portal&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://demo.ecommerce.endpointdev.com&#34;&gt;Web Store&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Are you interested in using End Point Ecommerce for your ecommerce site? Feel free to &lt;a href=&#34;/contact/&#34;&gt;contact us&lt;/a&gt; and we can work together to help you deploy, customize, and maintain it.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/08/announcing-end-point-ecommerce-our-new-open-source-project/epec_swagger.webp&#34; alt=&#34;REST API&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/08/announcing-end-point-ecommerce-our-new-open-source-project/epec_admin_portal.webp&#34; alt=&#34;Admin Portal&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;/blog/2025/08/announcing-end-point-ecommerce-our-new-open-source-project/epec_web_store.webp&#34; alt=&#34;Web Store&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;key-features&#34;&gt;Key features&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Built with &lt;a href=&#34;https://dotnet.microsoft.com/en-us/apps/aspnet&#34;&gt;ASP.NET&lt;/a&gt;, &lt;a href=&#34;https://www.postgresql.org/&#34;&gt;PostgreSQL&lt;/a&gt;, and &lt;a href=&#34;https://www.authorize.net/&#34;&gt;Authorize.net&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Multiple deployment strategies, including &lt;a href=&#34;https://docs.docker.com/compose/&#34;&gt;Docker Compose&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Code base designed to follow &lt;a href=&#34;https://learn.microsoft.com/en-us/dotnet/architecture/modern-web-apps-azure/common-web-application-architectures#clean-architecture&#34;&gt;Clean Architecture&lt;/a&gt; and &lt;a href=&#34;https://learn.microsoft.com/en-us/archive/msdn-magazine/2009/february/best-practice-an-introduction-to-domain-driven-design&#34;&gt;Domain Driven Design&lt;/a&gt; concepts, without being dogmatic about it — simplicity trumps all&lt;/li&gt;
&lt;li&gt;Good test coverage with &lt;a href=&#34;https://xunit.net/&#34;&gt;xUnit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Includes a backend admin portal for managing the store, a REST API for building user-facing store frontends, and a simple web frontend&lt;/li&gt;
&lt;li&gt;Limited functionality — implements the bare minimum of ecommerce use cases&lt;/li&gt;
&lt;li&gt;Product catalog, shopping cart, order submission, email delivery&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;how-we-got-here&#34;&gt;How we got here&lt;/h3&gt;
&lt;p&gt;We recently had the opportunity to help one of our clients migrate their ecommerce site. They were using an established ecommerce engine and were having various problems with it. Their requirements were simple, and they didn&amp;rsquo;t need all the bells and whistles of big ecommerce solutions. They didn&amp;rsquo;t want to deal with the overhead, bloat, and difficulty of finding maintainers.&lt;/p&gt;
&lt;p&gt;During initial investigation and planning, we didn&amp;rsquo;t find any options that were small and easy to set up, understand, and customize, so we decided to build them a new site from scratch.&lt;/p&gt;
&lt;p&gt;We realize that there may be others out there in the same boat that we were. You want to set up a custom ecommerce site but you don&amp;rsquo;t want to deal with the complexity of bigger off-the-shelf solutions. You want something which you can thoroughly understand quickly, and modify to your heart&amp;rsquo;s content, as if it were your own code base that you developed from scratch.&lt;/p&gt;
&lt;h3 id=&#34;who-is-this-for&#34;&gt;Who is this for?&lt;/h3&gt;
&lt;p&gt;This project is meant for .NET developers who want a solid and simple foundation to build and customize ecommerce sites. To that end, the feature set is very basic, the code base is very straightforward, and the documentation is very developer-oriented. The idea being that developers can quickly get up to speed with how everything works, develop ownership over the code base, and implement their bespoke use cases.&lt;/p&gt;
&lt;p&gt;This project is not meant for non-technical folks. There is no installation wizard, no templating engine, no plugin framework, no no-code customization capabilities.&lt;/p&gt;
&lt;p&gt;If you want to set up a simple online store, or develop a highly customized one from scratch, think of this as a way of skipping the first few steps of building the foundational architecture and basic functionality.&lt;/p&gt;
&lt;p&gt;Please check it out! And do whatever you want with it!&lt;/p&gt;

      </content>
    </entry>
  
    <entry>
      <title>Risks of Paper Checks for Secure Transactions</title>
      <link rel="alternate" href="https://www.endpointdev.com/blog/2025/08/risks-of-paper-checks-for-secure-transactions/"/>
      <id>https://www.endpointdev.com/blog/2025/08/risks-of-paper-checks-for-secure-transactions/</id>
      <published>2025-08-18T00:00:00+00:00</published>
      <author>
        <name>Joanne Tipton</name>
      </author>
      <content type="html">
        &lt;p&gt;&lt;img src=&#34;/blog/2025/08/risks-of-paper-checks-for-secure-transactions/rail-barrier-opening.webp&#34; alt=&#34;A rail barrier is in the process of opening, creating a strong line from the bottom right to the top left of the image. Behind is a train on the tracks in from of a deep gold sunrise&#34;&gt;&lt;/p&gt;
&lt;!-- Photo by Seth Jensen, 2025 --&gt;
&lt;p&gt;Fraud is on the rise, and paper checks are one of the easiest targets. In the not-so-distant past, a grandparent could tuck a crisp $5 bill into an envelope and trust it would arrive. Today, even checks sent to pay your bills are being stolen, altered, and cashed by criminals before they reach their destination.&lt;/p&gt;
&lt;p&gt;So, the practice of sending a paper check in the mail to reach a creditor should be stopped. According to the US Financial Crimes Enforcement Network (FinCEN), there were &lt;strong&gt;over 660,000 check fraud reports in 2024&lt;/strong&gt;. This number has stayed in the 600,000s every year since 2022, when it nearly doubled from the previous year.&lt;sup id=&#34;fnref:1&#34;&gt;&lt;a href=&#34;#fn:1&#34; class=&#34;footnote-ref&#34; role=&#34;doc-noteref&#34;&gt;1&lt;/a&gt;&lt;/sup&gt; The U.S. Postal Service has seen similar increases in mail theft, with criminals targeting collection boxes and even robbing postal carriers for paper checks.&lt;/p&gt;
&lt;p&gt;Fraudsters have many ways to manipulate checks. Intercepting checks that are not in a secured location by simply paying attention to where mail is lying around is at the top of the list. Once they get a check in hand, they can “wash” original amounts and payees and replace them with higher numbers and their own names.&lt;/p&gt;
&lt;p&gt;A recent incident at our company showed how risky paper checks can be. A check mailed to End Point by one of our long time customers was intercepted and fraudulently cashed by someone else. We contacted the customer multiple times to collect on the open invoice. They were sure they had paid the invoice. When they sent us a photo of the front and back of the cancelled check, we could see that an unknown person had endorsed the check and cashed it! In some cases, the sender is left holding the financial loss with no way to recover the funds.&lt;/p&gt;
&lt;p&gt;At End Point, we don’t cash customer checks in person — all payments are deposited directly into our business accounts. While this adds some protection, it’s useless if the check never reaches us.&lt;/p&gt;
&lt;p&gt;The safest way to pay today is electronically. &lt;strong&gt;We strongly encourage customers to switch to secure electronic methods&lt;/strong&gt; such as ACH transfers or direct deposits. If a check is unavoidable, send it via Certified Mail with tracking. Protecting your payment protects both your business and ours.&lt;/p&gt;
&lt;div class=&#34;footnotes&#34; role=&#34;doc-endnotes&#34;&gt;
&lt;hr&gt;
&lt;ol&gt;
&lt;li id=&#34;fn:1&#34;&gt;
&lt;p&gt;Data aggregated from &lt;a href=&#34;https://www.fincen.gov/reports/sar-stats&#34;&gt;https://www.fincen.gov/reports/sar-stats&lt;/a&gt;&amp;#160;&lt;a href=&#34;#fnref:1&#34; class=&#34;footnote-backref&#34; role=&#34;doc-backlink&#34;&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;

      </content>
    </entry>
  
</feed>
