<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Dan Guisinger</title>
    <description>The latest articles on DEV Community by Dan Guisinger (@dguisinger).</description>
    <link>https://dev.to/dguisinger</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1366515%2Fb2970319-665f-416e-b381-6f4f5449b766.jpeg</url>
      <title>DEV Community: Dan Guisinger</title>
      <link>https://dev.to/dguisinger</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dguisinger"/>
    <language>en</language>
    <item>
      <title>Calling a Public API Gateway Custom Domain from a Private VPC (Without Reconfiguring Your Clients)</title>
      <dc:creator>Dan Guisinger</dc:creator>
      <pubDate>Wed, 28 Jan 2026 20:43:54 +0000</pubDate>
      <link>https://dev.to/dguisinger/calling-a-public-api-gateway-custom-domain-from-a-private-vpc-without-reconfiguring-your-clients-2n35</link>
      <guid>https://dev.to/dguisinger/calling-a-public-api-gateway-custom-domain-from-a-private-vpc-without-reconfiguring-your-clients-2n35</guid>
      <description>&lt;p&gt;&lt;em&gt;&lt;a href="https://medium.com/@dguisinger/calling-a-public-api-gateway-custom-domain-from-a-private-vpc-b395f861ffa3" rel="noopener noreferrer"&gt;Originally published on Medium&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There’s a deceptively simple requirement that AWS API Gateway still doesn’t support cleanly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Use a public, regional API Gateway with a custom domain, and allow private VPC workloads to call that same domain without changing client configuration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you’ve tried this, you’ve probably been sent in circles—by documentation, AI tools, and even AWS recommendations—toward solutions that either don’t deploy or don’t work.&lt;/p&gt;

&lt;p&gt;This post documents what actually breaks, why most suggested approaches fail, and the pragmatic workaround that finally solved it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;You have a public API Gateway endpoint exposed as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://api.yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That domain is used everywhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Browsers&lt;/li&gt;
&lt;li&gt;External consumers&lt;/li&gt;
&lt;li&gt;Shared SDKs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You also have private workloads (Lambda, ECS, EC2) running in isolated subnets with no NAT gateway. Those workloads need to call the same hostname, not an execute-api URL, and not an environment-specific variant.&lt;/p&gt;

&lt;p&gt;Changing the base URL internally would fracture client configuration and defeat the point of having a stable public domain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the obvious solutions fail
&lt;/h2&gt;

&lt;p&gt;The first approach most people try is an interface VPC endpoint for execute-api, combined with a Route 53 private hosted zone that resolves api.yourdomain.com to the endpoint inside the VPC.&lt;/p&gt;

&lt;p&gt;DNS-wise, this works. Traffic routes privately. The request never touches the internet.&lt;/p&gt;

&lt;p&gt;But the TLS handshake fails.&lt;/p&gt;

&lt;p&gt;At that point, nothing reaches API Gateway:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No API is identified&lt;/li&gt;
&lt;li&gt;No routing occurs&lt;/li&gt;
&lt;li&gt;No authentication runs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;TLS negotiation fails before HTTP exists.&lt;/p&gt;




&lt;h2&gt;
  
  
  “Just use private custom domains”
&lt;/h2&gt;

&lt;p&gt;This recommendation comes up constantly—and it’s simply incorrect for this scenario.&lt;/p&gt;

&lt;p&gt;API Gateway private custom domains only work with private API endpoints. Domain associations explicitly require the API itself to be private. You cannot attach a private custom domain to a public regional API.&lt;/p&gt;

&lt;p&gt;CloudFormation rejects it.&lt;br&gt;
The console won’t allow it.&lt;br&gt;
There is no workaround.&lt;/p&gt;

&lt;p&gt;This is a product limitation, not a configuration error.&lt;/p&gt;


&lt;h2&gt;
  
  
  “Use the execute-api endpoint internally”
&lt;/h2&gt;

&lt;p&gt;That works technically, but it forces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Different base URLs per environment&lt;/li&gt;
&lt;li&gt;API IDs and stage names in clients&lt;/li&gt;
&lt;li&gt;Special-case logic in shared SDKs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For systems designed around a single public domain, this is architectural debt.&lt;/p&gt;


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

&lt;p&gt;Once you strip away the noise, the issue is very small and very specific.&lt;/p&gt;

&lt;p&gt;When you access API Gateway through a VPC endpoint, the endpoint presents a TLS certificate for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*.execute-api.{region}.amazonaws.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even if you connect using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://api.yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The certificate chain is valid.&lt;br&gt;
The issuer is trusted.&lt;br&gt;
The only failure is &lt;strong&gt;hostname mismatch&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And because TLS fails, API Gateway never sees the request at all.&lt;/p&gt;


&lt;h2&gt;
  
  
  The pragmatic workaround
&lt;/h2&gt;

&lt;p&gt;If you accept that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You are connecting to an AWS-owned service&lt;/li&gt;
&lt;li&gt;Over a VPC endpoint&lt;/li&gt;
&lt;li&gt;Using AWS-issued certificates&lt;/li&gt;
&lt;li&gt;With DNS fully under your control&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…then the fix is to &lt;strong&gt;explicitly accept execute-api certificates&lt;/strong&gt; when the only TLS error is a name mismatch.&lt;/p&gt;

&lt;p&gt;This preserves:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;One public domain&lt;/li&gt;
&lt;li&gt;One client configuration&lt;/li&gt;
&lt;li&gt;Private routing without NAT&lt;/li&gt;
&lt;li&gt;Full encryption in transit&lt;/li&gt;
&lt;/ul&gt;

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


&lt;h2&gt;
  
  
  Applying this in .NET
&lt;/h2&gt;
&lt;h2&gt;
  
  
  Configuring the HTTP client
&lt;/h2&gt;

&lt;p&gt;This is the missing piece you called out earlier.&lt;br&gt;
The TLS behavior must be applied when registering the HttpClient.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddHttpClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ApiClient"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureForVpcEndpoint&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Any SDK or service using this client will now tolerate API Gateway’s certificate when accessed via a VPC endpoint.&lt;/p&gt;




&lt;h3&gt;
  
  
  The HTTP client handler
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;IHttpClientBuilder&lt;/span&gt; &lt;span class="nf"&gt;ConfigureForVpcEndpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt; &lt;span class="n"&gt;IHttpClientBuilder&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigurePrimaryHttpMessageHandler&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
        &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;SocketsHttpHandler&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;SslOptions&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="n"&gt;SslClientAuthenticationOptions&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="n"&gt;RemoteCertificateValidationCallback&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt;
                    &lt;span class="n"&gt;ValidateApiGatewayCertificate&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&gt;```&lt;/span&gt;
&lt;span class="p"&gt;{%&lt;/span&gt; &lt;span class="n"&gt;endraw&lt;/span&gt; &lt;span class="p"&gt;%}&lt;/span&gt;


&lt;span class="err"&gt;##&lt;/span&gt; &lt;span class="n"&gt;Certificate&lt;/span&gt; &lt;span class="n"&gt;validation&lt;/span&gt;
&lt;span class="p"&gt;{%&lt;/span&gt; &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="p"&gt;%}&lt;/span&gt;


&lt;span class="err"&gt;```&lt;/span&gt;&lt;span class="n"&gt;csharp&lt;/span&gt;
&lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="nf"&gt;ValidateApiGatewayCertificate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="kt"&gt;object&lt;/span&gt; &lt;span class="n"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;X509Certificate&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;certificate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;X509Chain&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="n"&gt;chain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;SslPolicyErrors&lt;/span&gt; &lt;span class="n"&gt;sslPolicyErrors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sslPolicyErrors&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;SslPolicyErrors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sslPolicyErrors&lt;/span&gt; &lt;span class="p"&gt;==&lt;/span&gt; &lt;span class="n"&gt;SslPolicyErrors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RemoteCertificateNameMismatch&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
        &lt;span class="n"&gt;certificate&lt;/span&gt; &lt;span class="p"&gt;!=&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;subject&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;certificate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Subject&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"execute-api"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
            &lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Contains&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"amazonaws.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;StringComparison&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrdinalIgnoreCase&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This does not disable TLS validation. It narrowly allows a known, trusted AWS certificate that fails hostname validation only because API Gateway cannot present your custom-domain certificate over a VPC endpoint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this isn’t as scary as it looks
&lt;/h2&gt;

&lt;p&gt;This is safe because &lt;em&gt;it’s constrained&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The trust chain is still validated&lt;/li&gt;
&lt;li&gt;Only AWS API Gateway certificates are accepted&lt;/li&gt;
&lt;li&gt;DNS is private and controlled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Traffic never leaves AWS’s network&lt;/p&gt;

&lt;p&gt;This is not a blanket “ignore SSL errors” hack—it’s compensating for a missing feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real takeaway
&lt;/h2&gt;

&lt;p&gt;There is currently &lt;strong&gt;no supported way&lt;/strong&gt; to combine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;public regional API Gateway&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;custom domain&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Private VPC access&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;single, stable hostname&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Correct TLS without client changes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Until AWS fills that gap, this workaround is the cleanest solution I’ve found—and the only one that doesn’t require re-architecting clients around internal-only URLs.&lt;/p&gt;

&lt;p&gt;Sometimes the hardest problems aren’t misconfigurations.&lt;br&gt;
They’re the seams where cloud abstractions stop.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;&lt;em&gt;About the author&lt;/em&gt;&lt;/strong&gt;: &lt;a href="https://danguisinger.com" rel="noopener noreferrer"&gt;Dan Guisinger&lt;/a&gt; is a consultant specializing in system and security architecture on AWS.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>apigateway</category>
      <category>vpc</category>
      <category>security</category>
    </item>
    <item>
      <title>Building FluentDynamoDB in 45 Days with Kiro - A Hackathon Story</title>
      <dc:creator>Dan Guisinger</dc:creator>
      <pubDate>Thu, 04 Dec 2025 07:45:11 +0000</pubDate>
      <link>https://dev.to/dguisinger/building-fluentdynamodb-in-45-days-with-kiro-a-hackathon-story-41i9</link>
      <guid>https://dev.to/dguisinger/building-fluentdynamodb-in-45-days-with-kiro-a-hackathon-story-41i9</guid>
      <description>&lt;p&gt;&lt;em&gt;This is the story of how a massive .NET DynamoDB library went from an empty repo to production-ready in 45 days.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When I started building &lt;a href="https://fluentdynamodb.dev" rel="noopener noreferrer"&gt;FluentDynamoDB&lt;/a&gt;, I expected it to take months or even a year to reach a usable state. It’s an extremely large .NET library: source generators, expression translation, composite keys, geospatial indexing, encryption, stream handling, and thousands of tests across nine packages.&lt;/p&gt;

&lt;p&gt;Instead… it took 45 days.&lt;/p&gt;

&lt;p&gt;The only reason that was possible is Kiro.&lt;/p&gt;

&lt;p&gt;This post covers what I built, how Kiro enabled it, and why the project literally could not exist without an AI-augmented workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Overview: FluentDynamoDB
&lt;/h2&gt;

&lt;p&gt;FluentDynamoDB is a modern, strongly-typed DynamoDB toolkit for .NET. It includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Source-generated entity models&lt;/li&gt;
&lt;li&gt;Lambda-expression to DynamoDB request translation&lt;/li&gt;
&lt;li&gt;Composite key modeling&lt;/li&gt;
&lt;li&gt;Encryption + S3-backed blob storage&lt;/li&gt;
&lt;li&gt;A complete stream-processing framework&lt;/li&gt;
&lt;li&gt;Geospatial lookup via GeoHash, S2, and H3&lt;/li&gt;
&lt;li&gt;Thousands of automated tests&lt;/li&gt;
&lt;li&gt;Nine NuGet packages that work together as a unified ecosystem&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not a “weekend hackathon” project.&lt;br&gt;
It is a production-grade library, built fast — because the workflow was different.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Kiro Workflow — Requirements → Design → Tasks
&lt;/h2&gt;

&lt;p&gt;Kiro’s “Spec” system is the most important part of how this project got built.&lt;/p&gt;

&lt;h3&gt;
  
  
  requirements.md → What the system must do
&lt;/h3&gt;

&lt;p&gt;Every major feature begins with EARS-style requirement statements defining:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;expected behaviors&lt;/li&gt;
&lt;li&gt;constraints&lt;/li&gt;
&lt;li&gt;edge cases&lt;/li&gt;
&lt;li&gt;business rules and functional intent&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  design.md → How it should be built
&lt;/h3&gt;

&lt;p&gt;This is where architectural decisions took shape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;source generation boundaries&lt;/li&gt;
&lt;li&gt;lambda-expression normalization rules&lt;/li&gt;
&lt;li&gt;composite key representation&lt;/li&gt;
&lt;li&gt;consistency models&lt;/li&gt;
&lt;li&gt;geospatial data structures&lt;/li&gt;
&lt;li&gt;AOT safety considerations&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  tasks.md → What Kiro should implement
&lt;/h3&gt;

&lt;p&gt;Each task references:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the requirement it fulfills&lt;/li&gt;
&lt;li&gt;the design decision that governs how it should be implemented&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Kiro then generates the implementation while staying aligned to the spec.&lt;/p&gt;

&lt;p&gt;Working alone, that scale is nearly impossible without something like Kiro.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Hardest Part: Geospatial Encoding with Multiple LLMs
&lt;/h2&gt;

&lt;p&gt;I had zero geospatial background going in. I knew GeoHash existed, but that was the extent of it.&lt;/p&gt;

&lt;p&gt;Kiro made it possible to build a complete geospatial feature set — but not without major challenges.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Auto Model + GeoHash
&lt;/h3&gt;

&lt;p&gt;Using Kiro’s default “Auto” model, it implemented GeoHash reasonably well. But during development, Kiro suggested:&lt;/p&gt;

&lt;p&gt;“You may also want to consider Google S2 or Uber H3 for geospatial precision.”&lt;/p&gt;

&lt;p&gt;Great idea... in theory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: First Attempts at S2 and H3 (Failure)
&lt;/h3&gt;

&lt;p&gt;I let Kiro implement S2 and H3, and both implementations failed badly. It produced:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;inconsistent cell conversions&lt;/li&gt;
&lt;li&gt;mismatched bit shifts&lt;/li&gt;
&lt;li&gt;incorrect spherical coordinate handling&lt;/li&gt;
&lt;li&gt;decoding errors on specific edge-case coordinates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Auto-mode models simply got lost in the complexity.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3: Switching to Claude Opus 4.5 + the Clear-Thought MCP
&lt;/h3&gt;

&lt;p&gt;When Anthropic released Claude Opus 4.5, I retried the work with a different approach:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Break the problem into steps
&lt;/h3&gt;

&lt;p&gt;Using the clear-thought MCP plugin, I forced structured workflows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;normalization rules&lt;/li&gt;
&lt;li&gt;intermediate coordinate transforms&lt;/li&gt;
&lt;li&gt;step-by-step verification&lt;/li&gt;
&lt;li&gt;invariants for S2 and H3 cell relationships&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  2. Opus’s reasoning was noticeably stronger
&lt;/h3&gt;

&lt;p&gt;It could explain concepts coherently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Hilbert curves&lt;/li&gt;
&lt;li&gt;space-filling patterns&lt;/li&gt;
&lt;li&gt;face/cell transformations&lt;/li&gt;
&lt;li&gt;spherical geometry relationships&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. But we still had edge cases
&lt;/h3&gt;

&lt;p&gt;A handful of stubborn coordinates would not encode/decode correctly.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 4: Deep Debugging Using Reference C Code
&lt;/h3&gt;

&lt;p&gt;This is where Kiro became absolutely essential.&lt;/p&gt;

&lt;p&gt;I loaded the original Google S2 and Uber H3 C reference code into the Kiro workspace and had Opus:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;read them side-by-side&lt;/li&gt;
&lt;li&gt;trace execution paths&lt;/li&gt;
&lt;li&gt;compare intermediate values&lt;/li&gt;
&lt;li&gt;identify mismatched conversions&lt;/li&gt;
&lt;li&gt;reason through floating-point differences&lt;/li&gt;
&lt;li&gt;reconcile the two implementations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After several cycles, it found the issues - subtle mathematical transformation details I could not have discovered without deep math knowledge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Result
&lt;/h2&gt;

&lt;p&gt;S2 + H3 now both work in FluentDynamoDB.&lt;/p&gt;

&lt;p&gt;And I will be very blunt:&lt;/p&gt;

&lt;p&gt;I do not have the mathematical or domain background to implement S2 or H3 manually. This feature simply would not exist in FluentDynamoDB without Kiro.&lt;/p&gt;

&lt;p&gt;It also consumed over &lt;strong&gt;1,000 Kiro credits&lt;/strong&gt; on this set of Specs alone — but it was worth every one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Steering Documents: Quietly Powerful
&lt;/h2&gt;

&lt;p&gt;This project didn’t need as many steering rules as full-stack work,&lt;br&gt;
but one rule was essential:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“After any public API change, update the corresponding documentation and append a note to the documentation changelog.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because I have a separate Kiro workspace that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reads that changelog&lt;/li&gt;
&lt;li&gt;updates the web documentation&lt;/li&gt;
&lt;li&gt;keeps everything in sync&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is still an experiment, but so far it has been extremely successful.&lt;br&gt;
It ensures documentation never lags behind the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Project Scale (Powered by Kiro)
&lt;/h2&gt;

&lt;p&gt;Here’s what was produced during Kiroween:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;160,000+ lines of C#&lt;/li&gt;
&lt;li&gt;100,000+ lines of documentation, specs, and steering documents&lt;/li&gt;
&lt;li&gt;~4,000 automated tests&lt;/li&gt;
&lt;li&gt;9 NuGet packages&lt;/li&gt;
&lt;li&gt;5 example applications&lt;/li&gt;
&lt;li&gt;40+ Specs&lt;/li&gt;
&lt;li&gt;Used Multiple LLMs per feature&lt;/li&gt;
&lt;li&gt;Over 10,000 Kiro credits consumed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the output of a multi-team engineering project —&lt;br&gt;
delivered by one person using AI-augmented development —&lt;br&gt;
for a total cost of around $400.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.youtube.com/watch?v=4-SI6YgSX_s" rel="noopener noreferrer"&gt;Kiroween Hackathon Video&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/oproto/fluent-dynamodb" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned During Kiroween
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. AI-assisted engineering isn’t the future, it’s the present.
&lt;/h3&gt;

&lt;p&gt;This wasn’t theory. This was shipping real software.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Distributed system features become accessible to developers who lack domain expertise.
&lt;/h3&gt;

&lt;p&gt;The S2/H3 experience is proof.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Specs &amp;gt; prompts.
&lt;/h3&gt;

&lt;p&gt;Kiro’s structured workflow is genuinely transformative.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Good engineering + good AI &amp;gt; either alone.
&lt;/h3&gt;

&lt;h2&gt;
  
  
  What’s Next
&lt;/h2&gt;

&lt;p&gt;FluentDynamoDB’s first stable NuGet release is coming soon.&lt;br&gt;
We'll be continuing to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add runtime DynamoDB schema validation&lt;/li&gt;
&lt;li&gt;improve our FluentResults coverage over the full API surface&lt;/li&gt;
&lt;li&gt;build more sample applications&lt;/li&gt;
&lt;li&gt;keep scaling the spec-driven development workflow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you’re participating in Kiroween, I’d love to compare notes.&lt;/p&gt;

&lt;p&gt;— Dan&lt;/p&gt;

</description>
      <category>kiro</category>
      <category>dynamodb</category>
      <category>dotnet</category>
    </item>
  </channel>
</rss>
