{"id":48743866,"url":"https://github.com/kaio6fellipe/event-driven-bookinfo","last_synced_at":"2026-04-25T19:01:11.720Z","repository":{"id":349924213,"uuid":"1202460287","full_name":"kaio6fellipe/event-driven-bookinfo","owner":"kaio6fellipe","description":"Go hexagonal architecture monorepo — book review system demonstrating real-time event-driven architecture with Argo Events, Kafka, and full observability (OTel, Prometheus, Pyroscope, slog)","archived":false,"fork":false,"pushed_at":"2026-04-25T00:46:56.000Z","size":1120,"stargazers_count":0,"open_issues_count":9,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-25T02:37:10.797Z","etag":null,"topics":["argo-events","bookinfo","cqrs","docker","event-driven-architecture","github-actions","golang","hexagonal-architecture","htmx","kafka","kubernetes","kustomize","microservices","opentelemetry","prometheus","pyroscope"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"unlicense","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/kaio6fellipe.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":".github/CODEOWNERS","security":".github/SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-06T03:39:09.000Z","updated_at":"2026-04-16T15:42:25.000Z","dependencies_parsed_at":null,"dependency_job_id":"978124ad-b5b4-4642-8244-bb277510b3da","html_url":"https://github.com/kaio6fellipe/event-driven-bookinfo","commit_stats":null,"previous_names":["kaio6fellipe/event-driven-bookinfo"],"tags_count":46,"template":false,"template_full_name":null,"purl":"pkg:github/kaio6fellipe/event-driven-bookinfo","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kaio6fellipe%2Fevent-driven-bookinfo","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kaio6fellipe%2Fevent-driven-bookinfo/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kaio6fellipe%2Fevent-driven-bookinfo/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kaio6fellipe%2Fevent-driven-bookinfo/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/kaio6fellipe","download_url":"https://codeload.github.com/kaio6fellipe/event-driven-bookinfo/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/kaio6fellipe%2Fevent-driven-bookinfo/sbom","scorecard":{"id":1246051,"data":{"date":"2026-04-14T23:00:38Z","repo":{"name":"github.com/kaio6fellipe/event-driven-bookinfo","commit":"04e95bea8fd61724edf05c1fb46d5ec4a0fb0cbd"},"scorecard":{"version":"v5.3.0","commit":"c22063e786c11f9dd714d777a687ff7c4599b600"},"score":7.7,"checks":[{"name":"Dependency-Update-Tool","score":10,"reason":"update tool detected","details":["Info: detected update tool: Dependabot: .github/dependabot.yml:1"],"documentation":{"short":"Determines if the project uses a dependency update tool.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#dependency-update-tool"}},{"name":"Maintained","score":0,"reason":"project was created within the last 90 days. Please review its contents carefully","details":["Warn: Repository was created within the last 90 days."],"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#maintained"}},{"name":"Binary-Artifacts","score":10,"reason":"no binaries found in the repo","details":null,"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#binary-artifacts"}},{"name":"Code-Review","score":0,"reason":"Found 0/23 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#code-review"}},{"name":"Token-Permissions","score":10,"reason":"GitHub workflow tokens follow principle of least privilege","details":["Warn: jobLevel 'contents' permission set to 'write': .github/workflows/auto-tag.yml:16","Warn: jobLevel 'actions' permission set to 'write': .github/workflows/auto-tag.yml:17","Info: jobLevel 'contents' permission set to 'read': .github/workflows/ci.yml:16","Info: jobLevel 'contents' permission set to 'read': .github/workflows/ci.yml:53","Info: jobLevel 'contents' permission set to 'read': .github/workflows/ci.yml:98","Info: jobLevel 'contents' permission set to 'read': .github/workflows/ci.yml:135","Info: jobLevel 'contents' permission set to 'read': .github/workflows/ci.yml:160","Info: jobLevel 'contents' permission set to 'read': .github/workflows/ci.yml:216","Info: jobLevel 'contents' permission set to 'read': .github/workflows/ci.yml:36","Info: jobLevel 'contents' permission set to 'read': .github/workflows/ci.yml:114","Info: jobLevel 'contents' permission set to 'read': .github/workflows/ci.yml:232","Warn: jobLevel 'contents' permission set to 'write': .github/workflows/ci.yml:249","Info: jobLevel 'contents' permission set to 'read': .github/workflows/codeql.yml:20","Info: jobLevel 'contents' permission set to 'read': .github/workflows/helm-lint-test.yml:16","Warn: jobLevel 'contents' permission set to 'write': .github/workflows/helm-release.yml:18","Info: jobLevel 'actions' permission set to 'read': .github/workflows/release.yml:108","Info: jobLevel 'contents' permission set to 'read': .github/workflows/scorecard-pr.yml:15","Info: jobLevel 'issues' permission set to 'read': .github/workflows/scorecard-pr.yml:17","Info: jobLevel 'checks' permission set to 'read': .github/workflows/scorecard-pr.yml:18","Info: jobLevel 'contents' permission set to 'read': .github/workflows/scorecard.yml:20","Info: jobLevel 'actions' permission set to 'read': .github/workflows/scorecard.yml:21","Info: found token with 'none' permissions: .github/workflows/auto-tag.yml:1","Info: found token with 'none' permissions: .github/workflows/ci.yml:1","Info: found token with 'none' permissions: .github/workflows/codeql.yml:1","Info: found token with 'none' permissions: .github/workflows/helm-lint-test.yml:1","Info: found token with 'none' permissions: .github/workflows/helm-release.yml:1","Info: found token with 'none' permissions: .github/workflows/release.yml:1","Info: found token with 'none' permissions: .github/workflows/scorecard-pr.yml:1","Info: topLevel permissions set to 'read-all': .github/workflows/scorecard.yml:11"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#token-permissions"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#dangerous-workflow"}},{"name":"Security-Policy","score":10,"reason":"security policy file detected","details":["Info: security policy file detected: .github/SECURITY.md:1","Info: Found linked content: .github/SECURITY.md:1","Info: Found disclosure, vulnerability, and/or timelines in security policy: .github/SECURITY.md:1","Info: Found text in security policy: .github/SECURITY.md:1"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#security-policy"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#cii-best-practices"}},{"name":"Signed-Releases","score":10,"reason":"5 out of the last 5 releases have a total of 5 signed artifacts.","details":["Info: provenance for release artifact: multiple.intoto.jsonl: https://github.com/kaio6fellipe/event-driven-bookinfo/releases/tag/reviews-v0.4.0","Info: provenance for release artifact: multiple.intoto.jsonl: https://github.com/kaio6fellipe/event-driven-bookinfo/releases/tag/reviews-v0.3.0","Info: provenance for release artifact: multiple.intoto.jsonl: https://github.com/kaio6fellipe/event-driven-bookinfo/releases/tag/ratings-v0.4.0","Info: provenance for release artifact: multiple.intoto.jsonl: https://github.com/kaio6fellipe/event-driven-bookinfo/releases/tag/ratings-v0.3.0","Info: provenance for release artifact: multiple.intoto.jsonl: https://github.com/kaio6fellipe/event-driven-bookinfo/releases/tag/productpage-v0.4.0"],"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#signed-releases"}},{"name":"Pinned-Dependencies","score":9,"reason":"dependency not pinned by hash detected -- score normalized to 9","details":["Warn: third-party GitHubAction not pinned by hash: .github/workflows/release.yml:111: update your workflow using https://app.stepsecurity.io/secureworkflow/kaio6fellipe/event-driven-bookinfo/release.yml/main?enable=pin","Warn: containerImage not pinned by hash: build/Dockerfile.goreleaser.productpage:1: pin your Docker image by updating gcr.io/distroless/static-debian12:nonroot to gcr.io/distroless/static-debian12:nonroot@sha256:a9329520abc449e3b14d5bc3a6ffae065bdde0f02667fa10880c49b35c109fd1","Warn: containerImage not pinned by hash: build/Dockerfile.productpage:9: pin your Docker image by updating gcr.io/distroless/static-debian12:nonroot to gcr.io/distroless/static-debian12:nonroot@sha256:a9329520abc449e3b14d5bc3a6ffae065bdde0f02667fa10880c49b35c109fd1","Info:  34 out of  34 GitHub-owned GitHubAction dependencies pinned","Info:  19 out of  20 third-party GitHubAction dependencies pinned","Info:   6 out of   8 containerImage dependencies pinned","Info:   1 out of   1 goCommand dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#pinned-dependencies"}},{"name":"Vulnerabilities","score":8,"reason":"2 existing vulnerabilities detected","details":["Warn: Project is vulnerable to: GO-2026-4771","Warn: Project is vulnerable to: GO-2026-4772"],"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#vulnerabilities"}},{"name":"Packaging","score":10,"reason":"packaging workflow detected","details":["Info: Project packages its releases by way of GitHub Actions.: .github/workflows/release.yml:25"],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#packaging"}},{"name":"SAST","score":7,"reason":"SAST tool detected but not run on all commits","details":["Info: SAST configuration detected: CodeQL","Warn: 10 commits out of 30 are checked with a SAST tool"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#sast"}},{"name":"Branch-Protection","score":8,"reason":"branch protection is not maximal on development and all release branches","details":["Info: 'allow deletion' disabled on branch 'main'","Info: 'force pushes' disabled on branch 'main'","Warn: 'branch protection settings apply to administrators' is disabled on branch 'main'","Info: 'stale review dismissal' is required to merge on branch 'main'","Warn: required approving review count is 1 on branch 'main'","Info: codeowner review is required on branch 'main'","Info: 'last push approval' is required to merge on branch 'main'","Info: 'up-to-date branches' is required to merge on branch 'main'","Info: status check found to merge onto on branch 'main'","Info: PRs are required in order to make changes on branch 'main'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#branch-protection"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: The Unlicense: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#license"}},{"name":"Fuzzing","score":10,"reason":"project is fuzzed","details":["Info: GoBuiltInFuzzer integration found: services/details/internal/core/domain/fuzz_test.go:9","Info: GoBuiltInFuzzer integration found: services/dlqueue/internal/core/domain/fuzz_test.go:10","Info: GoBuiltInFuzzer integration found: services/ratings/internal/core/domain/fuzz_test.go:9","Info: GoBuiltInFuzzer integration found: services/reviews/internal/core/domain/fuzz_test.go:9"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#fuzzing"}},{"name":"Contributors","score":3,"reason":"project has 1 contributing companies or organizations -- score normalized to 3","details":["Info: found contributions from: tidydaily"],"documentation":{"short":"Determines if the project has a set of contributors from multiple organizations (e.g., companies).","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#contributors"}},{"name":"CI-Tests","score":10,"reason":"30 out of 30 merged PRs checked by a CI test -- score normalized to 10","details":null,"documentation":{"short":"Determines if the project runs tests before pull requests are merged.","url":"https://github.com/ossf/scorecard/blob/c22063e786c11f9dd714d777a687ff7c4599b600/docs/checks.md#ci-tests"}}]},"last_synced_at":"2026-04-14T23:03:24.734Z","repository_id":349924213,"created_at":"2026-04-14T23:03:24.734Z","updated_at":"2026-04-14T23:03:24.734Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32273223,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-25T18:29:39.964Z","status":"ssl_error","status_checked_at":"2026-04-25T18:29:32.149Z","response_time":59,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["argo-events","bookinfo","cqrs","docker","event-driven-architecture","github-actions","golang","hexagonal-architecture","htmx","kafka","kubernetes","kustomize","microservices","opentelemetry","prometheus","pyroscope"],"created_at":"2026-04-12T09:12:11.103Z","updated_at":"2026-04-25T19:01:11.700Z","avatar_url":"https://github.com/kaio6fellipe.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003ch1 align=\"center\"\u003eEvent-Driven Bookinfo\u003c/h1\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/kaio6fellipe/event-driven-bookinfo/actions/workflows/ci.yml?query=branch%3Amain\"\u003e\u003cimg src=\"https://github.com/kaio6fellipe/event-driven-bookinfo/actions/workflows/ci.yml/badge.svg\" alt=\"CI Status\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://pkg.go.dev/github.com/kaio6fellipe/event-driven-bookinfo\"\u003e\u003cimg src=\"https://pkg.go.dev/badge/github.com/kaio6fellipe/event-driven-bookinfo.svg\" alt=\"Go Reference\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://go.dev/\"\u003e\u003cimg src=\"https://img.shields.io/github/go-mod/go-version/kaio6fellipe/event-driven-bookinfo?color=%239F50DA\u0026label=Go\" alt=\"Go Version\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://unlicense.org/\"\u003e\u003cimg src=\"https://img.shields.io/badge/license-Unlicense-blue.svg\" alt=\"License\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://securityscorecards.dev/viewer/?uri=github.com/kaio6fellipe/event-driven-bookinfo\"\u003e\u003cimg src=\"https://api.securityscorecards.dev/projects/github.com/kaio6fellipe/event-driven-bookinfo/badge\" alt=\"OpenSSF Scorecard\" /\u003e\u003c/a\u003e\n  \u003ca href=\"https://www.conventionalcommits.org\"\u003e\u003cimg src=\"https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg\" alt=\"Conventional Commits\" /\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n```mermaid\nflowchart LR\n    User([User])\n    Gateway[Envoy Gateway]\n\n    subgraph \"Query Side\"\n        ReadAPI[Read API]\n        ReadDB[(Read DB)]\n    end\n\n    subgraph \"Command Side\"\n        subgraph \"Argo Events\"\n            Webhook[Webhook]\n            Kafka[[Kafka EventBus]]\n            Sensor[Sensor]\n        end\n        WriteSvc[Write Services]\n        WriteDB[(Write DB)]\n    end\n\n    subgraph \"Failure Recovery\"\n        DLQ[(DLQueue)]\n    end\n\n    User --\u003e|GET Query| Gateway\n    User --\u003e|POST Command| Gateway\n\n    Gateway --\u003e|method: GET| ReadAPI\n    ReadAPI --\u003e ReadDB\n\n    Gateway --\u003e|method: POST| Webhook\n    Webhook --\u003e Kafka\n    Kafka --\u003e Sensor\n    Sensor --\u003e|HTTP Trigger| WriteSvc\n    WriteSvc --\u003e WriteDB\n\n    WriteDB -.-\u003e|replication| ReadDB\n\n    Sensor -.-\u003e|dlqTrigger\u003cbr/\u003eretries exhausted| DLQ\n    DLQ -.-\u003e|replay| Webhook\n\n    classDef queryStyle fill:#1e3a8a,stroke:#3b82f6,color:#fff\n    classDef writeStyle fill:#7c2d12,stroke:#f97316,color:#fff\n    classDef infraStyle fill:#1f2937,stroke:#06b6d4,color:#fff\n    classDef dlqStyle fill:#4a044e,stroke:#c026d3,color:#fff\n\n    class ReadAPI,ReadDB queryStyle\n    class WriteSvc,WriteDB writeStyle\n    class Gateway,Webhook,Kafka,Sensor infraStyle\n    class DLQ dlqStyle\n```\n\nGo hexagonal architecture monorepo adapting Istio's Bookinfo as a book review system, demonstrating real-time event-driven architecture with Argo Events and Kafka.\n\nServices are plain REST APIs — all event-driven complexity (Kafka consumers, retries, dead-letter queues) is abstracted by Argo Events EventSources and Sensors. The write path flows through Kafka via Argo Events, ensuring every mutation is event-sourced, while the read path remains synchronous HTTP. Failure recovery is built into the pipeline — a `dlqTrigger` on every sensor captures events that exhaust retries into the `dlqueue` service, where they can be inspected, replayed, or marked resolved. This demonstrates using Argo Events not only for workflow automation, but as a real-time event-driven architecture platform — a self-hosted alternative to Google Eventarc or AWS EventBridge. A standalone `ingestion` service publishes CloudEvents directly to a dedicated Kafka topic (`raw_books_details`). Downstream services consume these events by declaring an Argo Events Kafka EventSource and Sensor, independent of the CQRS webhook pipeline used by the UI.\n\n## Architecture Overview\n\n### Service Topology\n\n```mermaid\ngraph TD\n    PP[\"productpage (BFF)\u003cbr/\u003eGo + html/template + HTMX\u003cbr/\u003e:8080 / :9090\"]\n    D[\"details\u003cbr/\u003e:8081 / :9091\"]\n    R[\"reviews\u003cbr/\u003e:8082 / :9092\"]\n    RT[\"ratings\u003cbr/\u003e:8083 / :9093\"]\n    N[\"notification\u003cbr/\u003e:8084 / :9094\u003cbr/\u003e\u003ci\u003eevent consumer only\u003c/i\u003e\"]\n    DLQ[\"dlqueue\u003cbr/\u003e:8085 / :9095\u003cbr/\u003e\u003ci\u003efailure capture + replay\u003c/i\u003e\"]\n    Redis[\"Redis\u003cbr/\u003e\u003ci\u003epending review cache\u003c/i\u003e\"]\n\n    PP --\u003e|sync GET| D\n    PP --\u003e|sync GET| R\n    R --\u003e|sync GET| RT\n    PP --\u003e|\"pending cache\"| Redis\n\n    style PP fill:#6366f1,color:#fff,stroke:#818cf8\n    style D fill:#1a1d27,color:#e4e4e7,stroke:#2a2d3a\n    style R fill:#1a1d27,color:#e4e4e7,stroke:#2a2d3a\n    style RT fill:#1a1d27,color:#e4e4e7,stroke:#2a2d3a\n    style N fill:#1a1d27,color:#e4e4e7,stroke:#f59e0b\n    style DLQ fill:#1a1d27,color:#e4e4e7,stroke:#a855f7\n    style Redis fill:#ef4444,color:#fff,stroke:#dc2626\n```\n\n### Event-Driven Write Flow\n\n```mermaid\ngraph TD\n    Browser[\"Browser POST /partials/rating\"]\n    PP[\"productpage (BFF)\"]\n    GW[\"Gateway (CQRS routing)\"]\n    WH[\"External POST /v1/*\"]\n    ING[\"ingestion\u003cbr/\u003eOpen Library poller\"]\n    ES[\"Argo Events\u003cbr/\u003eEventSource (webhook)\"]\n    K[\"Kafka EventBus\u003cbr/\u003eCloudEvent + traceparent\"]\n\n    S1[\"Sensor: book-added\"]\n    S2[\"Sensor: review-submitted\"]\n    S3[\"Sensor: rating-submitted\"]\n\n    D[\"details-write\u003cbr/\u003ePOST /v1/details\"]\n    R[\"reviews-write\u003cbr/\u003ePOST /v1/reviews\"]\n    RT[\"ratings-write\u003cbr/\u003ePOST /v1/ratings\"]\n    N1[\"notification\u003cbr/\u003ePOST /v1/notifications\"]\n    N2[\"notification\u003cbr/\u003ePOST /v1/notifications\"]\n    N3[\"notification\u003cbr/\u003ePOST /v1/notifications\"]\n\n    DLQES[\"dlq-event-received\u003cbr/\u003eEventSource\"]\n    DLQS[\"Sensor: dlq-event-received\"]\n    DLQW[\"dlqueue-write\u003cbr/\u003ePOST /v1/events\"]\n\n    Browser --\u003e PP\n    PP --\u003e|\"POST /v1/* via gateway\"| GW\n    WH --\u003e GW\n    ING --\u003e|\"POST /v1/details\"| GW\n    GW --\u003e|\"method: POST\"| ES\n    ES --\u003e K\n\n    K --\u003e S1\n    K --\u003e S2\n    K --\u003e S3\n\n    S1 --\u003e|HTTP Trigger| D\n    S1 --\u003e|HTTP Trigger| N1\n\n    S2 --\u003e|HTTP Trigger| R\n    S2 --\u003e|HTTP Trigger| N2\n\n    S3 --\u003e|HTTP Trigger| RT\n    S3 --\u003e|HTTP Trigger| N3\n\n    S1 -.-\u003e|\"dlqTrigger (retries exhausted)\"| DLQES\n    S2 -.-\u003e|\"dlqTrigger (retries exhausted)\"| DLQES\n    S3 -.-\u003e|\"dlqTrigger (retries exhausted)\"| DLQES\n    DLQES --\u003e K\n    K --\u003e DLQS\n    DLQS --\u003e|HTTP Trigger| DLQW\n    DLQW -.-\u003e|replay via POST to eventsource_url| WH\n\n    style Browser fill:#6366f1,color:#fff,stroke:#818cf8\n    style PP fill:#6366f1,color:#fff,stroke:#818cf8\n    style GW fill:#1a1d27,color:#e4e4e7,stroke:#2a2d3a\n    style WH fill:#6366f1,color:#fff,stroke:#818cf8\n    style ES fill:#22c55e,color:#fff,stroke:#16a34a\n    style K fill:#f59e0b,color:#000,stroke:#d97706\n    style S1 fill:#1a1d27,color:#e4e4e7,stroke:#2a2d3a\n    style S2 fill:#1a1d27,color:#e4e4e7,stroke:#2a2d3a\n    style S3 fill:#1a1d27,color:#e4e4e7,stroke:#2a2d3a\n    style DLQES fill:#22c55e,color:#fff,stroke:#16a34a\n    style DLQS fill:#1a1d27,color:#e4e4e7,stroke:#a855f7\n    style DLQW fill:#1a1d27,color:#e4e4e7,stroke:#a855f7\n    style ING fill:#1a1d27,color:#e4e4e7,stroke:#10b981\n```\n\nReads are synchronous HTTP calls between services. Writes are fully async via the Envoy Gateway's method-based CQRS routing — POST requests are routed to Argo Events webhook EventSources, which publish to the Kafka EventBus. Sensors consume events and fire HTTP triggers against the write services. The gateway acts as the CQRS boundary, while services remain plain HTTP servers with no Kafka dependency.\n\n### Hexagonal Architecture\n\nEach backend service (details, reviews, ratings, notification) is structured into three layers:\n\n- **Core** — domain types, inbound ports (use-case interfaces), outbound ports (repository/client interfaces). No framework or infrastructure imports.\n- **Inbound adapters** — HTTP handlers that translate HTTP requests into core use-case calls.\n- **Outbound adapters** — repository implementations (`memory`, `postgres`) and external HTTP clients. Swapped at composition root via the `STORAGE_BACKEND` env var.\n\nAll service wiring happens in `services/\u003cname\u003e/cmd/main.go`. The shared `pkg/` packages handle cross-cutting concerns (config, logging, metrics, tracing, profiling, health, server lifecycle) so each service's `main.go` stays under ~60 lines.\n\n---\n\n## Services\n\n| Service | Release | Coverage | Type | API Port | Admin Port | Description |\n|---|---|---|---|---|---|---|\n| **productpage** | [![release](https://img.shields.io/github/v/release/kaio6fellipe/event-driven-bookinfo?filter=productpage-v*\u0026sort=semver\u0026display_name=tag\u0026label=release)](https://github.com/kaio6fellipe/event-driven-bookinfo/releases?q=productpage-v\u0026expanded=true) | [![coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/kaio6fellipe/event-driven-bookinfo/badges/coverage-productpage.json)](https://github.com/kaio6fellipe/event-driven-bookinfo/actions/workflows/ci.yml?query=branch%3Amain) | BFF (Go + HTMX) | 8080 | 9090 | Aggregates details + reviews + ratings into an HTML product page. Fans out sync GET calls; pending review cache via Redis. |\n| **details** | [![release](https://img.shields.io/github/v/release/kaio6fellipe/event-driven-bookinfo?filter=details-v*\u0026sort=semver\u0026display_name=tag\u0026label=release)](https://github.com/kaio6fellipe/event-driven-bookinfo/releases?q=details-v\u0026expanded=true) | [![coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/kaio6fellipe/event-driven-bookinfo/badges/coverage-details.json)](https://github.com/kaio6fellipe/event-driven-bookinfo/actions/workflows/ci.yml?query=branch%3Amain) | Backend | 8081 | 9091 | Book metadata CRUD. Event-written via `book-added` sensor. |\n| **reviews** | [![release](https://img.shields.io/github/v/release/kaio6fellipe/event-driven-bookinfo?filter=reviews-v*\u0026sort=semver\u0026display_name=tag\u0026label=release)](https://github.com/kaio6fellipe/event-driven-bookinfo/releases?q=reviews-v\u0026expanded=true) | [![coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/kaio6fellipe/event-driven-bookinfo/badges/coverage-reviews.json)](https://github.com/kaio6fellipe/event-driven-bookinfo/actions/workflows/ci.yml?query=branch%3Amain) | Backend | 8082 | 9092 | User reviews. Makes sync GET to ratings service. Event-written via `review-submitted` sensor. |\n| **ratings** | [![release](https://img.shields.io/github/v/release/kaio6fellipe/event-driven-bookinfo?filter=ratings-v*\u0026sort=semver\u0026display_name=tag\u0026label=release)](https://github.com/kaio6fellipe/event-driven-bookinfo/releases?q=ratings-v\u0026expanded=true) | [![coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/kaio6fellipe/event-driven-bookinfo/badges/coverage-ratings.json)](https://github.com/kaio6fellipe/event-driven-bookinfo/actions/workflows/ci.yml?query=branch%3Amain) | Backend | 8083 | 9093 | Star ratings per reviewer. Event-written via `rating-submitted` sensor. |\n| **notification** | [![release](https://img.shields.io/github/v/release/kaio6fellipe/event-driven-bookinfo?filter=notification-v*\u0026sort=semver\u0026display_name=tag\u0026label=release)](https://github.com/kaio6fellipe/event-driven-bookinfo/releases?q=notification-v\u0026expanded=true) | [![coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/kaio6fellipe/event-driven-bookinfo/badges/coverage-notification.json)](https://github.com/kaio6fellipe/event-driven-bookinfo/actions/workflows/ci.yml?query=branch%3Amain) | Event consumer | 8084 | 9094 | Receives POST from sensors, stores audit log. Exposes GET for review. |\n| **dlqueue** | [![release](https://img.shields.io/github/v/release/kaio6fellipe/event-driven-bookinfo?filter=dlqueue-v*\u0026sort=semver\u0026display_name=tag\u0026label=release)](https://github.com/kaio6fellipe/event-driven-bookinfo/releases?q=dlqueue-v\u0026expanded=true) | [![coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/kaio6fellipe/event-driven-bookinfo/badges/coverage-dlqueue.json)](https://github.com/kaio6fellipe/event-driven-bookinfo/actions/workflows/ci.yml?query=branch%3Amain) | Backend (hex arch) | 8085 | 9095 | Captures events failing sensor retry exhaustion; stores in PostgreSQL; supports replay via REST API |\n| **ingestion** | [![release](https://img.shields.io/github/v/release/kaio6fellipe/event-driven-bookinfo?filter=ingestion-v*\u0026sort=semver\u0026display_name=tag\u0026label=release)](https://github.com/kaio6fellipe/event-driven-bookinfo/releases?q=ingestion-v\u0026expanded=true) | [![coverage](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/kaio6fellipe/event-driven-bookinfo/badges/coverage-ingestion.json)](https://github.com/kaio6fellipe/event-driven-bookinfo/actions/workflows/ci.yml?query=branch%3Amain) | Producer (hex arch) | 8086 | 9096 | Polls Open Library for books on a configurable interval and publishes CloudEvents directly to Kafka (`raw_books_details` topic). Stateless; no storage adapters. |\n\nAll services expose their business API on the API port and observability endpoints (`/metrics`, `/healthz`, `/readyz`, `/debug/pprof/*`) on the admin port.\n\n---\n\n## Shared Packages\n\n| Package | Description |\n|---|---|\n| `pkg/config` | Loads all service configuration from environment variables with defaults. |\n| `pkg/health` | `/healthz` (liveness) and `/readyz` (readiness) handlers. Readiness supports optional check functions (e.g., `db.Ping`). |\n| `pkg/idempotency` | `Store` interface (`CheckAndRecord`) with memory + postgres adapters; `NaturalKey` (SHA-256 with `0x1f` separator to prevent boundary collisions); `Resolve` picks explicit `idempotency_key` when present, otherwise derives a natural key from business fields. |\n| `pkg/logging` | JSON `slog` logger with `otelslog` bridge for automatic `trace_id`/`span_id` injection. HTTP middleware that creates a request-scoped logger with `request_id`, method, path. |\n| `pkg/metrics` | OTel Metrics SDK -\u003e Prometheus exporter setup. HTTP middleware recording request duration, request count, and in-flight gauge. Go runtime metrics (goroutines, GC, memory). |\n| `pkg/profiling` | Pyroscope SDK wrapper. No-op when `PYROSCOPE_SERVER_ADDRESS` is unset. Enables CPU, alloc, inuse, goroutine, mutex, and block profiles. |\n| `pkg/server` | Dual-port HTTP server. API port gets the full middleware chain (logging -\u003e metrics -\u003e tracing -\u003e handler). Admin port gets observability routes. Graceful shutdown on SIGINT/SIGTERM. |\n| `pkg/telemetry` | OTel tracing setup with OTLP exporter. No-op when `OTEL_EXPORTER_OTLP_ENDPOINT` is unset. |\n\n---\n\n## Prerequisites\n\n- Go 1.25+\n- Docker (Docker Desktop recommended, ~8 GB RAM allocated)\n- [golangci-lint v2](https://golangci-lint.run/welcome/install/)\n- [goreleaser v2](https://goreleaser.com/install/) (for releases)\n\n**Additional for local Kubernetes (`make run-k8s`):**\n\n- [k3d](https://k3d.io/) (k3s-in-Docker)\n- [kubectl](https://kubernetes.io/docs/tasks/tools/)\n- [Helm](https://helm.sh/docs/intro/install/)\n\n---\n\n## Quick Start\n\n```bash\n# Build all services\nmake build-all\n\n# Run locally — open a separate terminal for each service\n\nSERVICE_NAME=ratings HTTP_PORT=8083 ADMIN_PORT=9093 ./bin/ratings\n\nSERVICE_NAME=details HTTP_PORT=8081 ADMIN_PORT=9091 ./bin/details\n\nSERVICE_NAME=reviews HTTP_PORT=8082 ADMIN_PORT=9092 \\\n  RATINGS_SERVICE_URL=http://localhost:8083 \\\n  ./bin/reviews\n\nSERVICE_NAME=notification HTTP_PORT=8084 ADMIN_PORT=9094 ./bin/notification\n\nSERVICE_NAME=dlqueue HTTP_PORT=8085 ADMIN_PORT=9095 ./bin/dlqueue\n\n# Optional: REDIS_URL=redis://localhost:6379 (enables pending review cache)\nSERVICE_NAME=productpage HTTP_PORT=8080 ADMIN_PORT=9090 \\\n  DETAILS_SERVICE_URL=http://localhost:8081 \\\n  REVIEWS_SERVICE_URL=http://localhost:8082 \\\n  RATINGS_SERVICE_URL=http://localhost:8083 \\\n  TEMPLATE_DIR=services/productpage/templates \\\n  ./bin/productpage\n\n# Optional standalone demo — publishes CloudEvents to Kafka\nSERVICE_NAME=ingestion HTTP_PORT=8086 ADMIN_PORT=9096 \\\n  KAFKA_BROKERS=localhost:9092 \\\n  KAFKA_TOPIC=raw_books_details \\\n  POLL_INTERVAL=5m \\\n  SEARCH_QUERIES=programming,golang \\\n  MAX_RESULTS_PER_QUERY=10 \\\n  ./bin/ingestion\n\n# Open in browser\nopen http://localhost:8080\n```\n\nBy default all services use the in-memory storage backend. To use PostgreSQL, set `STORAGE_BACKEND=postgres` and `DATABASE_URL=\u003cdsn\u003e` for each backend service. Note: the in-memory backend does not support multiple replicas (state is pod-local).\n\n**Or use Docker Compose** (PostgreSQL backend, all services with one command):\n\n```bash\nmake run          # Start all services + PostgreSQL, seed databases\nmake run-logs     # Tail logs\nmake stop         # Stop and remove containers\n```\n\n\u003e **Lite mode.** Docker Compose runs the synchronous service mesh\n\u003e only: postgres + redis + 5 backend services + productpage. Kafka,\n\u003e the ingestion service, Argo Events, and the observability stack\n\u003e are NOT included. Producers detect missing `KAFKA_BROKERS` and\n\u003e fall back to a no-op publisher; events are dropped silently.\n\u003e\n\u003e The full event-driven path (ingestion polling Open Library →\n\u003e Kafka → Argo Events EventSource → Sensor → notification HTTP\n\u003e trigger) is exercised only via `make run-k8s` (k3d-based local\n\u003e cluster).\n\n---\n\n## Makefile Targets\n\n| Target | Description |\n|---|---|\n| `make build SERVICE=\u003cname\u003e` | Build a single service binary to `bin/\u003cname\u003e` |\n| `make build-all` | Build all 7 service binaries |\n| `make test` | Run all tests |\n| `make test-cover` | Run tests with HTML coverage report |\n| `make test-race` | Run tests with the race detector |\n| `make lint` | Run golangci-lint across the whole module |\n| `make fmt` | Format all Go source files with gofmt |\n| `make vet` | Run go vet |\n| `make mod-tidy` | Tidy go module dependencies |\n| `make docker-build SERVICE=\u003cname\u003e` | Build Docker image for one service |\n| `make docker-build-all` | Build Docker images for all 7 services |\n| `make run` | Start all services via Docker Compose (PostgreSQL backend) |\n| `make stop` | Stop services and remove containers |\n| `make e2e` | Run E2E tests via docker-compose |\n| `make clean` | Remove `bin/` and `dist/` directories |\n| `make help` | List all available targets |\n| `make run-k8s` | Full local k8s setup (cluster, platform, observability, deploy, seed) |\n| `make stop-k8s` | Delete k3d cluster and all resources |\n| `make k8s-rebuild` | Fast iteration: rebuild images, reimport, rollout restart |\n| `make k8s-status` | Show pod status across all namespaces + access URLs |\n| `make k8s-logs` | Tail logs from bookinfo namespace |\n| `make k8s-cluster` | Create k3d cluster with port mappings |\n| `make k8s-platform` | Install Envoy Gateway, Strimzi, Kafka, Argo Events, Gateway |\n| `make k8s-observability` | Install Prometheus, Grafana, Tempo, Loki, Pyroscope, Alloy |\n| `make k8s-deploy` | Build images, import to k3d, deploy apps + Argo Events + HTTPRoutes |\n| `make k8s-seed` | Seed PostgreSQL databases with sample data |\n\n---\n\n## Docker\n\nEach service has its own Dockerfile in the `build/` directory (`build/Dockerfile.\u003cservice\u003e`). Backend services use `FROM scratch` for the smallest possible image; productpage uses `gcr.io/distroless/static-debian12:nonroot` because it needs the HTML templates directory alongside the binary.\n\n```bash\n# Build a single image\nmake docker-build SERVICE=ratings\n\n# Build all images\nmake docker-build-all\n\n# Run all services with PostgreSQL (recommended for local dev)\nmake run          # builds images, starts postgres + redis + all services, seeds databases\nmake run-logs     # tail service logs\nmake stop         # stop and remove containers (keeps data)\nmake clean-data   # stop and remove containers + postgres data volume\n\n# Or run directly with docker-compose (memory backend, for E2E tests)\ndocker compose -f test/e2e/docker-compose.yml up\n```\n\nImages are tagged `event-driven-bookinfo/\u003cservice\u003e:latest` locally. Released images are pushed to GitHub Container Registry with the service version tag:\n\n```\nghcr.io/kaio6fellipe/event-driven-bookinfo/productpage:\u003ctag\u003e\nghcr.io/kaio6fellipe/event-driven-bookinfo/details:\u003ctag\u003e\nghcr.io/kaio6fellipe/event-driven-bookinfo/reviews:\u003ctag\u003e\nghcr.io/kaio6fellipe/event-driven-bookinfo/ratings:\u003ctag\u003e\nghcr.io/kaio6fellipe/event-driven-bookinfo/notification:\u003ctag\u003e\nghcr.io/kaio6fellipe/event-driven-bookinfo/dlqueue:\u003ctag\u003e\nghcr.io/kaio6fellipe/event-driven-bookinfo/ingestion:\u003ctag\u003e\n```\n\n---\n\n## Kubernetes Deployment\n\nKubernetes manifests live under `deploy/` and use Kustomize with a `base` plus three overlays per service.\n\n| Overlay | Replicas | Storage | Log level | Notes |\n|---|---|---|---|---|\n| `dev` | 1 | memory | debug | No resource limits |\n| `staging` | 2 | postgres | info | `DATABASE_URL` from Secret |\n| `production` | 3 | postgres | warn | HPA (min 3, max 10, 70% CPU target), higher resource limits |\n\n```bash\n# Preview rendered manifests for one service/overlay\nkustomize build deploy/ratings/overlays/dev\n\n# Apply dev overlay\nkubectl apply -k deploy/ratings/overlays/dev\n\n# Apply all services (dev)\nfor svc in productpage details reviews ratings notification; do\n  kubectl apply -k deploy/$svc/overlays/dev\ndone\n```\n\nAll deployments expose dual ports (API `:8080` + admin `:9090`). Liveness and readiness probes target `/healthz` and `/readyz` on the admin port. Continuous profiling is push-based via the Pyroscope Go SDK with trace-to-profile correlation.\n\nFor a fully automated local development cluster with all infrastructure included (Kafka, Envoy Gateway, observability stack), see [Local Kubernetes Environment](#local-kubernetes-environment) below.\n\n---\n\n## Local Kubernetes Environment\n\nOne-command local development cluster using k3d. Deploys the full stack: Envoy Gateway, Kafka (KRaft), Argo Events, PostgreSQL, observability (Prometheus + Grafana + Tempo + Loki + Pyroscope + Alloy), and all services with CQRS read/write deployment split.\n\n### Cluster Architecture\n\n```mermaid\ngraph TD\n    Browser[\"Browser :8080\"]\n\n    subgraph envoy-gateway-system\n        EG[\"Envoy Gateway\"]\n        GWSvc[\"gateway (stable svc)\"]\n    end\n\n    subgraph platform\n        GW[\"Gateway: default-gw\"]\n        Kafka[\"Kafka KRaft\"]\n        ArgoCtrl[\"Argo Events Controller\"]\n    end\n\n    subgraph bookinfo\n        PP[\"productpage\"]\n\n        DR[\"details\"]\n        RR[\"reviews\"]\n        RTR[\"ratings\"]\n\n        DW[\"details-write\"]\n        RW[\"reviews-write\"]\n        RTW[\"ratings-write\"]\n        N[\"notification\"]\n\n        DLQR[\"dlqueue\"]\n        DLQW[\"dlqueue-write\"]\n        ING[\"ingestion\"]\n\n        ES[\"EventSources\"]\n        EB[\"EventBus\"]\n        S[\"Sensors\"]\n\n        PG[\"PostgreSQL\"]\n        Redis[\"Redis\"]\n    end\n\n    subgraph observability\n        Alloy[\"Alloy\"]\n        Prom[\"Prometheus\"]\n        Tempo[\"Tempo\"]\n        Loki[\"Loki\"]\n        Pyro[\"Pyroscope\"]\n        Grafana[\"Grafana :3000\"]\n    end\n\n    Browser --\u003e EG --\u003e GW\n    GW --\u003e|\"GET /\"| PP\n    GW --\u003e|\"POST /v1/*\"| ES\n    PP --\u003e|\"GET /v1/* via gateway\"| DR\n    PP --\u003e|\"GET /v1/* via gateway\"| RR\n    PP --\u003e|\"POST /v1/* via gateway\"| ES\n    RR --\u003e|\"GET via gateway\"| RTR\n\n    ES --\u003e EB\n    EB --\u003e Kafka\n    Kafka --\u003e S\n    S --\u003e|trigger| DW\n    S --\u003e|trigger| RW\n    S --\u003e|trigger| RTW\n    S --\u003e|trigger| N\n    S --\u003e|\"dlqTrigger\u003cbr/\u003e(on failure)\"| DLQW\n\n    DR \u0026 DW \u0026 RR \u0026 RW \u0026 RTR \u0026 RTW \u0026 N \u0026 DLQR \u0026 DLQW --\u003e PG\n    ING -.-\u003e|\"outbound HTTPS\"| GW\n    PP --\u003e Redis\n\n    Alloy -.-\u003e|scrape :9090/metrics| PP\n    Alloy -.-\u003e|OTLP traces| Tempo\n    Alloy -.-\u003e|remote_write| Prom\n    Alloy -.-\u003e|push logs| Loki\n    PP \u0026 DR \u0026 DW \u0026 RR \u0026 RW \u0026 RTR \u0026 RTW \u0026 N \u0026 DLQR \u0026 DLQW \u0026 ING -.-\u003e|push profiles| Pyro\n    Prom \u0026 Tempo \u0026 Loki \u0026 Pyro --\u003e Grafana\n\n    style Browser fill:#6366f1,color:#fff,stroke:#818cf8\n    style EG fill:#1a1d27,color:#e4e4e7,stroke:#2a2d3a\n    style GWSvc fill:#1a1d27,color:#e4e4e7,stroke:#2a2d3a\n    style GW fill:#1a1d27,color:#e4e4e7,stroke:#2a2d3a\n    style Kafka fill:#f59e0b,color:#000,stroke:#d97706\n    style EB fill:#f59e0b,color:#000,stroke:#d97706\n    style ES fill:#22c55e,color:#fff,stroke:#16a34a\n    style S fill:#22c55e,color:#fff,stroke:#16a34a\n    style ArgoCtrl fill:#22c55e,color:#fff,stroke:#16a34a\n    style DLQR fill:#1a1d27,color:#e4e4e7,stroke:#a855f7\n    style DLQW fill:#1a1d27,color:#e4e4e7,stroke:#a855f7\n    style Grafana fill:#e879f9,color:#000,stroke:#c026d3\n    style PG fill:#3b82f6,color:#fff,stroke:#2563eb\n    style Redis fill:#ef4444,color:#fff,stroke:#dc2626\n    style ING fill:#1a1d27,color:#e4e4e7,stroke:#10b981\n```\n\nThe cluster (`bookinfo-local`) runs four namespaces:\n\n| Namespace | Components |\n|---|---|\n| `platform` | Strimzi operator, Kafka (KRaft single-node), Argo Events controller, EventBus, Gateway `default-gw` |\n| `envoy-gateway-system` | Envoy Gateway controller, GatewayClass `eg`, stable `gateway` Service for CQRS routing |\n| `observability` | Prometheus, Grafana, Tempo, Loki, Pyroscope, Alloy (DaemonSet for logs + Deployment for metrics/traces) |\n| `bookinfo` | 11 app deployments (CQRS split incl. dlqueue read/write, plus `ingestion` single deployment), PostgreSQL, 4 EventSources (incl. `dlq-event-received`), 4 Sensors (incl. DLQ), method-based HTTPRoutes |\n\n### CQRS Deployment Split\n\nEach backend service deploys as two separate Deployments sharing the same image and PostgreSQL database: a read instance (serves GET traffic from productpage) and a write instance (receives POST triggers from Argo Events sensors).\n\n| Deployment | Role | Called by |\n|---|---|---|\n| `productpage` | Read-only BFF | Envoy Gateway (HTTPRoute) |\n| `details` / `details-write` | Read / Write | productpage / book-added sensor |\n| `reviews` / `reviews-write` | Read / Write | productpage / review-submitted sensor |\n| `ratings` / `ratings-write` | Read / Write | reviews (read) / rating-submitted sensor |\n| `notification` | Write-only | All 3 sensors |\n| `dlqueue` / `dlqueue-write` | Read / Write | operator/service API (GET) / `dlq-event-received` sensor (POST) |\n\n\u003e `ingestion` deploys as a single stateless deployment with no CQRS split — it is a pure event producer and is omitted from the table above.\n\n### Usage\n\n```bash\nmake run-k8s        # Full setup (~5-10 min first run)\nmake k8s-status     # Check status + access URLs\nmake k8s-rebuild    # Fast iteration (rebuild + redeploy, skip infra)\nmake k8s-logs       # Tail application logs\nmake stop-k8s       # Tear down\n```\n\n### Access URLs\n\n| Service | URL | Notes |\n|---|---|---|\n| Productpage | http://localhost:8080 | BFF web UI |\n| Grafana | http://localhost:3000 | admin / admin |\n| Prometheus | http://localhost:9090 | Metrics queries |\n| Webhooks | http://localhost:8080/v1/details | POST to trigger write flow (same port, method-based routing) |\n\n---\n\n## Argo Events\n\nThe event-driven write flow is implemented with Argo Events. This repository provides the EventSource and Sensor manifests only; the Kafka EventBus is expected to exist in the cluster.\n\n```\ndeploy/argo-events/\n├── eventsources/\n│   ├── book-added.yaml          # Webhook -\u003e Kafka\n│   ├── review-submitted.yaml    # Webhook -\u003e Kafka\n│   └── rating-submitted.yaml    # Webhook -\u003e Kafka\n└── sensors/\n    ├── book-added-sensor.yaml       # Triggers: details + notification\n    ├── review-submitted-sensor.yaml # Triggers: reviews + notification\n    └── rating-submitted-sensor.yaml # Triggers: ratings + notification\n```\n\nEach EventSource exposes an HTTP webhook whose endpoint mirrors the target service's API path (e.g., the book-added EventSource listens on /v1/details). The Envoy Gateway routes POST requests to these endpoints via method-based CQRS routing on port 8080. Argo Events converts received payloads to CloudEvents and publishes to Kafka. Sensors subscribe to specific event types and fire HTTP triggers against the write services.\n\nOTel trace context propagates via `traceparent`/`tracestate` CloudEvent extensions. Services extract context when present and start a new trace when it is absent (graceful degradation).\n\n```bash\n# Validate manifests without applying\nkubectl apply --dry-run=client -f deploy/argo-events/eventsources/\nkubectl apply --dry-run=client -f deploy/argo-events/sensors/\n```\n\n### Dead Letter Queue\n\nEvery primary sensor trigger carries `atLeastOnce: true` + exponential backoff and a `dlqTrigger` that fires after retry exhaustion. The dlqTrigger captures the full CloudEvents context (`id`, `type`, `source`, `subject`, `time`, `datacontenttype`) via `contextKey`, plus the original body and HTTP headers (preserving `traceparent` for distributed trace correlation), and POSTs the structured payload to a dedicated `dlq-event-received` EventSource. The DLQ event then flows through the standard Argo Events pipeline: EventSource → Kafka → DLQ sensor → `dlqueue-write` service → PostgreSQL.\n\nThe dlqueue service deduplicates arrivals by a natural composite key (`sensor_name + failed_trigger + SHA-256(original_payload)`). The CloudEvents `id` cannot be used as a dedup key because Argo Events regenerates it on every EventSource pass — per the CNCF CloudEvents spec, `id` is a hop-level identifier, not an end-to-end correlation key. Events are tracked through a state machine (`pending → replayed → resolved` on success; `poisoned` after `max_retries` failed replays).\n\nReplay is operator- or service-initiated via `POST /v1/events/{id}/replay`: dlqueue re-POSTs the original payload and headers to the source EventSource URL stored on the DLQ record, re-entering the full CQRS pipeline. All write services are idempotent (see `pkg/idempotency`) so replays are safe. For the full domain model, API surface, and metric definitions, see [docs/superpowers/specs/2026-04-13-dlqueue-service-design.md](docs/superpowers/specs/2026-04-13-dlqueue-service-design.md).\n\n---\n\n## Data Ingestion\n\nThe `ingestion` service is a synthetic-data generator that publishes CloudEvents directly to a dedicated Kafka topic (`raw_books_details`) using a native Go Kafka client (franz-go). It runs as a single stateless deployment with no storage adapters and no CQRS split.\n\nA background poll loop ticks every `POLL_INTERVAL` (default 5m). For each query in `SEARCH_QUERIES`, the service calls `GET https://openlibrary.org/search.json`, validates each returned book (title, ISBN, authors, and publish year are required), and publishes accepted books as CloudEvents to Kafka. Each event carries a deterministic idempotency key `ingestion-isbn-\u003cISBN\u003e` and CloudEvents headers (`ce_type: com.bookinfo.ingestion.book-added`, `ce_source: ingestion`). On-demand scrapes are also available via `POST /v1/ingestion/trigger`.\n\nThe Helm chart creates a Kafka EventSource (`ingestion-raw-books-details`) that reads from the topic and makes it available on the EventBus. Downstream services (like `details`) declare `events.consumed` in their Helm values to create a Consumer Sensor with triggers, independently of the CQRS webhook Sensor.\n\nConfiguration via `KAFKA_BROKERS`, `KAFKA_TOPIC`, `POLL_INTERVAL`, `SEARCH_QUERIES`, and `MAX_RESULTS_PER_QUERY` environment variables. The topic is auto-created on startup if it doesn't exist. Because idempotency is enforced at the write service, replays and overlapping cycles are safe.\n\n---\n\n## E2E Tests\n\nE2E tests spin up all five services via docker-compose and exercise each service's HTTP API with shell scripts.\n\n```bash\n# Run E2E tests (memory backend)\nmake e2e\n\n# Run E2E tests with PostgreSQL backend\ndocker compose -f test/e2e/docker-compose.yml \\\n               -f test/e2e/docker-compose.postgres.yml up -d\nbash test/e2e/run-tests.sh\n```\n\nIndividual test scripts under `test/e2e/` cover health endpoints, CRUD operations, and cross-service integration (e.g., reviews fetching ratings).\n\n\u003e **Scope.** `make e2e` covers HTTP-level acceptance tests\n\u003e (idempotency, validation, CRUD round-trips) under the lite-mode\n\u003e compose stack. The event chain (Kafka publish, Argo Events sensor,\n\u003e notification HTTP trigger) is verified end-to-end via Tempo trace\n\u003e inspection after `make run-k8s`.\n\n---\n\n## Observability\n\nAll observability endpoints are served on the admin port (default `:9090`) and are isolated from the business API.\n\n### Metrics\n\nPrometheus-format metrics are available at `/metrics` on the admin port. Each service exposes:\n\n- **HTTP middleware metrics**: `http_server_request_duration_seconds` (histogram), `http_server_requests_total` (counter), `http_server_active_requests` (gauge) — labeled by method, route, status.\n- **Go runtime metrics**: goroutine count, GC stats, memory usage.\n- **Business metrics** (per service):\n\n  | Service | Metric |\n  |---|---|\n  | ratings | `ratings_submitted_total` |\n  | details | `books_added_total` |\n  | reviews | `reviews_submitted_total` |\n  | notification | `notifications_dispatched_total`, `notifications_failed_total`, `notifications_by_status` |\n  | ingestion | `ingestion_scrapes_total`, `ingestion_books_published_total`, `ingestion_errors_total` |\n\n### Tracing\n\nOTel tracing with OTLP exporter. Set `OTEL_EXPORTER_OTLP_ENDPOINT` to enable. When the variable is unset the tracer is a no-op and the service runs without any collector dependency. Trace context is propagated between services via standard `traceparent` headers.\n\n```bash\nOTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318 ./bin/ratings\n```\n\n### Profiling\n\nPush-based continuous profiling with trace-to-profile correlation:\n\n- **Pyroscope Go SDK** with `grafana/otel-profiling-go` wrapper. Set `PYROSCOPE_SERVER_ADDRESS` to enable. No-op when unset. Profiles CPU, alloc, inuse objects/space, goroutines, mutex, and block. Span IDs are automatically injected into profiling samples, enabling Grafana to link Tempo traces directly to Pyroscope profiles.\n\n```bash\nPYROSCOPE_SERVER_ADDRESS=http://localhost:4040 ./bin/ratings\n```\n\n### Logging\n\nStructured JSON via `log/slog` with the `otelslog` bridge. Every log entry from a request-scoped context automatically includes `trace_id` and `span_id`. Each request log includes `request_id`, `method`, `path`, `status`, and `duration_ms`.\n\n```bash\nLOG_LEVEL=debug ./bin/ratings\n```\n\npprof endpoints are available at `/debug/pprof/*` on the admin port for on-demand profiling.\n\n---\n\n## Project Structure\n\n```\nevent-driven-bookinfo/\n├── services/\n│   ├── productpage/            # BFF — Go + html/template + HTMX\n│   │   ├── cmd/main.go\n│   │   ├── internal/\n│   │   │   ├── client/         # HTTP clients for backend services\n│   │   │   ├── handler/        # Page + API handlers\n│   │   │   └── model/          # Aggregated view models\n│   │   └── templates/          # HTML templates + HTMX partials\n│   ├── details/                # Book metadata (hex arch)\n│   │   ├── cmd/main.go\n│   │   └── internal/\n│   │       ├── core/           # domain/, port/inbound.go, port/outbound.go, service/\n│   │       └── adapter/\n│   │           ├── inbound/http/\n│   │           └── outbound/   # memory/ and postgres/\n│   ├── reviews/                # User reviews (hex arch, calls ratings)\n│   ├── ratings/                # Star ratings (hex arch)\n│   ├── notification/           # Event consumer + audit log (hex arch)\n│   ├── dlqueue/                # Dead letter queue (hex arch) — NEW\n│   │   ├── cmd/main.go\n│   │   ├── migrations/         # dlq_events + processed_events\n│   │   └── internal/\n│   │       ├── core/           # domain/, port/, service/\n│   │       ├── adapter/\n│   │       │   ├── inbound/http/\n│   │       │   └── outbound/   # memory/, postgres/, http/ (replay client)\n│   │       └── metrics/        # dlq_events_* counters\n│   └── ingestion/              # Producer (hex arch) — Open Library scraper, publishes to Gateway\n│       ├── cmd/main.go\n│       └── internal/\n│           ├── core/           # domain/, port/, service/\n│           └── adapter/\n│               ├── inbound/http/\n│               └── outbound/   # openlibrary/ (BookFetcher), kafka/ (EventPublisher)\n├── pkg/\n│   ├── config/                 # Env-based configuration\n│   ├── health/                 # /healthz and /readyz handlers\n│   ├── idempotency/            # Store interface + adapters; natural-key hashing\n│   ├── logging/                # slog + otelslog bridge + HTTP middleware\n│   ├── metrics/                # OTel -\u003e Prometheus + HTTP middleware + runtime\n│   ├── profiling/              # Pyroscope SDK wrapper\n│   ├── server/                 # Dual-port server + graceful shutdown\n│   └── telemetry/              # OTel tracing setup\n├── deploy/\n│   ├── \u003cservice\u003e/\n│   │   ├── base/               # Kustomize base (deployment, service, configmap)\n│   │   └── overlays/           # dev / staging / production / local patches\n│   ├── argo-events/\n│   │   ├── eventsources/       # Webhook EventSource manifests\n│   │   ├── sensors/            # Sensor + HTTP trigger manifests\n│   │   └── overlays/local/     # EventBus, sensors targeting -write services\n│   ├── gateway/\n│   │   ├── base/               # Gateway, GatewayClass, ReferenceGrant\n│   │   └── overlays/local/     # HTTPRoutes for bookinfo\n│   ├── observability/local/    # Helm values: Prometheus, Grafana, Tempo, Loki, Pyroscope, Alloy\n│   ├── platform/local/         # Helm values: Strimzi, Argo Events; Kafka CRDs\n│   ├── redis/local/            # Helm values: Bitnami Redis\n│   └── postgres/local/         # StatefulSet, Service, init ConfigMap\n├── test/\n│   └── e2e/                    # docker-compose files + shell test scripts\n├── build/\n│   └── Dockerfile.\u003cservice\u003e    # One per service (5 total)\n├── Makefile\n├── .golangci.yml               # golangci-lint v2 configuration\n├── .github/workflows/\n│   ├── release.yml             # Per-service release via workflow_dispatch\n│   └── auto-tag.yml            # Auto-tag on PR merge, dispatches release\n├── go.mod                      # Single module: github.com/kaio6fellipe/event-driven-bookinfo\n└── go.sum\n```\n\n---\n\n## Releasing\n\nEach service is versioned and released independently. Releases are fully automated on PR merge to `main`.\n\n### How It Works\n\n1. **PR merged to `main`** → `auto-tag.yml` runs\n2. **Detects changed services** — file paths under `services/\u003cname\u003e/`; changes to `pkg/`, `go.mod`, or `go.sum` trigger all 7 services\n3. **Determines version bump** — PR labels (`major`/`minor`) take priority, then conventional commit prefixes (`feat` → minor, `fix` → patch, `BREAKING CHANGE` → major), default is `patch`\n4. **Creates tag** — e.g., `details-v0.2.0`\n5. **Dispatches release** — `release.yml` builds binaries, Docker images (multi-arch), and creates a GitHub release\n\n### Tag Format\n\n```\n\u003cservice\u003e-v\u003cmajor\u003e.\u003cminor\u003e.\u003cpatch\u003e\n```\n\nExamples: `details-v0.1.0`, `reviews-v1.2.3`, `productpage-v0.3.0`\n\n### Manual Release\n\n```bash\n# Trigger a release for a specific service\ngh workflow run release.yml -f service=details -f tag=details-v0.1.0\n```\n\n### Version Bump Labels\n\n| Label | Effect |\n|-------|--------|\n| `major` | Major version bump (breaking change) |\n| `minor` | Minor version bump (new feature) |\n| *(none)* | Determined by conventional commits, default patch |\n\n### GoReleaser Configs\n\nPer-service configs at `services/\u003cname\u003e/.goreleaser.yaml`. Uses GoReleaser OSS with `GORELEASER_CURRENT_TAG` environment variable for version resolution.\n\n---\n\n## Contributing\n\n- Follow [Conventional Commits](https://www.conventionalcommits.org/) for commit messages (`feat:`, `fix:`, `docs:`, `refactor:`, etc.).\n- Run `make lint` and `make test` before opening a pull request. The CI pipeline enforces both.\n- Tests are table-driven. New handlers require both a unit test (`handler_test.go`) and service-layer test (`\u003cdomain\u003e_service_test.go`).\n- Use `make test-race` to check for data races before submitting.\n\n---\n\n## License\n\nThis project is licensed under the MIT License. See [LICENSE](LICENSE) for details.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkaio6fellipe%2Fevent-driven-bookinfo","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fkaio6fellipe%2Fevent-driven-bookinfo","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fkaio6fellipe%2Fevent-driven-bookinfo/lists"}