<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Architecture on Serhii Chechun - Developer&#39;s Blog</title>
    <link>https://blog.chechun.org/tags/architecture/</link>
    <description>Recent content in Architecture on Serhii Chechun - Developer&#39;s Blog</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <copyright>© 2026 Serhii Chechun</copyright>
    <lastBuildDate>Sun, 20 Sep 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://blog.chechun.org/tags/architecture/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>HTTP APIs in Go: Architecture, Lifecycle &amp; Layered Testing</title>
      <link>https://blog.chechun.org/posts/http-api-in-go/</link>
      <pubDate>Sun, 20 Sep 2026 00:00:00 +0000</pubDate>
      
      <guid>https://blog.chechun.org/posts/http-api-in-go/</guid>
      <description>&lt;p&gt;Every Go codebase has one: a &lt;code&gt;main.go&lt;/code&gt; that reads the environment, opens a database, registers
thirty routes inline, starts a goroutine that mutates shared state, and exits the moment the process
is asked to stop. It works. It ships. And then the deploy pipeline gets a bit faster, the pod is
replaced mid-request, and a handful of users get a &lt;code&gt;502&lt;/code&gt; they will never be able to explain.&lt;/p&gt;</description>
      <content>&lt;p&gt;Every Go codebase has one: a &lt;code&gt;main.go&lt;/code&gt; that reads the environment, opens a database, registers
thirty routes inline, starts a goroutine that mutates shared state, and exits the moment the process
is asked to stop. It works. It ships. And then the deploy pipeline gets a bit faster, the pod is
replaced mid-request, and a handful of users get a &lt;code&gt;502&lt;/code&gt; they will never be able to explain.&lt;/p&gt;
&lt;p&gt;The failure is not a bug in any single line. It is the absence of decisions that were never made
explicitly: where does the SQL live, who owns the request deadline, what happens between &lt;code&gt;SIGTERM&lt;/code&gt;
and process exit, and what exactly does each test in the example prove.&lt;/p&gt;
&lt;p&gt;This post walks through a small but complete HTTP API in Go - a hotel room booking service - and
follows those decisions through the code. The full example is the
&lt;a href=&#34;https://github.com/serhii-chechun/developers-blog/tree/main/programming/go/http-api/examples/hotel-booking-api&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;&lt;code&gt;hotel-booking-api&lt;/code&gt; example&lt;/a&gt;
:
a self-contained Go 1.27 module with Gin, PostgreSQL, &lt;code&gt;go-sqlmock&lt;/code&gt;, mockery v3, a Taskfile, Docker
Compose and a distroless image. Everything below is copied from it, not paraphrased.&lt;/p&gt;
&lt;p&gt;The organizing claim is simple: &lt;strong&gt;a layered structure is only worth its cost if each layer makes a
&lt;em&gt;different kind&lt;/em&gt; of test possible.&lt;/strong&gt; If it does not, it is a folder, not an architecture.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The Golden Rule of layering:&lt;/strong&gt; every layer must be &lt;em&gt;individually replaceable&lt;/em&gt;. If replacing the
repository with an in-memory fake requires touching the handler, the layers are decorative.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;1-module--project-layout&#34;&gt;1. Module &amp;amp; Project Layout&lt;/h2&gt;
&lt;h3 id=&#34;11-the-module-root-is-the-example-root&#34;&gt;1.1 The module root is the example root&lt;/h3&gt;
&lt;p&gt;The module is self-contained: &lt;code&gt;go.mod&lt;/code&gt; sits at the root of &lt;code&gt;examples/hotel-booking-api&lt;/code&gt;, and nothing
above it in the tree takes part in the build:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#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;hotel-booking-api/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-- .mockery.yml
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-- Dockerfile
&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;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-- Taskfile.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-- docker-compose.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-- go.mod
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-- go.sum
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-- cmd/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   +-- booking-api/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-- internal/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   +-- app/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   |   +-- server/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   |   +-- storage/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   |   |   +-- postgres/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   |   +-- testing/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   |       +-- handler/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   |       +-- model/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   |       +-- service/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   +-- helper/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   |   +-- date/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   |   +-- pagination/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   +-- hotel/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   |   +-- handler/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   |   +-- model/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   |   +-- repository/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   |   +-- service/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   +-- reservation/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   |   +-- handler/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   |   +-- model/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   |   +-- repository/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   |   +-- service/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   |   +-- worker/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|   +-- room/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|       +-- handler/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|       +-- model/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|       +-- repository/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;|       +-- service/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;+-- specs/
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    +-- openapi.yaml
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    +-- postgres.sql
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Three properties follow from that single decision, and all three are load-bearing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Everything is inside the module.&lt;/strong&gt; &lt;code&gt;go build ./...&lt;/code&gt; and &lt;code&gt;go test ./...&lt;/code&gt;, run from the example
root, cover everything there is - no &lt;code&gt;cd&lt;/code&gt; into a subdirectory, no remembering which folder is &amp;ldquo;the
real project&amp;rdquo;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One module, one dependency graph.&lt;/strong&gt; A single &lt;code&gt;go.mod&lt;/code&gt; at the root means a test-only dependency
cannot drift into code that ships, and the module can move - another directory, another
repository - without a single import changing.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The Docker build context is the module root&lt;/strong&gt;, which is why the Dockerfile can copy &lt;code&gt;cmd&lt;/code&gt;
and &lt;code&gt;internal&lt;/code&gt; and nothing else.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note also the deliberate absence of &lt;code&gt;pkg/&lt;/code&gt;. Nothing here is reusable by another module - the whole
module is an application, so everything belongs under &lt;code&gt;internal/&lt;/code&gt;, which Go&amp;rsquo;s compiler already
enforces: a package under &lt;code&gt;internal/&lt;/code&gt; cannot be imported from outside the module. &lt;code&gt;internal/&lt;/code&gt; is the
default; exporting is the exception.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A note on the word itself.&lt;/strong&gt; That collision between the two meanings of &amp;ldquo;repository&amp;rdquo; is worth
naming once: from here on a bare &lt;em&gt;repository&lt;/em&gt; means the data-access layer described in section 2.1,
and the code this post is about is called the &lt;em&gt;example&lt;/em&gt;.&lt;/p&gt;
&lt;h3 id=&#34;12-the-module-path-is-a-plain-name&#34;&gt;1.2 The module path is a plain name&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;module&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;hotel&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;booking&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;api&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;go&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;1.27&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;One line, and every import in the project stays short - no hostname, no owner, no path segment to
repeat:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;hotel-booking-api/internal/hotel/model&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:#e6db74&#34;&gt;&amp;#34;hotel-booking-api/internal/helper/pagination&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;The convention of naming a module after the URL it is cloned from exists so that other people can
resolve it as a dependency. &lt;strong&gt;This is an application, not a library.&lt;/strong&gt; Nothing imports it, so that
convention buys nothing here: &lt;code&gt;go get&lt;/code&gt;-ability is not a feature for a binary, it is a tax paid on
every import line and every rename - and it would tie the module path to wherever the code happens
to be hosted today. The day part of this code is published as a reusable package is the day the
module path earns a hostname.&lt;/p&gt;
&lt;h3 id=&#34;13-dev-tooling-as-dependencies&#34;&gt;1.3 Dev tooling as dependencies&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;tool&lt;/code&gt; directive in &lt;code&gt;go.mod&lt;/code&gt; (Go 1.24+) pins the dev CLI tools inside the module itself:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;tool&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;github&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;com&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;go&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;task&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;task&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;v3&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;cmd&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#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:#a6e22e&#34;&gt;github&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;com&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;vektra&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;mockery&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;/&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;v3&lt;/span&gt;
&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;Nothing has to be installed globally or version-managed separately - &lt;code&gt;go tool task&lt;/code&gt; and
&lt;code&gt;go tool mockery&lt;/code&gt; always resolve to exactly the versions in &lt;code&gt;go.sum&lt;/code&gt;. The honest cost: the tool
dependency graph is large (the task runner alone pulls in a TUI stack, an AWS SDK and OpenTelemetry
as indirect requirements), so &lt;code&gt;go.sum&lt;/code&gt; is noisy and the first &lt;code&gt;go mod download&lt;/code&gt; is slower than it
looks. The alternative - installing tools by hand, or via a version manager - is a longer README
paragraph and a class of &amp;ldquo;works on my machine&amp;rdquo; bug that &lt;code&gt;go.mod&lt;/code&gt; eliminates for free. Also note that
&lt;code&gt;tool&lt;/code&gt; directives require Go 1.24+, which is one reason the module pins &lt;code&gt;go 1.27&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The other two files at the module root are &lt;code&gt;specs/openapi.yaml&lt;/code&gt; (the contract) and
&lt;code&gt;specs/postgres.sql&lt;/code&gt; (the schema) - and the same SQL file is mounted as the Compose initialization
script, so there is exactly one source of truth for each concern.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;2-the-four-layers&#34;&gt;2. The Four Layers&lt;/h2&gt;
&lt;h3 id=&#34;21-responsibilities&#34;&gt;2.1 Responsibilities&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Layer&lt;/th&gt;
					&lt;th&gt;Package&lt;/th&gt;
					&lt;th&gt;Responsibility&lt;/th&gt;
					&lt;th&gt;Test double&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Handler&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;internal/&amp;lt;domain&amp;gt;/handler&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;Bind and validate input, call the service, map the result to a status code and JSON&lt;/td&gt;
					&lt;td&gt;mockery mock of the domain service&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Service&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;internal/&amp;lt;domain&amp;gt;/service&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;Business rules, orchestration, invariants&lt;/td&gt;
					&lt;td&gt;mockery mock of the repository&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Repository&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;internal/&amp;lt;domain&amp;gt;/repository&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;SQL only; rows to models and back&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;go-sqlmock&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Helper&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;internal/helper/{date,pagination}&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;Pure functions used by all layers&lt;/td&gt;
					&lt;td&gt;none needed - direct table tests&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;The dependency arrow only ever points inward&lt;/strong&gt;: handler -&amp;gt; service -&amp;gt; repository -&amp;gt; &lt;code&gt;sql.DB&lt;/code&gt;. No
layer knows its caller, and the repository has never heard of HTTP.&lt;/p&gt;
&lt;h3 id=&#34;22-domain-first-not-layer-first&#34;&gt;2.2 Domain-first, not layer-first&lt;/h3&gt;
&lt;p&gt;The alternative layout - &lt;code&gt;internal/handler/hotel.go&lt;/code&gt;, &lt;code&gt;internal/service/hotel.go&lt;/code&gt; - groups by
technical role. This example groups by domain instead: &lt;code&gt;internal/hotel/{handler,service,repository,model}&lt;/code&gt;.&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Layout&lt;/th&gt;
					&lt;th&gt;Adding a feature touches&lt;/th&gt;
					&lt;th&gt;Deleting a feature&lt;/th&gt;
					&lt;th&gt;Cost&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Layer-first&lt;/td&gt;
					&lt;td&gt;one file in each of three directories&lt;/td&gt;
					&lt;td&gt;carefully, three deletions&lt;/td&gt;
					&lt;td&gt;shared package namespace, so every domain name collides&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Domain-first (chosen)&lt;/td&gt;
					&lt;td&gt;one directory&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;rm -rf internal/room&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;import aliases at the composition root&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Flat (one package per service)&lt;/td&gt;
					&lt;td&gt;one file&lt;/td&gt;
					&lt;td&gt;nothing&lt;/td&gt;
					&lt;td&gt;no seams, no layers, and every test is an integration test&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The cost is real and visible in the composition root, which imports five packages named &lt;code&gt;handler&lt;/code&gt;,
three named &lt;code&gt;service&lt;/code&gt; and three named &lt;code&gt;repository&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;import&lt;/span&gt; (
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;handlerHotel&lt;/span&gt;       &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;hotel-booking-api/internal/hotel/handler&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:#a6e22e&#34;&gt;handlerReservation&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;hotel-booking-api/internal/reservation/handler&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:#a6e22e&#34;&gt;handlerRoom&lt;/span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;hotel-booking-api/internal/room/handler&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;That is the price of domain-first: aliases in exactly one file (the wiring file), in exchange for
short, unambiguous package names everywhere else.&lt;/p&gt;
&lt;h3 id=&#34;23-interfaces-belong-to-the-consumer&#34;&gt;2.3 Interfaces belong to the consumer&lt;/h3&gt;
&lt;p&gt;The Go idiom is usually quoted as &lt;strong&gt;&amp;ldquo;accept interfaces, return structs&amp;rdquo;&lt;/strong&gt; - and it has a second half
that matters more here. &lt;strong&gt;Declare the interface where it is consumed, not where it is implemented.&lt;/strong&gt;
This is not a style preference; it is the rule as written in
&lt;a href=&#34;https://go.dev/wiki/CodeReviewComments#interfaces&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Go Code Review Comments&lt;/a&gt;
: &lt;em&gt;&amp;ldquo;Go interfaces
generally belong in the package that uses values of the interface type, not the package that
implements them.&amp;rdquo;&lt;/em&gt; The domain service does not know its repository interface lives in &lt;code&gt;service.go&lt;/code&gt; -
it only knows it needs three methods:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;hotelHandler&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;hotelService&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#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:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;hotelService&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;interface&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;FindAllHotels&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;context&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Context&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;model&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;FindHotelsParams&lt;/span&gt;) (&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;model&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;HotelsPage&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;error&lt;/span&gt;)
&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;Every interface in the project follows the same shape:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;hotelService&lt;/code&gt; in the handler package - one method, because the hotel handler lists hotels.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;hotelRepository&lt;/code&gt; in the service package - three methods: &lt;code&gt;GetHotelsByName&lt;/code&gt;, &lt;code&gt;PutHotels&lt;/code&gt;,
&lt;code&gt;DeleteAllHotels&lt;/code&gt;, which are the only ones the service calls.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;storage&lt;/code&gt; in the server package - &lt;code&gt;Connect()&lt;/code&gt; and &lt;code&gt;Close()&lt;/code&gt;, nothing more, even though the
PostgreSQL type behind it does a great deal more.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;reservationWorker&lt;/code&gt; in the server package - &lt;code&gt;Start(ctx)&lt;/code&gt;, so the composition root can be tested
without a running background loop.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Two details are worth flagging for anyone copying the pattern.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; constructors here return &lt;em&gt;unexported&lt;/em&gt; types (&lt;code&gt;func New(...) *hotelHandler&lt;/code&gt;). That is
idiomatic inside a module - the caller can hold the value and call its exported methods - but it
makes the package impossible to fake from outside, and some linters object to it. The safety net
in this project is &lt;code&gt;internal/&lt;/code&gt;: nobody outside the module can depend on these packages anyway.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The second detail is the cost of many small interfaces: each one needs a mock. That is why
&lt;code&gt;internal/&amp;lt;domain&amp;gt;/service/mock&lt;/code&gt; exists at all, and why &lt;code&gt;.mockery.yml&lt;/code&gt; is curated rather than
generated wholesale (&lt;code&gt;all: false&lt;/code&gt;, then an explicit list). Where an interface has a single trivial
method and only one consumer needs it, a hand-written stub in the test file is cheaper than a
generated mock - generate mocks for the repository and service seams, hand-write everything else.&lt;/p&gt;
&lt;h3 id=&#34;24-where-the-business-logic-actually-lives&#34;&gt;2.4 Where the business logic actually lives&lt;/h3&gt;
&lt;p&gt;Honesty first: &lt;code&gt;hotelService.FindAllHotels&lt;/code&gt; is a pass-through.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#75715e&#34;&gt;// FindAllHotels finds hotels by their name using the hotel repository.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;s&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;hotelService&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;FindAllHotels&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;context&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Context&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;model&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;FindHotelsParams&lt;/span&gt;) (&lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;model&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;HotelsPage&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;error&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;s&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;hotelRepository&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;GetHotelsByName&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;)
&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;Three wrappers around a repository look like ceremony, and if the service layer never did anything
else, calling it that would be fair. It exists as a &lt;strong&gt;seam&lt;/strong&gt;: it gives a future invariant somewhere
to live (&amp;ldquo;never return deactivated hotels&amp;rdquo;), it keeps handlers from depending on repositories
directly, and it is where the request&amp;rsquo;s &lt;code&gt;context&lt;/code&gt; gets a chance to be observed. The point is not
that every layer must be thick - it is that each layer must have a reason to change independently
of its neighbours.&lt;/p&gt;
&lt;p&gt;The reservation domain is where that bet pays off, because it has rules the database cannot express
in a constraint:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;for&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;range&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;maxReferenceAttempts&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Reference&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;generateReference&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;existing&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;s&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;reservationRepository&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;GetReservationByReference&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Reference&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;existing&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;continue&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#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:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;s&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;reservationRepository&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;PutReservation&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;)
&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;A booking is made for a &lt;em&gt;party size&lt;/em&gt;, not a specific room, so the service asks the repository to
allocate one room - or a combination of rooms - that holds the party for the entire date range,
preferring the smallest sufficient room, then falling back to sorting candidates by capacity and
distributing guests across them. The allocation rule itself is out of scope here; what matters for
this post is that it is a rule, not a query - and that it has somewhere to live.&lt;/p&gt;
&lt;h3 id=&#34;25-cross-domain-work-gets-its-own-domain&#34;&gt;2.5 Cross-domain work gets its own domain&lt;/h3&gt;
&lt;p&gt;Seeding and resetting test data touches hotels, rooms and reservations. Rather than letting
&lt;code&gt;internal/hotel&lt;/code&gt; know about &lt;code&gt;internal/room&lt;/code&gt;, the cross-cutting concern becomes a domain of its own
under &lt;code&gt;internal/app/testing&lt;/code&gt;, composing the three services behind one wider interface - and
importing nothing from the other domains except their models:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;testingService&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;hotelService&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;roomService&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;reservationService&lt;/span&gt;
&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;code&gt;SeedTestData&lt;/code&gt; resets everything, loads room types, generates hotels, rotates room types across the
generated rooms, and inserts. &lt;code&gt;ResetTestData&lt;/code&gt; removes reservations, then rooms, then hotels - in
that order, because the foreign keys demand it.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; &lt;code&gt;PUT /v1/testing&lt;/code&gt; is a demo affordance and nothing else. In a real deployment it must
be gated - a config flag that skips route registration, or a build tag that omits the package
entirely. Shipping it silently is the kind of mistake that only shows up in a postmortem.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;26-what-the-request-path-actually-does&#34;&gt;2.6 What the request path actually does&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#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;GET /v1/hotels?name_pattern=mercury&amp;amp;page_size=5
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        |
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;        V
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;gin engine --&amp;gt; hotelHandler.GetAll
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 |  c.ShouldBindQuery(struct{ NamePattern; NextPage; PageSize })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 |  pagination.NormalizeSize(size)   pagination.Decode(token)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 V
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              hotelService.FindAllHotels(ctx, model.FindHotelsParams)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 V
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              hotelRepository.GetHotelsByName
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 |  ctx, cancel := context.WithTimeout(ctx, 5s)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 |  QueryContext(selectHotelsQuery, pattern, afterID, pageSize+1)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 V
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;              PostgreSQL --&amp;gt; rows --&amp;gt; scan --&amp;gt; trim the extra row --&amp;gt; encode next_page
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Four decisions hide in that path.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Validation belongs to the handler.&lt;/strong&gt; Each handler package has a &lt;code&gt;validate.go&lt;/code&gt;, so binding and
rejection are one function away from the route that uses them:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;validateGetAllRequest&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;gin&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Context&lt;/span&gt;) (&lt;span style=&#34;color:#a6e22e&#34;&gt;model&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;FindHotelsParams&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;error&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;var&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;query&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;NamePattern&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`form:&amp;#34;name_pattern&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:#a6e22e&#34;&gt;NextPage&lt;/span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`form:&amp;#34;next_page&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:#a6e22e&#34;&gt;PageSize&lt;/span&gt;    &lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;`form:&amp;#34;page_size&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:#f92672&#34;&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;result&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;model&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;FindHotelsParams&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;NamePattern&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;query&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NamePattern&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;PageSize&lt;/span&gt;:    &lt;span style=&#34;color:#a6e22e&#34;&gt;pagination&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NormalizeSize&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;query&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;PageSize&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	}
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#f92672&#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 handler returns &lt;code&gt;400&lt;/code&gt; before the service is ever called - a fact the tests assert by &lt;em&gt;not&lt;/em&gt;
setting up any mock expectation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Pagination is keyset, not &lt;code&gt;OFFSET&lt;/code&gt;.&lt;/strong&gt; The query asks for one row more than the page size:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;SELECT&lt;/span&gt; id, name, address, phone
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;FROM&lt;/span&gt; hotels
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;WHERE&lt;/span&gt; name &lt;span style=&#34;color:#66d9ef&#34;&gt;ILIKE&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;%&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;||&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;$&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;||&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;%&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:#66d9ef&#34;&gt;AND&lt;/span&gt; (&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;$&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;OR&lt;/span&gt; id &lt;span style=&#34;color:#f92672&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;$&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#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:#66d9ef&#34;&gt;ORDER&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;BY&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:#66d9ef&#34;&gt;LIMIT&lt;/span&gt; &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;$&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The extra row is the &amp;ldquo;is there a next page&amp;rdquo; signal; the last kept row&amp;rsquo;s id becomes an opaque token
(&lt;code&gt;base64.RawURLEncoding&lt;/code&gt; of &lt;code&gt;{&amp;quot;after_id&amp;quot;:&amp;quot;...&amp;quot;}&lt;/code&gt;), which is why the repository can paginate without
counting anything. And the argument is &lt;code&gt;pageSize+1&lt;/code&gt; - a behavioural contract, not an implementation
detail: a regression that fetches exactly &lt;code&gt;pageSize&lt;/code&gt; would silently make &lt;code&gt;next_page&lt;/code&gt; disappear from
every response.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; that token is &lt;em&gt;opaque, not signed&lt;/em&gt;. A client can decode it and forge an &lt;code&gt;after_id&lt;/code&gt;.
For public data ordered by a key this is fine; as an authorization cursor it is not, because
&amp;ldquo;continue after this id&amp;rdquo; becomes an invitation to guess ids. Opaque means &amp;ldquo;do not parse this&amp;rdquo;,
not &amp;ldquo;trust me&amp;rdquo;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;One round trip instead of N.&lt;/strong&gt; Bulk inserts use a single statement with array parameters:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-sql&#34; data-lang=&#34;sql&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;INSERT&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;INTO&lt;/span&gt; hotels (id, name, address, phone)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;SELECT&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;FROM&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;unnest&lt;/span&gt;(&lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;$&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;::text[], &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;$&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;2&lt;/span&gt;::text[], &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;$&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;3&lt;/span&gt;::text[], &lt;span style=&#34;color:#960050;background-color:#1e0010&#34;&gt;$&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#34;&gt;4&lt;/span&gt;::text[])
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The repository builds the arrays and passes them as &lt;code&gt;pq.Array(...)&lt;/code&gt;. Three hotels or three hundred,
it is still one statement.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The deadline is owned by the layer that knows it.&lt;/strong&gt; The repository applies &lt;code&gt;DB_QUERY_TIMEOUT&lt;/code&gt;
per call, and the handler never thinks about time at all:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;cancel&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;context&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;WithTimeout&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;queryTimeout&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;defer&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;cancel&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;27-errors-wrapping-and-one-honest-gap&#34;&gt;2.7 Errors: wrapping, and one honest gap&lt;/h3&gt;
&lt;p&gt;Every layer wraps what it returns, with the operation as context:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;rows&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;r&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;db&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;QueryContext&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;selectHotelsQuery&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NamePattern&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;AfterID&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;p&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;PageSize&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt;&lt;span style=&#34;color:#ae81ff&#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:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;fmt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Errorf&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;find hotels by name: %w&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)
&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;code&gt;%w&lt;/code&gt; keeps the chain walkable, so callers can test with &lt;code&gt;errors.Is&lt;/code&gt;; the tests in this example
assert on those prefixes, which is what makes them behaviour tests rather than line coverage.&lt;/p&gt;
&lt;p&gt;The gap, stated plainly: the handlers currently map &lt;em&gt;any&lt;/em&gt; service error to &lt;code&gt;500&lt;/code&gt; and echo
&lt;code&gt;err.Error()&lt;/code&gt; into the response body, which can leak internals to a client. The shape to aim for is
a small vocabulary of sentinel errors in the service and one mapping function in the handler:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;statusFor&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;error&lt;/span&gt;) &lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;switch&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;errors&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Is&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;model&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ErrHotelNotFound&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;StatusNotFound&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;errors&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Is&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;model&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ErrInsufficientCapacity&lt;/span&gt;):
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;StatusConflict&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;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:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;StatusInternalServerError&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#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;Fixing that is a change of its own and out of scope here; it is named because a known boundary is
more useful to a reader than an example presented as finished.&lt;/p&gt;
&lt;h3 id=&#34;28-when-not-to-layer&#34;&gt;2.8 When &lt;em&gt;not&lt;/em&gt; to layer&lt;/h3&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; three files and two interfaces around a &lt;code&gt;SELECT&lt;/code&gt; are a cost. If a service has one
table, no invariants beyond the schema, and no second consumer, one package is the better design.
Layers buy replaceability; if nothing will ever be replaced, buy nothing.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h2 id=&#34;3-local-setup-docker-environment-tasks&#34;&gt;3. Local Setup: Docker, Environment, Tasks&lt;/h2&gt;
&lt;h3 id=&#34;31-requirements&#34;&gt;3.1 Requirements&lt;/h3&gt;
&lt;p&gt;Go 1.27+, Docker, and &lt;code&gt;jq&lt;/code&gt; if one wants pretty-printed responses. Nothing else - the task runner and
the mock generator are module-pinned.&lt;/p&gt;
&lt;h3 id=&#34;32-one-command-for-the-whole-stack&#34;&gt;3.2 One command for the whole stack&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#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;$ docker compose up --build
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;PostgreSQL starts on &lt;code&gt;5432&lt;/code&gt; and the API on &lt;code&gt;8080&lt;/code&gt;. Three details in &lt;code&gt;docker-compose.yaml&lt;/code&gt; make that
reliable rather than lucky: the &lt;code&gt;storage&lt;/code&gt; service declares a healthcheck (&lt;code&gt;pg_isready -U ... -d ...&lt;/code&gt;),
the &lt;code&gt;server&lt;/code&gt; service waits for it with &lt;code&gt;depends_on: { storage: { condition: service_healthy } }&lt;/code&gt;,
and the schema is mounted into &lt;code&gt;/docker-entrypoint-initdb.d/&lt;/code&gt;, so a fresh database is initialized
with &lt;code&gt;specs/postgres.sql&lt;/code&gt; on first start.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; the entrypoint script runs only against an &lt;em&gt;empty&lt;/em&gt; data volume. After editing
&lt;code&gt;specs/postgres.sql&lt;/code&gt;, &lt;code&gt;docker compose up&lt;/code&gt; will happily reuse the old schema - run
&lt;code&gt;docker compose down -v&lt;/code&gt; first. This is the most common local-setup failure in the project&amp;rsquo;s
README, and it is worth a line there for exactly that reason.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id=&#34;33-the-hybrid-loop&#34;&gt;3.3 The hybrid loop&lt;/h3&gt;
&lt;p&gt;For day-to-day work, the container is only needed for the database:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#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;$ docker compose up -d storage
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ go run ./cmd/booking-api
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This keeps the edit-run cycle fast while still testing against real PostgreSQL, and it proves
configuration is fully environment-driven - the same binary runs in both setups.&lt;/p&gt;
&lt;h3 id=&#34;34-configuration-with-working-defaults&#34;&gt;3.4 Configuration with working defaults&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;config&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;ListenAddress&lt;/span&gt;   &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;        &lt;span style=&#34;color:#e6db74&#34;&gt;`env:&amp;#34;LISTEN_ADDRESS,default=:8080&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:#a6e22e&#34;&gt;ShutdownTimeout&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Duration&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`env:&amp;#34;SHUTDOWN_TIMEOUT,default=10s&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:#a6e22e&#34;&gt;WorkerInterval&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Duration&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`env:&amp;#34;WORKER_INTERVAL,default=10s&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:#a6e22e&#34;&gt;PostgresUser&lt;/span&gt;     &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`env:&amp;#34;POSTGRES_USER,default=booking_api&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:#a6e22e&#34;&gt;PostgresPassword&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`env:&amp;#34;POSTGRES_PASSWORD,default=booking_api&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:#a6e22e&#34;&gt;PostgresDB&lt;/span&gt;       &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`env:&amp;#34;POSTGRES_DB,default=booking_api&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:#a6e22e&#34;&gt;PostgresHost&lt;/span&gt;     &lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt; &lt;span style=&#34;color:#e6db74&#34;&gt;`env:&amp;#34;POSTGRES_HOST,default=localhost&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:#a6e22e&#34;&gt;PostgresPort&lt;/span&gt;     &lt;span style=&#34;color:#66d9ef&#34;&gt;int&lt;/span&gt;    &lt;span style=&#34;color:#e6db74&#34;&gt;`env:&amp;#34;POSTGRES_PORT,default=5432&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:#f92672&#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 variable set falls into four groups:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Listening and lifecycle&lt;/strong&gt; - &lt;code&gt;LISTEN_ADDRESS&lt;/code&gt;, &lt;code&gt;SHUTDOWN_TIMEOUT&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Database connection&lt;/strong&gt; - &lt;code&gt;POSTGRES_*&lt;/code&gt;, plus &lt;code&gt;DB_CONNECT_MAX_RETRIES&lt;/code&gt; and &lt;code&gt;DB_CONNECT_RETRY_DELAY&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Connection pool&lt;/strong&gt; - &lt;code&gt;DB_MAX_IDLE_CONNS&lt;/code&gt;, &lt;code&gt;DB_MAX_OPEN_CONNS&lt;/code&gt;, &lt;code&gt;DB_CONN_MAX_IDLE_TIME&lt;/code&gt;,
&lt;code&gt;DB_CONN_MAX_LIFETIME&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Background work&lt;/strong&gt; - &lt;code&gt;WORKER_INTERVAL&lt;/code&gt;, and &lt;code&gt;DB_QUERY_TIMEOUT&lt;/code&gt; for per-query deadlines.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The useful property is that &lt;strong&gt;the defaults in code are the Compose configuration.&lt;/strong&gt; The container
overrides exactly one variable, &lt;code&gt;POSTGRES_HOST=storage&lt;/code&gt;, because that is the only value that
genuinely differs between a laptop and a container network. There is one place to reason about
configuration even though there are two environments.&lt;/p&gt;
&lt;h3 id=&#34;35-tasks-instead-of-a-wiki-page&#34;&gt;3.5 Tasks instead of a wiki page&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#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;$ go tool task          &lt;span style=&#34;color:#75715e&#34;&gt;# list the available tasks&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ go tool task tests    &lt;span style=&#34;color:#75715e&#34;&gt;# per-package coverage plus the overall total&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ go tool task mocks    &lt;span style=&#34;color:#75715e&#34;&gt;# delete the generated mocks, then regenerate them&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;tests&lt;/code&gt; task looks like this, and every character of it is a decision:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#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:#f92672&#34;&gt;tests&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;cmds&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#f92672&#34;&gt;defer&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;rm -f coverage.out&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#ae81ff&#34;&gt;go test -coverprofile=coverage.out $(go list ./... | grep -v /mock)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    - &lt;span style=&#34;color:#ae81ff&#34;&gt;go tool cover -func=coverage.out | tail -1&lt;/span&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;/mock&lt;/code&gt; packages are excluded, because generated code should not inflate - or deflate - the
coverage denominator.&lt;/li&gt;
&lt;li&gt;The overall total is printed last, so the one number a reader cares about is the last line on
screen.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;defer: rm -f coverage.out&lt;/code&gt; means the task leaves no artifacts behind; a clean tree after a test
run is a small thing that removes a whole category of accidental commits.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;mocks:clean&lt;/code&gt; runs &lt;code&gt;find internal -type d -name mock -prune -exec rm -rf {} +&lt;/code&gt; before regenerating,
so &lt;strong&gt;a stale mock cannot survive an interface change&lt;/strong&gt;. Without that, a renamed method leaves an old
mock that still compiles, and the next green test run is a lie.&lt;/p&gt;
&lt;p&gt;Readers who expect a &lt;code&gt;Makefile&lt;/code&gt;: &lt;code&gt;Taskfile.yaml&lt;/code&gt; is the Makefile here. The reason is only that
&lt;code&gt;task&lt;/code&gt; is pinned alongside the other tools instead of depending on whichever version happens to be
installed; &lt;code&gt;make test&lt;/code&gt; wrapping the same two &lt;code&gt;go&lt;/code&gt; commands is an equally good answer. What matters
is that there is exactly one documented entry point, and a new contributor never has to guess the
invocation.&lt;/p&gt;
&lt;h3 id=&#34;36-the-manual-walkthrough&#34;&gt;3.6 The manual walkthrough&lt;/h3&gt;
&lt;p&gt;The README doubles as a demo script, and the whole story fits in six commands:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#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;$ curl -s -X PUT &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;http://localhost:8080/v1/testing?max_hotels=1&amp;amp;max_rooms_per_hotel=6&amp;#39;&lt;/span&gt; | jq
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ curl -s &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;http://localhost:8080/v1/hotels?name_pattern=mercury&amp;#39;&lt;/span&gt; | jq
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ curl -s &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;http://localhost:8080/v1/rooms?guests_count=2&amp;amp;check_in_date=2026-10-01&amp;amp;check_out_date=2026-10-03&amp;#39;&lt;/span&gt; | jq
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ curl -s -X POST http://localhost:8080/v1/reservations -H &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;Content-Type: application/json&amp;#39;&lt;/span&gt; -d @booking.json | jq
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ curl -s http://localhost:8080/v1/reservations/CYYKSX24PR | jq
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;$ curl -i -X DELETE http://localhost:8080/v1/testing
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Seed, search, check availability, book, look the booking up, reset. &lt;code&gt;PUT /v1/testing&lt;/code&gt; replaces
whatever was there, so the sequence is repeatable.&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Status&lt;/th&gt;
					&lt;th&gt;Meaning&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;200&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;Success&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;204&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;Reset succeeded&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;400&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;Missing or invalid parameters, dates in the past, &lt;code&gt;check_out_date&lt;/code&gt; not after &lt;code&gt;check_in_date&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;404&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;Unknown hotel, unknown requested room, or unknown booking reference&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;409&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;The party cannot be accommodated, a requested room is no longer free, or a booking reference collision&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;500&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;Unexpected server error&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Two API-shape notes: list endpoints are paginated with &lt;code&gt;page_size&lt;/code&gt; (default 20, maximum 100) and an
opaque &lt;code&gt;next_page&lt;/code&gt; token, and &lt;code&gt;GET /health&lt;/code&gt; deliberately sits &lt;strong&gt;outside&lt;/strong&gt; the &lt;code&gt;/v1&lt;/code&gt; group - probes
are infrastructure, not API surface, and their path should not need versioning.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;4-bootstrapping--the-container-lifecycle&#34;&gt;4. Bootstrapping &amp;amp; the Container Lifecycle&lt;/h2&gt;
&lt;h3 id=&#34;41-main-is-nearly-empty&#34;&gt;4.1 &lt;code&gt;main&lt;/code&gt; is nearly empty&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;main&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;server&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;New&lt;/span&gt;().&lt;span style=&#34;color:#a6e22e&#34;&gt;Run&lt;/span&gt;(); &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;fmt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Fprintln&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;os&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Stderr&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;os&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Exit&lt;/span&gt;(&lt;span style=&#34;color:#ae81ff&#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;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;No package-level globals, no &lt;code&gt;init()&lt;/code&gt;, no &lt;code&gt;log.Fatal&lt;/code&gt; buried in a library package, and &lt;code&gt;os.Exit&lt;/code&gt;
appears exactly once - in &lt;code&gt;main&lt;/code&gt;, where it belongs. The entry point is untestable by design, and
that is fine, because everything it delegates to is not.&lt;/p&gt;
&lt;h3 id=&#34;42-run-as-an-explicit-lifecycle&#34;&gt;4.2 &lt;code&gt;Run()&lt;/code&gt; as an explicit lifecycle&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;apiServer&lt;/code&gt; composes its collaborators as embedded fields - the storage engine, the workers, the
router, the configuration and the logger - and &lt;code&gt;Run()&lt;/code&gt; executes the lifecycle in a fixed order:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Process the configuration.&lt;/strong&gt; Invalid values fail &lt;em&gt;before&lt;/em&gt; any resource is opened.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Initialize.&lt;/strong&gt; Build the logger, connect to storage (with retries), then wire repositories into
services into handlers into routes.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Start the workers&lt;/strong&gt; in their own goroutine, with their own cancelable context.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build the HTTP server&lt;/strong&gt; - &lt;code&gt;Addr&lt;/code&gt;, &lt;code&gt;Handler&lt;/code&gt;, and a &lt;code&gt;BaseContext&lt;/code&gt; returning the &lt;code&gt;Run&lt;/code&gt;-scoped
context.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Serve&lt;/strong&gt; in a goroutine; a failure that is not &lt;code&gt;http.ErrServerClosed&lt;/code&gt; lands on a buffered
&lt;code&gt;failure&lt;/code&gt; channel of size one.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wait&lt;/strong&gt; on &lt;code&gt;select&lt;/code&gt; for either that failure or a signal.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drain&lt;/strong&gt; with &lt;code&gt;Shutdown&lt;/code&gt; bounded by &lt;code&gt;SHUTDOWN_TIMEOUT&lt;/code&gt;, falling back to &lt;code&gt;Close()&lt;/code&gt; if the drain
times out.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stop the workers&lt;/strong&gt;, wait for them to return, then close storage.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Return&lt;/strong&gt; &lt;code&gt;errors.Join(runErr, closeErr)&lt;/code&gt;, so a shutdown error is not lost when a run error
already exists.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The wiring step is the one place where the whole graph is visible - and the only place that needs
to know about every layer:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;reservationService&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;serviceReservation&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#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:#a6e22e&#34;&gt;repositoryReservation&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;New&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;db&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;s&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;config&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;DatabaseQueryTimeout&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;s&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;handlerMux&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;s&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;registerRoutes&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;gin&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Default&lt;/span&gt;(), &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;handlers&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;hotelHandler&lt;/span&gt;:       &lt;span style=&#34;color:#a6e22e&#34;&gt;handlerHotel&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;New&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;hotelService&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;roomHandler&lt;/span&gt;:        &lt;span style=&#34;color:#a6e22e&#34;&gt;handlerRoom&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;New&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;roomService&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;reservationHandler&lt;/span&gt;: &lt;span style=&#34;color:#a6e22e&#34;&gt;handlerReservation&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;New&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;reservationService&lt;/span&gt;),
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#f92672&#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;strong&gt;Shutdown runs in the reverse order of startup, and the database is closed last&lt;/strong&gt; - because both
in-flight requests and the background worker are still using it.&lt;/p&gt;
&lt;h3 id=&#34;43-why-the-workers-get-their-own-context&#34;&gt;4.3 Why the workers get their own context&lt;/h3&gt;
&lt;p&gt;The HTTP server already has a drain mechanism, so the worker cannot share the server&amp;rsquo;s lifetime: it
needs a signal that fires &lt;em&gt;after&lt;/em&gt; the drain finishes but &lt;em&gt;before&lt;/em&gt; storage closes.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#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;SIGTERM --&amp;gt; httpServer.Shutdown(timeout)   stop accepting, drain in-flight
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 | (up to SHUTDOWN_TIMEOUT)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 V
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          cancel workersCtx --&amp;gt; worker returns from its select loop
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;                 V
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;          &amp;lt;-workersDone --&amp;gt; storage.Close() --&amp;gt; Run() returns
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The temptation is to reuse the root context for everything - and that is exactly the mistake
&lt;code&gt;BaseContext&lt;/code&gt; makes subtle. The context handed to &lt;code&gt;BaseContext&lt;/code&gt; becomes the parent of every
request&amp;rsquo;s context, so cancelling the root would abort in-flight requests instead of letting them
finish: the precise opposite of a graceful shutdown. Two contexts, two jobs.&lt;/p&gt;
&lt;h3 id=&#34;44-the-worker&#34;&gt;4.4 The worker&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; (&lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;reservationWorker&lt;/span&gt;) &lt;span style=&#34;color:#a6e22e&#34;&gt;Start&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;context&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;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:#a6e22e&#34;&gt;ticker&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NewTicker&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;interval&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;defer&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;ticker&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Stop&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;log&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Info&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;reservation worker started&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:#66d9ef&#34;&gt;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:#66d9ef&#34;&gt;select&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;-&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#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 style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;log&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Info&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;reservation worker stopped&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:#66d9ef&#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 style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;-&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;ticker&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;C&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#a6e22e&#34;&gt;processed&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;processPendingReservations&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#a6e22e&#34;&gt;w&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;log&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Info&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;processed pending reservations&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;count&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;processed&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#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;Pending bookings are confirmed on a ticker, and the simulated confirmation email lives inside a
&lt;code&gt;select&lt;/code&gt; that also watches the context - so the two-second wait is interruptible rather than an
unconditional &lt;code&gt;time.Sleep&lt;/code&gt;. The batch loop checks &lt;code&gt;ctx.Err()&lt;/code&gt; between items for the same reason: a
worker that only checks on tick boundaries will be &lt;code&gt;SIGKILL&lt;/code&gt;ed in the middle of a batch.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;recover()&lt;/code&gt; around the batch is a pragmatic guard so one bad reservation cannot take the process
down - not a substitute for fixing whatever panics, and it is written that way on purpose.&lt;/p&gt;
&lt;h3 id=&#34;45-why-signals-matter-in-containers&#34;&gt;4.5 Why signals matter in containers&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;PID 1 semantics.&lt;/strong&gt; The image is &lt;code&gt;gcr.io/distroless/static-debian12:nonroot&lt;/code&gt; with an exec-form
entrypoint (&lt;code&gt;ENTRYPOINT [&amp;quot;./booking-api&amp;quot;]&lt;/code&gt;), so the binary &lt;em&gt;is&lt;/em&gt; PID 1 and receives &lt;code&gt;SIGTERM&lt;/code&gt;
directly. A shell-form &lt;code&gt;CMD ./booking-api&lt;/code&gt; wraps it in &lt;code&gt;/bin/sh&lt;/code&gt;, which does not forward signals -
the process would be killed after the grace period with no cleanup at all.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The default action of &lt;code&gt;SIGTERM&lt;/code&gt; terminates the process immediately.&lt;/strong&gt; Go only turns it into a
channel delivery if the program subscribes with &lt;code&gt;signal.Notify&lt;/code&gt;. Without that subscription there
is no drain, no &lt;code&gt;Close()&lt;/code&gt;, no worker shutdown - which is the failure from the opening paragraph.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Teardown in Kubernetes is a race.&lt;/strong&gt; On pod deletion the endpoint is removed from the
EndpointSlice and &lt;code&gt;SIGTERM&lt;/code&gt; is delivered roughly in parallel, so requests can still arrive after
the signal. Graceful shutdown narrows the window; a &lt;code&gt;preStop&lt;/code&gt; hook and a grace period longer than
the drain close it. The user-visible symptom of getting this wrong is &lt;code&gt;502 Bad Gateway&lt;/code&gt; on a
handful of requests during &lt;em&gt;every&lt;/em&gt; rollout.&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Knob&lt;/th&gt;
					&lt;th&gt;Value here&lt;/th&gt;
					&lt;th&gt;Why it must be set this way&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;SHUTDOWN_TIMEOUT&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;10s&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;Upper bound on the drain&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;terminationGracePeriodSeconds&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;must exceed the above&lt;/td&gt;
					&lt;td&gt;Otherwise the kubelet &lt;code&gt;SIGKILL&lt;/code&gt;s mid-drain&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;docker stop&lt;/code&gt; (Compose &lt;code&gt;down&lt;/code&gt;)&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;10s&lt;/code&gt; default, then &lt;code&gt;SIGKILL&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;Same rule; &lt;code&gt;SIGTERM&lt;/code&gt; first is why the handler must exist&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;preStop&lt;/code&gt; hook&lt;/td&gt;
					&lt;td&gt;optional short sleep&lt;/td&gt;
					&lt;td&gt;Covers endpoint-propagation lag&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;readiness probe&lt;/td&gt;
					&lt;td&gt;withdrawn before shutdown&lt;/td&gt;
					&lt;td&gt;Prevents new traffic during the drain&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A manifest fragment would be out of place in a post about Go: the mechanics above are the subject,
and the YAML is a one-line consequence of them.&lt;/p&gt;
&lt;p&gt;Two more mechanisms are worth naming:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Both &lt;code&gt;SIGINT&lt;/code&gt; and &lt;code&gt;SIGTERM&lt;/code&gt; are registered&lt;/strong&gt;, because &lt;code&gt;docker compose down&lt;/code&gt; and Kubernetes send
&lt;code&gt;SIGTERM&lt;/code&gt;, while Ctrl-C in a terminal sends &lt;code&gt;SIGINT&lt;/code&gt;. Handling one leaves a gap in exactly one of
the two environments.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Errors wrapped with &lt;code&gt;%w&lt;/code&gt; all the way up and printed once in &lt;code&gt;main&lt;/code&gt;&lt;/strong&gt; mean &lt;code&gt;docker logs&lt;/code&gt; and
&lt;code&gt;kubectl logs&lt;/code&gt; show a single readable line before exit code 1 - which is what turns a
&lt;code&gt;CrashLoopBackOff&lt;/code&gt; into a diagnosis instead of a mystery.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This example registers only &lt;code&gt;/health&lt;/code&gt;. A production shape splits liveness (no dependencies, always
fine) from readiness (checks the database), so a broken database removes the pod from service
instead of restarting it in a loop.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Gotcha:&lt;/strong&gt; an &lt;code&gt;http.Server&lt;/code&gt; with no &lt;code&gt;ReadHeaderTimeout&lt;/code&gt; is trivially exposed to slow-loris
attacks; a reverse proxy mitigates but does not fix it. Also absent here: &lt;code&gt;ReadTimeout&lt;/code&gt;,
&lt;code&gt;WriteTimeout&lt;/code&gt;, &lt;code&gt;MaxHeaderBytes&lt;/code&gt; and per-route timeouts. They are left out so the bootstrap stays
readable - in a real service the struct looks like this:&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;httpServer&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;amp;&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Server&lt;/span&gt;{
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;Addr&lt;/span&gt;:              &lt;span style=&#34;color:#a6e22e&#34;&gt;s&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;config&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ListenAddress&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;Handler&lt;/span&gt;:           &lt;span style=&#34;color:#a6e22e&#34;&gt;s&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;handlerMux&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;ReadHeaderTimeout&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;5&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Second&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;ReadTimeout&lt;/span&gt;:       &lt;span style=&#34;color:#ae81ff&#34;&gt;30&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Second&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;WriteTimeout&lt;/span&gt;:      &lt;span style=&#34;color:#ae81ff&#34;&gt;30&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Second&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;IdleTimeout&lt;/span&gt;:       &lt;span style=&#34;color:#ae81ff&#34;&gt;60&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;time&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Second&lt;/span&gt;,
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;MaxHeaderBytes&lt;/span&gt;:    &lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;,
&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;hr&gt;
&lt;h2 id=&#34;5-testing-one-layer-one-kind-of-test&#34;&gt;5. Testing: One Layer, One Kind of Test&lt;/h2&gt;
&lt;h3 id=&#34;51-the-map&#34;&gt;5.1 The map&lt;/h3&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Layer&lt;/th&gt;
					&lt;th&gt;Tool&lt;/th&gt;
					&lt;th&gt;What is faked&lt;/th&gt;
					&lt;th&gt;What the test proves&lt;/th&gt;
					&lt;th&gt;What it cannot catch&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;internal/app/server&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;mocks + &lt;code&gt;httptest&lt;/code&gt; + &lt;code&gt;testing/synctest&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;handlers, storage, worker&lt;/td&gt;
					&lt;td&gt;the route table, fail-fast config, worker hand-off&lt;/td&gt;
					&lt;td&gt;handler internals, SQL&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;&amp;lt;domain&amp;gt;/handler&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;mock of the service + &lt;code&gt;httptest.Recorder&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;the service&lt;/td&gt;
					&lt;td&gt;binding, validation, status codes, response JSON&lt;/td&gt;
					&lt;td&gt;route registration&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;&amp;lt;domain&amp;gt;/service&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;mock of the repository&lt;/td&gt;
					&lt;td&gt;the repository&lt;/td&gt;
					&lt;td&gt;orchestration, error propagation&lt;/td&gt;
					&lt;td&gt;SQL, HTTP&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;&amp;lt;domain&amp;gt;/repository&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;go-sqlmock&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;the database&lt;/td&gt;
					&lt;td&gt;the SQL text, the arguments, error wrapping&lt;/td&gt;
					&lt;td&gt;whether PostgreSQL &lt;em&gt;accepts&lt;/em&gt; the SQL&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;internal/helper/*&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;plain table tests&lt;/td&gt;
					&lt;td&gt;nothing&lt;/td&gt;
					&lt;td&gt;pure logic: cursors, clamping, dates&lt;/td&gt;
					&lt;td&gt;nothing - and that is the point&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;The layer you fake tells you which layer you are testing.&lt;/strong&gt; That is what the interfaces in section 2.3
were for.&lt;/p&gt;
&lt;h3 id=&#34;52-server-tests&#34;&gt;5.2 Server tests&lt;/h3&gt;
&lt;p&gt;Three kinds of assertion live at this level. First, that configuration failures fail &lt;em&gt;fast&lt;/em&gt; - an
invalid duration or port must be rejected before anything is opened:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#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:#a6e22e&#34;&gt;name&lt;/span&gt;:        &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;fails fast on an invalid shutdown timeout&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:#a6e22e&#34;&gt;env&lt;/span&gt;:         &lt;span style=&#34;color:#66d9ef&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;]&lt;span style=&#34;color:#66d9ef&#34;&gt;string&lt;/span&gt;{&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;SHUTDOWN_TIMEOUT&amp;#34;&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;soon&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:#a6e22e&#34;&gt;wantErrText&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;failed to process configuration:&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;Second, that the route table says what it claims to say. &lt;code&gt;TestRegisterRoutes&lt;/code&gt; builds the engine with
mocked handlers and drives it through &lt;code&gt;httptest&lt;/code&gt;, asserting that &lt;code&gt;/health&lt;/code&gt; answers outside the
version prefix, that &lt;code&gt;GET /v1/reservations/REF-1&lt;/code&gt; reaches &lt;code&gt;reservationHandler.Get&lt;/code&gt; with
&lt;code&gt;c.Param(&amp;quot;booking_ref&amp;quot;)&lt;/code&gt; set, that an unknown path &lt;strong&gt;and&lt;/strong&gt; a known path with an unregistered method
both return &lt;code&gt;404&lt;/code&gt;, and that &lt;code&gt;/v1/hotels/&lt;/code&gt; redirects with &lt;code&gt;301&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Third, and most interesting, that the worker hand-off is ordered. That test uses
&lt;code&gt;testing/synctest&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;synctest&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Test&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;testing&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;T&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;release&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; make(&lt;span style=&#34;color:#66d9ef&#34;&gt;chan&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt;{})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#a6e22e&#34;&gt;done&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; make(&lt;span style=&#34;color:#66d9ef&#34;&gt;chan&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;struct&lt;/span&gt;{})
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#f92672&#34;&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;go&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;() {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#66d9ef&#34;&gt;defer&lt;/span&gt; close(&lt;span style=&#34;color:#a6e22e&#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 style=&#34;color:#a6e22e&#34;&gt;server&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;startWorkers&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#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:#a6e22e&#34;&gt;synctest&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Wait&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;select&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;		&lt;span style=&#34;color:#66d9ef&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;&amp;lt;-&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#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 style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Fatal&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;startWorkers returned before the worker did&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:#66d9ef&#34;&gt;default&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#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;code&gt;testing/synctest&lt;/code&gt; gives a deterministic scheduler and virtual time: &lt;code&gt;synctest.Wait()&lt;/code&gt; guarantees
the goroutine has parked, so &amp;ldquo;has not returned yet&amp;rdquo; is a real assertion rather than a race that
passes on a fast machine. It is made for exactly this class - tickers, background goroutines and
shutdown ordering - and it removes the &lt;code&gt;time.Sleep(100 * time.Millisecond)&lt;/code&gt; that would otherwise be
load-bearing. Note also &lt;code&gt;t.Context()&lt;/code&gt;, which cancels when the test ends and removes a whole family
of leaked-goroutine bugs.&lt;/p&gt;
&lt;h3 id=&#34;53-handler-tests&#34;&gt;5.3 Handler tests&lt;/h3&gt;
&lt;p&gt;The handler is called directly, with no router:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#a6e22e&#34;&gt;recorder&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;httptest&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NewRecorder&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;_&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;gin&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;CreateTestContext&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;recorder&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Request&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;httptest&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NewRequest&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;http&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;MethodGet&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;/v1/hotels?&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;+&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;tt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;query&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;).
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;				&lt;span style=&#34;color:#a6e22e&#34;&gt;WithContext&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;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:#a6e22e&#34;&gt;New&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;service&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;GetAll&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;c&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#a6e22e&#34;&gt;assert&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Equal&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;tt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;wantStatus&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;recorder&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Code&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This isolates binding, validation and status mapping - and the cost is that a route typo is
invisible here. That is precisely why &lt;code&gt;TestRegisterRoutes&lt;/code&gt; exists: one test owns routing, the other
owns the handler body. Naming that division of labour is more useful than pretending either test is
complete.&lt;/p&gt;
&lt;p&gt;Two cases in the table are worth pointing at, because they encode intent:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The validation cases (&lt;code&gt;page_size=abc&lt;/code&gt;, &lt;code&gt;next_page=not_a_token!!&lt;/code&gt;) have &lt;strong&gt;no&lt;/strong&gt; mock expectations at
all. The absence &lt;em&gt;is&lt;/em&gt; the assertion: the service was never called.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;null&lt;/code&gt; is an expected body when the service returns a nil page - asserting it verbatim makes any
accidental nil-handling change visible instead of quietly acceptable.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;54-service-tests&#34;&gt;5.4 Service tests&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#a6e22e&#34;&gt;expectations&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;repo&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;mock&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;MockHotelRepository&lt;/span&gt;) {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;				&lt;span style=&#34;color:#a6e22e&#34;&gt;repo&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;EXPECT&lt;/span&gt;().
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;					&lt;span style=&#34;color:#a6e22e&#34;&gt;GetHotelsByName&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;model&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;FindHotelsParams&lt;/span&gt;{&lt;span style=&#34;color:#a6e22e&#34;&gt;NamePattern&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;grand&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;PageSize&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;20&lt;/span&gt;}).
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;					&lt;span style=&#34;color:#a6e22e&#34;&gt;Return&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;page&lt;/span&gt;, &lt;span style=&#34;color:#66d9ef&#34;&gt;nil&lt;/span&gt;).
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;					&lt;span style=&#34;color:#a6e22e&#34;&gt;Once&lt;/span&gt;()
&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;Two assertions carry more meaning than the rest. &lt;code&gt;assert.Same&lt;/code&gt; proves the service returns &lt;em&gt;the same&lt;/em&gt;
page object, catching an accidental copy or rebuild that &lt;code&gt;assert.Equal&lt;/code&gt; would accept. And
&lt;code&gt;require.ErrorIs&lt;/code&gt; proves the repository error survived the layer intact - which is only true if the
wrapping used &lt;code&gt;%w&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#a6e22e&#34;&gt;got&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;newTestService&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;tt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;expectations&lt;/span&gt;).&lt;span style=&#34;color:#a6e22e&#34;&gt;FindAllHotels&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;ctx&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;tt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;params&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#a6e22e&#34;&gt;require&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NoError&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;			&lt;span style=&#34;color:#a6e22e&#34;&gt;assert&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Same&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;tt&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;want&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;got&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;The edge cases make the suite credible: empty input passed through unchanged, an empty page returned
as it is, and errors surfaced without embellishment.&lt;/p&gt;
&lt;h3 id=&#34;55-repository-tests-and-the-sql-contract&#34;&gt;5.5 Repository tests, and the SQL contract&lt;/h3&gt;
&lt;p&gt;The repository is the layer where &amp;ldquo;did we write the query we think we wrote&amp;rdquo; actually gets tested:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;newTestRepository&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;testing&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;T&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;expectations&lt;/span&gt; &lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;mock&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;sqlmock&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Sqlmock&lt;/span&gt;)) &lt;span style=&#34;color:#f92672&#34;&gt;*&lt;/span&gt;&lt;span style=&#34;color:#a6e22e&#34;&gt;hotelRepository&lt;/span&gt; {
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Helper&lt;/span&gt;()
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;db&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;mock&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt; &lt;span style=&#34;color:#f92672&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;sqlmock&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#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:#a6e22e&#34;&gt;require&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NoError&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;err&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Cleanup&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;() { &lt;span style=&#34;color:#a6e22e&#34;&gt;_&lt;/span&gt; = &lt;span style=&#34;color:#a6e22e&#34;&gt;db&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Close&lt;/span&gt;() })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;Cleanup&lt;/span&gt;(&lt;span style=&#34;color:#66d9ef&#34;&gt;func&lt;/span&gt;() { &lt;span style=&#34;color:#a6e22e&#34;&gt;assert&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;NoError&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;t&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;mock&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ExpectationsWereMet&lt;/span&gt;()) })
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#a6e22e&#34;&gt;expectations&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;mock&lt;/span&gt;)
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;	&lt;span style=&#34;color:#66d9ef&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#a6e22e&#34;&gt;New&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;db&lt;/span&gt;, &lt;span style=&#34;color:#a6e22e&#34;&gt;testQueryTimeout&lt;/span&gt;)
&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 second &lt;code&gt;t.Cleanup&lt;/code&gt; is the most valuable line in the file: &lt;strong&gt;an unfulfilled expectation fails
the test&lt;/strong&gt;, so a query that is never executed - or executed with different arguments - cannot slip
through on the strength of the other assertions.&lt;/p&gt;
&lt;p&gt;Queries are pinned by reference to the production constants, never by copying the SQL into the test:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;				&lt;span style=&#34;color:#a6e22e&#34;&gt;mock&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;ExpectQuery&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;regexp&lt;/span&gt;.&lt;span style=&#34;color:#a6e22e&#34;&gt;QuoteMeta&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;selectHotelsQuery&lt;/span&gt;)).
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;					&lt;span style=&#34;color:#a6e22e&#34;&gt;WithArgs&lt;/span&gt;(&lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;grand&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#ae81ff&#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:#a6e22e&#34;&gt;WillReturnRows&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;hotelRows&lt;/span&gt;(&lt;span style=&#34;color:#a6e22e&#34;&gt;hotels&lt;/span&gt;[&lt;span style=&#34;color:#ae81ff&#34;&gt;0&lt;/span&gt;], &lt;span style=&#34;color:#a6e22e&#34;&gt;hotels&lt;/span&gt;[&lt;span style=&#34;color:#ae81ff&#34;&gt;1&lt;/span&gt;]))
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;code&gt;regexp.QuoteMeta&lt;/code&gt; escapes the multi-line query, and reusing &lt;code&gt;selectHotelsQuery&lt;/code&gt; means a renamed
column or a changed &lt;code&gt;WHERE&lt;/code&gt; clause fails the test without the test duplicating the query text - one
source of truth, still verified. The arguments are pinned too, and the third one is &lt;code&gt;3&lt;/code&gt;:
&lt;code&gt;pageSize+1&lt;/code&gt;, the pagination contract from section 2.6 encoded as an assertion.&lt;/p&gt;
&lt;p&gt;The interesting half of the table is the failure paths, because each row corresponds to a real bug
class:&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Case&lt;/th&gt;
					&lt;th&gt;How it is simulated&lt;/th&gt;
					&lt;th&gt;Asserted&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Query failure&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;WillReturnError(errDefault)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;&amp;quot;find hotels by name:&amp;quot;&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Scan failure&lt;/td&gt;
					&lt;td&gt;a &lt;code&gt;nil&lt;/code&gt; for a non-nullable column&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;&amp;quot;scan hotel:&amp;quot;&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Iteration failure&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;hotelRows(...).RowError(0, errDefault)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;&amp;quot;iterate hotels:&amp;quot;&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Exec failure&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;WillReturnError(errDefault)&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;&amp;quot;insert hotels:&amp;quot;&lt;/code&gt; / &lt;code&gt;&amp;quot;delete all hotels:&amp;quot;&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The iteration case is the one worth copying: &lt;code&gt;rows.Err()&lt;/code&gt; is the check most codebases forget, and
&lt;code&gt;RowError&lt;/code&gt; is how it gets a test.&lt;/p&gt;
&lt;p&gt;What this buys: the SQL text, the argument order, the pagination arithmetic and four error paths,
with no database, in milliseconds. What it does not buy, stated plainly: &lt;code&gt;ILIKE&lt;/code&gt;, the
&lt;code&gt;unnest($1::text[], ...)&lt;/code&gt; insert and the type casts are never parsed by PostgreSQL. A typo in a cast
would pass every test in this file.&lt;/p&gt;
&lt;h3 id=&#34;56-mocks-generated-curated-disposable&#34;&gt;5.6 Mocks: generated, curated, disposable&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;.mockery.yml&lt;/code&gt; is short, and every setting is a decision:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; style=&#34;color:#f8f8f2;background-color:#272822;-moz-tab-size:2;-o-tab-size:2;tab-size:2;-webkit-text-size-adjust:none;&#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:#f92672&#34;&gt;dir&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;{{.InterfaceDir}}/mock&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:#f92672&#34;&gt;filename&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;mocks.go&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:#f92672&#34;&gt;structname&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;Mock{{.InterfaceName | firstUpper}}&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:#f92672&#34;&gt;pkgname&lt;/span&gt;: &lt;span style=&#34;color:#e6db74&#34;&gt;&amp;#39;mock&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:#f92672&#34;&gt;template&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;testify&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;formatter&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;goimports&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;formatter-options&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;  &lt;span style=&#34;color:#f92672&#34;&gt;goimports&lt;/span&gt;:
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;    &lt;span style=&#34;color:#f92672&#34;&gt;local-prefix&lt;/span&gt;: &lt;span style=&#34;color:#ae81ff&#34;&gt;hotel-booking-api&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span style=&#34;display:flex;&#34;&gt;&lt;span&gt;&lt;span style=&#34;color:#f92672&#34;&gt;all&lt;/span&gt;: &lt;span style=&#34;color:#66d9ef&#34;&gt;false&lt;/span&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;all: false&lt;/code&gt; with an explicit list of packages and interfaces means only the chosen seams are
mocked - and the file itself documents where the architecture&amp;rsquo;s replaceable points are.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;dir: &#39;{{.InterfaceDir}}/mock&#39;&lt;/code&gt; puts mocks in a &lt;code&gt;mock&lt;/code&gt; sub-package next to the interface, so no
package both is mocked and does the mocking.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;template: testify&lt;/code&gt; gives the &lt;code&gt;EXPECT()...Once()&lt;/code&gt; style used above.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;local-prefix: hotel-booking-api&lt;/code&gt; - the module name from section 1.2 reappears, because import grouping
of the generated files depends on it. A nice demonstration that a module path is not cosmetic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;go tool task mocks&lt;/code&gt; deletes every &lt;code&gt;mock&lt;/code&gt; package and regenerates it, which is what makes a
regenerated mock trustworthy after an interface change.&lt;/p&gt;
&lt;h3 id=&#34;57-what-this-suite-is-worth&#34;&gt;5.7 What this suite is worth&lt;/h3&gt;
&lt;p&gt;The whole thing runs with no Docker, no PostgreSQL and no network, in parallel
(&lt;code&gt;t.Parallel()&lt;/code&gt; appears in nearly every test), in well under a second. What it verifies is the set of
&lt;em&gt;contracts between layers&lt;/em&gt; - the SQL text, the argument lists, the error vocabulary, the route
table, the shutdown ordering. What it does not verify is the system: no real database ever parses
those statements, and no request travels from socket to storage.&lt;/p&gt;
&lt;p&gt;The lane that would close that gap is easy to describe: boot a throwaway PostgreSQL - the Compose
service that already exists, or &lt;a href=&#34;https://golang.testcontainers.org/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Testcontainers for Go&lt;/a&gt;
 when the
test should own the lifecycle - apply &lt;code&gt;specs/postgres.sql&lt;/code&gt;, run the repository methods and assert on
real rows. It buys hermeticity at the price of a Docker dependency in CI and seconds per run.&lt;/p&gt;
&lt;p&gt;At the outermost rung sits a smoke test that starts the whole stack and drives one booking from seed
to lookup. It is the only test that can catch a wiring mistake in &lt;code&gt;init()&lt;/code&gt; - and wiring is exactly
what per-layer mocks are blind to. Neither the integration lane nor the smoke test exists in this
example yet; both are named in section 6 rather than implied to be present.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Testing each layer separately is what makes the layering real.&lt;/strong&gt; If a layer cannot be tested with
exactly one thing faked beneath it, it is not a layer - it is a folder.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;6-what-this-example-deliberately-leaves-out&#34;&gt;6. What This Example Deliberately Leaves Out&lt;/h2&gt;
&lt;p&gt;Collected in one place, with the reason and the fix, so the scope is a decision rather than a series
of apologies.&lt;/p&gt;
&lt;table&gt;
	&lt;thead&gt;
			&lt;tr&gt;
					&lt;th&gt;Omitted&lt;/th&gt;
					&lt;th&gt;Why it is absent&lt;/th&gt;
					&lt;th&gt;What the fix looks like&lt;/th&gt;
			&lt;/tr&gt;
	&lt;/thead&gt;
	&lt;tbody&gt;
			&lt;tr&gt;
					&lt;td&gt;Authentication / authorization&lt;/td&gt;
					&lt;td&gt;Out of scope for an architecture post&lt;/td&gt;
					&lt;td&gt;Middleware plus a per-route policy&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;A real readiness probe (only &lt;code&gt;/health&lt;/code&gt; exists)&lt;/td&gt;
					&lt;td&gt;One probe is enough to run locally&lt;/td&gt;
					&lt;td&gt;Split liveness (no dependencies) from readiness (DB ping)&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;&lt;code&gt;ReadHeaderTimeout&lt;/code&gt;, &lt;code&gt;ReadTimeout&lt;/code&gt;, &lt;code&gt;MaxHeaderBytes&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;Kept out so the bootstrap stays readable&lt;/td&gt;
					&lt;td&gt;The hardened &lt;code&gt;http.Server&lt;/code&gt; in section 4.5&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;An error taxonomy (&lt;code&gt;errors.Is&lt;/code&gt; to status mapping)&lt;/td&gt;
					&lt;td&gt;Handlers currently return &lt;code&gt;500&lt;/code&gt; with &lt;code&gt;err.Error()&lt;/code&gt;; naming the gap beats half-fixing it&lt;/td&gt;
					&lt;td&gt;Sentinel errors in the service, mapping in the handler&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Integration tests against a real PostgreSQL&lt;/td&gt;
					&lt;td&gt;Would add a Docker dependency to every test run&lt;/td&gt;
					&lt;td&gt;The lane described in section 5.7&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;An end-to-end smoke test&lt;/td&gt;
					&lt;td&gt;Same reason - the per-layer suite is the point of section 5&lt;/td&gt;
					&lt;td&gt;One seeded booking driven through the real stack&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Metrics and tracing&lt;/td&gt;
					&lt;td&gt;Meaningless without a backend&lt;/td&gt;
					&lt;td&gt;&lt;code&gt;otelhttp&lt;/code&gt; middleware plus &lt;code&gt;/metrics&lt;/code&gt;&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;Migrations&lt;/td&gt;
					&lt;td&gt;Compose applies &lt;code&gt;specs/postgres.sql&lt;/code&gt; as an init script&lt;/td&gt;
					&lt;td&gt;A migration tool and versioned files&lt;/td&gt;
			&lt;/tr&gt;
			&lt;tr&gt;
					&lt;td&gt;A production gate on &lt;code&gt;/v1/testing&lt;/code&gt;&lt;/td&gt;
					&lt;td&gt;It is a demo affordance&lt;/td&gt;
					&lt;td&gt;A config flag or a build tag&lt;/td&gt;
			&lt;/tr&gt;
	&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;None of these is a hidden gap. Each is a delimited next step, and the checklist below is the audit
that catches them.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;7-the-checklist&#34;&gt;7. The Checklist&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Structure&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; One &lt;code&gt;go.mod&lt;/code&gt; at the example root; the module path is short and stable, not a URL.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; &lt;code&gt;cmd/&amp;lt;binary&amp;gt;&lt;/code&gt; contains only &lt;code&gt;main&lt;/code&gt; - no &lt;code&gt;init()&lt;/code&gt;, no globals, one &lt;code&gt;os.Exit&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; One can name, in a sentence, what each layer may and may not import.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; Interfaces are declared where they are consumed, and are as narrow as the consumer needs.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; &lt;code&gt;internal/&lt;/code&gt; everywhere something would otherwise be published by accident.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Local workflow&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; &lt;code&gt;docker compose up --build&lt;/code&gt; brings the stack up from a clean clone.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; Schema changes are documented as requiring &lt;code&gt;down -v&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; Dev tooling is pinned in &lt;code&gt;go.mod&lt;/code&gt; (&lt;code&gt;tool&lt;/code&gt;) or equivalent - not installed by hand.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; Generated mocks are deleted before regeneration; coverage excludes them.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Lifecycle&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; &lt;code&gt;SIGINT&lt;/code&gt; &lt;strong&gt;and&lt;/strong&gt; &lt;code&gt;SIGTERM&lt;/code&gt; are handled; the process exits non-zero with one readable log line.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; Shutdown order is: stop accepting -&amp;gt; drain -&amp;gt; stop background work -&amp;gt; close the database.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; Background workers select on &lt;code&gt;ctx.Done()&lt;/code&gt; and check &lt;code&gt;ctx.Err()&lt;/code&gt; between items.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; &lt;code&gt;terminationGracePeriodSeconds&lt;/code&gt; exceeds the shutdown timeout; the entrypoint is exec-form.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; &lt;code&gt;ReadHeaderTimeout&lt;/code&gt; and friends are set, or the omission is deliberate and documented.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Tests&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; Every layer has tests that fake exactly one thing beneath it.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; SQL is asserted against the production constants, not against copies.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; Argument assertions include the pagination arithmetic.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; &lt;code&gt;rows.Err()&lt;/code&gt; has a test.&lt;/li&gt;
&lt;li&gt;&lt;input disabled=&#34;&#34; type=&#34;checkbox&#34;&gt; Fake expectations are verified in &lt;code&gt;t.Cleanup&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h2 id=&#34;8-conclusion&#34;&gt;8. Conclusion&lt;/h2&gt;
&lt;p&gt;The layering is what makes the four different kinds of test possible, and the bootstrap is what makes
the layer stack deployable. Everything else in this post is in service of two sentences:
&lt;strong&gt;architecture is the set of seams you can replace, and the bootstrap is the promise that the
process will not be killed mid-request.&lt;/strong&gt; A &lt;code&gt;Handler&lt;/code&gt; interface nobody can fake, or a signal handler
nobody installed, undoes both - and neither failure shows up in a code review of a single file.&lt;/p&gt;
&lt;p&gt;At the same time, this example is a teaching shape. Everything it lacks fits in one table, one
section up: authentication, metrics, integration tests, a real readiness probe, timeouts on the HTTP
server. Each is a delimited next step rather than a hidden assumption, and two of them are the ones
worth reaching for first: &lt;strong&gt;an error taxonomy across layer boundaries&lt;/strong&gt;, and the &lt;strong&gt;room-allocation
rule&lt;/strong&gt; that section 2.4 only gestures at.&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;9-references--examples&#34;&gt;9. References &amp;amp; Examples&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Examples&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/serhii-chechun/developers-blog/tree/main/programming/go/http-api/examples/hotel-booking-api&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;The &lt;code&gt;hotel-booking-api&lt;/code&gt; example&lt;/a&gt;
 - the complete, runnable module&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/serhii-chechun/developers-blog/blob/main/programming/go/http-api/examples/hotel-booking-api/specs/openapi.yaml&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;&lt;code&gt;specs/openapi.yaml&lt;/code&gt;&lt;/a&gt;
 - the API contract&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/serhii-chechun/developers-blog/blob/main/programming/go/http-api/examples/hotel-booking-api/specs/postgres.sql&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;&lt;code&gt;specs/postgres.sql&lt;/code&gt;&lt;/a&gt;
 - the schema, also the Compose init script&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Go documentation&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://pkg.go.dev/net/http&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;&lt;code&gt;net/http&lt;/code&gt;&lt;/a&gt;
 - &lt;code&gt;Server.Shutdown&lt;/code&gt;, &lt;code&gt;Server.BaseContext&lt;/code&gt;, &lt;code&gt;ErrServerClosed&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://pkg.go.dev/os/signal&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;&lt;code&gt;os/signal&lt;/code&gt;&lt;/a&gt;
 - &lt;code&gt;Notify&lt;/code&gt;, and the default disposition of &lt;code&gt;SIGTERM&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://pkg.go.dev/context&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;&lt;code&gt;context&lt;/code&gt;&lt;/a&gt;
 - deadlines, cancellation and propagation&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://pkg.go.dev/database/sql&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;&lt;code&gt;database/sql&lt;/code&gt;&lt;/a&gt;
 - connection pool settings, &lt;code&gt;BeginTx&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://pkg.go.dev/errors&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;&lt;code&gt;errors&lt;/code&gt;&lt;/a&gt;
 - &lt;code&gt;Is&lt;/code&gt;, &lt;code&gt;As&lt;/code&gt;, &lt;code&gt;Join&lt;/code&gt;, and &lt;code&gt;%w&lt;/code&gt; wrapping&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://pkg.go.dev/testing/synctest&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;&lt;code&gt;testing/synctest&lt;/code&gt;&lt;/a&gt;
 - deterministic concurrency tests with virtual time&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://go.dev/doc/modules/managing-dependencies&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;&lt;code&gt;go.mod&lt;/code&gt; &lt;code&gt;tool&lt;/code&gt; directives&lt;/a&gt;
 - pinning dev tools as dependencies&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://go.dev/wiki/CodeReviewComments&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Go Code Review Comments&lt;/a&gt;
 - the &lt;em&gt;Interfaces&lt;/em&gt; section is the source of the consumer-side rule quoted in section 2.3; the same page covers &lt;code&gt;internal&lt;/code&gt; and error wrapping&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Libraries used&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/gin-gonic/gin&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Gin&lt;/a&gt;
, &lt;a href=&#34;https://github.com/lib/pq&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;&lt;code&gt;lib/pq&lt;/code&gt;&lt;/a&gt;
,
&lt;a href=&#34;https://github.com/DATA-DOG/go-sqlmock&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;&lt;code&gt;go-sqlmock&lt;/code&gt;&lt;/a&gt;
, &lt;a href=&#34;https://github.com/vektra/mockery&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;mockery&lt;/a&gt;
,
&lt;a href=&#34;https://github.com/sethvargo/go-envconfig&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;&lt;code&gt;go-envconfig&lt;/code&gt;&lt;/a&gt;
,
&lt;a href=&#34;https://github.com/stretchr/testify&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;testify&lt;/a&gt;
, &lt;a href=&#34;https://taskfile.dev/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Task&lt;/a&gt;
,
&lt;a href=&#34;https://github.com/google/uuid&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;&lt;code&gt;google/uuid&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Container lifecycle&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Kubernetes: Pod Lifecycle&lt;/a&gt;
 - &lt;code&gt;preStop&lt;/code&gt;, grace periods&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://docs.docker.com/reference/cli/docker/container/stop/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Docker: &lt;code&gt;docker stop&lt;/code&gt;&lt;/a&gt;
 - the &lt;code&gt;SIGTERM&lt;/code&gt;-then-&lt;code&gt;SIGKILL&lt;/code&gt; sequence&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://golang.testcontainers.org/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Testcontainers for Go&lt;/a&gt;
 - an integration lane, when it is time for one&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Related posts&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://blog.chechun.org/posts/optimizations-in-go/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Optimizations in Go: A Multi-Layer Framework&lt;/a&gt;
 - same
&amp;ldquo;every claim is reproducible&amp;rdquo; promise&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://blog.chechun.org/posts/regexp-in-go-deep-dive/&#34; target=&#34;_blank&#34; rel=&#34;noopener noreferrer&#34;&gt;Regular Expressions in Go: A Deep Dive&lt;/a&gt;
 - same framing: a design
decision, and the trade it implies&lt;/li&gt;
&lt;/ul&gt;
</content>
    </item>
    
  </channel>
</rss>
