Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

[![Build Check](https://github.com/jonathanperis/rinha2-back-end-dotnet/actions/workflows/build-check.yml/badge.svg)](https://github.com/jonathanperis/rinha2-back-end-dotnet/actions/workflows/build-check.yml) [![Main Release](https://github.com/jonathanperis/rinha2-back-end-dotnet/actions/workflows/main-release.yml/badge.svg)](https://github.com/jonathanperis/rinha2-back-end-dotnet/actions/workflows/main-release.yml) [![CodeQL](https://github.com/jonathanperis/rinha2-back-end-dotnet/actions/workflows/codeql.yml/badge.svg)](https://github.com/jonathanperis/rinha2-back-end-dotnet/actions/workflows/codeql.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

**[Live demo →](https://jonathanperis.github.io/rinha2-back-end-dotnet/)** | **[Documentation →](https://github.com/jonathanperis/rinha2-back-end-dotnet/wiki)**
**[Live demo →](https://jonathanperis.github.io/rinha2-back-end-dotnet/)** | **[Documentation →](https://jonathanperis.github.io/rinha2-back-end-dotnet/docs/)**

---

Expand Down Expand Up @@ -54,6 +54,8 @@ API available at `http://localhost:9999`
| `/clientes/{id}/extrato` | GET | Get account balance statement |
| `/healthz` | GET | Health check |

For request/response payloads, validation behavior, and known implementation notes, see the [API Reference](https://jonathanperis.github.io/rinha2-back-end-dotnet/docs/api-reference/).

## Project Structure

```
Expand All @@ -69,8 +71,8 @@ rinha2-back-end-dotnet/
├── prod/docker-compose.yml — Prod stack with GHCR images
├── nginx.conf — Load balancer (least_conn)
├── grafana/ — Legacy Grafana dashboard provisioning
├── docs/wiki/ — Markdown source for the documentation/wiki pages
├── docs/ — Astro GitHub Pages site + published k6 reports
├── docs/wiki/ — Markdown source rendered to the Pages docs routes
├── docs/ — Astro 6.4 GitHub Pages site using Sätteri + published k6 reports
└── .github/workflows/ — CI/CD pipelines
```

Expand Down
49 changes: 46 additions & 3 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,30 @@
# Docs

Astro static site deployed to GitHub Pages. The Markdown pages under `docs/wiki/` are the source for the public documentation/wiki-style routes.
Astro static site deployed to GitHub Pages. The Markdown pages under `docs/wiki/` are the source for the public documentation routes at `/docs/`.

## Architecture

| Area | Source of truth |
|---|---|
| Site framework | Astro `^6.4.2` |
| Markdown renderer | Astro 6.4 `markdown.processor` using `@astrojs/markdown-satteri` |
| Markdown content | `docs/wiki/*.md` |
| Docs route renderer | `docs/src/pages/docs/[...slug].astro` |
| Sidebar/category order | `docs/src/sidebar.config.ts` |
| Reports archive | Static files in `docs/public/reports/*.html` rendered by `docs/src/pages/reports/index.astro` |
| Production base path | `/rinha2-back-end-dotnet` when `NODE_ENV=production` |
| Build output | `docs/out/` |

The docs are static. There is no server runtime after GitHub Pages publishes the generated HTML.

## Adding or editing a docs page

1. Add or edit Markdown in `docs/wiki/`.
2. If the page is new, add the slug to `SECTION_CATEGORIES` in `docs/src/sidebar.config.ts`.
3. Add a label and summary for the slug in `docs/src/pages/docs/[...slug].astro`.
4. Build locally before opening a PR.

The docs build has a build-time assertion that every sidebar slug maps to a real `docs/wiki/{slug}.md` file.

## Commands

Expand All @@ -10,8 +34,27 @@ Run from this directory (`docs/`):
|---|---|
| `bun install` | Install dependencies |
| `bun run dev` | Start dev server |
| `bun run build` | Build the Astro site to `./out/` |
| `bun run preview` | Preview production build locally |
| `NODE_ENV=production bun run build` | Build the GitHub Pages-shaped site to `./out/` with the repository base path |
| `bun run preview` | Preview the production build locally |
| `bun run lint` | Run ESLint |

Use Bun for this package so `bun.lock` stays authoritative.

## Markdown processor notes

The site uses Sätteri through Astro's `markdown.processor` API:

```js
import { satteri } from '@astrojs/markdown-satteri';

export default defineConfig({
markdown: {
processor: satteri(),
},
});
```

Do not assume arbitrary Remark/Rehype plugin behavior unless it has been tested with Sätteri. If a future docs feature needs Markdown plugins, validate the build and the rendered Pages output before merging.

## Environment

Expand Down
2 changes: 2 additions & 0 deletions docs/src/pages/docs/[...slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const SLUG_LABEL: Record<string, string> = {
challenge: 'Challenge',
'ci-cd-pipeline': 'CI/CD Pipeline',
'getting-started': 'Getting Started',
'api-reference': 'API Reference',
performance: 'Performance',
};

Expand All @@ -17,6 +18,7 @@ const SECTION_SUMMARIES: Record<string, { eyebrow: string; description: string;
challenge: { eyebrow: 'Spec', description: 'Rules, endpoints, validation behavior, and resource envelope.', signal: '1.5 CPU / 550MB' },
architecture: { eyebrow: 'Runtime', description: 'NGINX, two Native AOT API containers, PostgreSQL stored procedures, and telemetry.', signal: 'API x2 + PostgreSQL' },
'getting-started': { eyebrow: 'Runbook', description: 'Clone, boot the compose stack, smoke `/healthz`, and issue sample requests.', signal: 'copy-ready' },
'api-reference': { eyebrow: 'Contract', description: 'Endpoint payloads, response shapes, validation rules, and current implementation notes.', signal: 'HTTP 200 / 404 / 422' },
performance: { eyebrow: 'Evidence', description: 'Budget split, latency choices, k6 lane, and release validation links.', signal: 'AOT + k6' },
'ci-cd-pipeline': { eyebrow: 'Release', description: 'PR gates, GHCR publishing, CodeQL, and Pages deploy workflow.', signal: 'linear main' },
};
Expand Down
9 changes: 8 additions & 1 deletion docs/src/pages/reports/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ function parseReportMeta(filename: string) {
<main class="reports-main">
<header class="reports-header">
<h1>Stress Test Reports</h1>
<p>{reports.length} runs archived — k6 load tests executed on every push to <code>main</code></p>
<p>{reports.length} runs archived — k6 load-test HTML reports preserved from release validation runs and benchmark experiments.</p>
<p class="reports-note">Each file is an immutable report artifact. Use the docs performance page for methodology and only update homepage benchmark numbers when they can be traced to a specific archived report.</p>
</header>

<div class="reports-list">
Expand Down Expand Up @@ -101,6 +102,12 @@ function parseReportMeta(filename: string) {
font-size: 0.9rem;
}

.reports-note {
margin-top: 0.75rem;
max-width: 720px;
line-height: 1.6;
}

.reports-header code {
background: rgba(10, 22, 40, 0.8);
border: 1px solid var(--rust-dark);
Expand Down
2 changes: 1 addition & 1 deletion docs/src/sidebar.config.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const SECTION_CATEGORIES = [
{ label: "", ids: ["home"] },
{ label: "Overview", ids: ["challenge", "architecture"] },
{ label: "Develop", ids: ["getting-started", "performance", "ci-cd-pipeline"] },
{ label: "Develop", ids: ["getting-started", "api-reference", "performance", "ci-cd-pipeline"] },
] as const;

export const SECTION_ORDER = SECTION_CATEGORIES.flatMap(({ ids }) => ids);
115 changes: 115 additions & 0 deletions docs/wiki/api-reference.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# API Reference

## Base URL

Local and CI smoke tests reach the API through NGINX:

```text
http://localhost:9999
```

The production compose file exposes the same public entrypoint on port `9999`.

## Client IDs and limits

The implementation has five predefined clients. The API keeps this small map in memory for fast invalid-client rejection, and PostgreSQL seeds the same limits in `docker-entrypoint-initdb.d/rinha.dump.sql`.

| Client ID | Limit, in cents |
|---:|---:|
| `1` | `100000` |
| `2` | `80000` |
| `3` | `1000000` |
| `4` | `10000000` |
| `5` | `500000` |

## `POST /clientes/{id}/transacoes`

Submits a credit or debit transaction.

### Request body

```json
{
"valor": 1000,
"tipo": "c",
"descricao": "deposito"
}
```

| Field | Type | Rule |
|---|---|---|
| `valor` | integer | Required positive integer amount in cents (`> 0`) |
| `tipo` | string | Required; `"c"` for credit or `"d"` for debit |
| `descricao` | string | Required, non-empty, maximum 10 characters |

### Successful response

The response uses `snake_case` JSON output generated by `System.Text.Json` source generation.

```json
{
"id": 1,
"limite": 100000,
"saldo": 1000
}
```

### Status codes

| Status | When |
|---:|---|
| `200` | Transaction accepted and a balance is returned |
| `404` | Client ID is not one of `1` through `5` |
| `422` | Payload validation fails (`valor <= 0`, invalid `tipo`, missing/empty/long `descricao`) |

### Current implementation note

The intended Rinha contract treats a debit that would exceed the client's limit as an unprocessable transaction. The current PostgreSQL function keeps the balance unchanged and returns the current balance when the limit update fails, so the API can return `200` with an unchanged balance for this case. If strict challenge behavior is required, adjust `InsertTransacao`/the route handler before documenting over-limit debits as guaranteed `422` responses.

## `GET /clientes/{id}/extrato`

Returns the current account statement.

### Successful response

```json
{
"saldo": {
"total": 1000,
"limite": 100000,
"data_extrato": "2026-04-01T19:20:20.000000"
},
"ultimas_transacoes": [
{
"valor": 1000,
"tipo": "c",
"descricao": "deposito"
}
]
}
```

The statement returns the latest 10 transactions ordered from newest to oldest.

### Status codes

| Status | When |
|---:|---|
| `200` | Client exists and statement data was returned |
| `404` | Client ID is not one of `1` through `5` |

## `GET /healthz`

Health check used by local smoke tests and GitHub Actions.

| Status | When |
|---:|---|
| `200` | ASP.NET Core health checks pass |

## Responsibility split

| Layer | Responsibility |
|---|---|
| API (`Program.cs`) | Route mapping, JSON serialization, payload validation, fast invalid-client checks, database calls |
| PostgreSQL (`rinha.dump.sql`) | Seed clients, keep balances, apply atomic balance updates, insert transaction rows, return recent statement data |
| NGINX (`nginx.conf`) | Public port `9999` and load balancing across the two API instances |
18 changes: 16 additions & 2 deletions docs/wiki/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@ The API project targets `net9.0`; its Dockerfile currently builds and runs it wi

## Database boundary

PostgreSQL owns the race-sensitive work. The API calls stored procedures for balance reads and transaction inserts instead of doing multi-step application-side coordination.
PostgreSQL owns the race-sensitive work. The API calls stored procedures for balance reads and transaction inserts instead of doing multi-step application-side coordination. Payload-shape validation remains API-side; the stored procedure should be read as the atomic consistency boundary, not as the only validation layer.

| Procedure | Responsibility |
|-----------|----------------|
| `InsertTransacao` | Validate client, type, value, description, and credit limit, then insert atomically |
| `InsertTransacao` | Check client existence, apply the atomic balance update, enforce the database-side credit-limit guard, and insert the transaction row when the update succeeds |
| `GetSaldoClienteById` | Return balance metadata and recent transactions as JSONB |

The compose command also tunes write durability for the contest setting:
Expand Down Expand Up @@ -77,3 +77,17 @@ That shape keeps database concurrency explicit, avoids unbounded connection grow
| `TRIM` | `false` | Leaves trimming separate from AOT path |
| `EXTRA_OPTIMIZE` | `true` | Removes observability/runtime support guarded by `EXTRAOPTIMIZE` |
| `BUILD_CONFIGURATION` | `Release` | Uses optimized .NET build configuration |


## API and database contract

| Rule | Enforced by | Source |
|------|-------------|--------|
| Client ID is one of `1..5` | API fast path and database existence check | `Program.cs`, `InsertTransacao` |
| `valor` is positive | API | `IsTransacaoValid` |
| `tipo` is exactly `c` or `d` | API | `IsTransacaoValid` |
| `descricao` is present and <= 10 chars | API, with SQL parameter width as a backstop | `IsTransacaoValid`, `InsertTransacao(... descricao VARCHAR(10))` |
| Balance update and transaction insert stay atomic | PostgreSQL | `InsertTransacao` |
| Statement returns newest 10 transactions | PostgreSQL | `GetSaldoClienteById` |

The NGINX config currently uses `least_conn`. If this repository is used as a strict official challenge submission, keep the config comments and docs aligned with whatever balancing policy is allowed by the target ruleset.
19 changes: 15 additions & 4 deletions docs/wiki/challenge.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,18 @@ Rinha de Backend is a Brazilian backend challenge focused on constrained, concur

| Endpoint | Method | Purpose | Expected statuses |
|----------|--------|---------|-------------------|
| `/clientes/{id}/transacoes` | POST | Submit a debit (`d`) or credit (`c`) transaction for client IDs 1 through 5 | `200`, `404`, `422` |
| `/clientes/{id}/transacoes` | POST | Submit a debit (`d`) or credit (`c`) transaction for client IDs 1 through 5 | `200`, `404`, `422` for invalid payloads |
| `/clientes/{id}/extrato` | GET | Return balance, credit limit, statement time, and recent transactions | `200`, `404` |
| `/healthz` | GET | Local and CI health check for this implementation | `200` |

## Validation rules

- Only client IDs `1` through `5` exist.
- Transaction type must be debit (`d`) or credit (`c`).
- Transaction value must be an integer amount.
- Description must be present and at most 10 characters, matching the current API validation and database function signature.
- Debits cannot push the account beyond the configured credit limit.
- Transaction value must be a positive integer amount in cents (`valor > 0`).
- Description must be present, non-empty, and at most 10 characters.
- Transaction type must be checked by the API before the request reaches PostgreSQL.
- Debits are intended not to push the account beyond the configured credit limit.
- Statement responses return the current balance plus the latest 10 transactions.

## Resource envelope
Expand All @@ -42,3 +43,13 @@ The k6 runner and observability containers are test infrastructure. They are use
## Source

Full specification: [github.com/zanfranceschi/rinha-de-backend-2024-q1](https://github.com/zanfranceschi/rinha-de-backend-2024-q1)


## Current implementation compatibility note

Payload validation is split across the API and PostgreSQL:

- `Program.cs` rejects invalid client IDs, non-positive values, invalid transaction types, and missing/empty/long descriptions before calling the database.
- `InsertTransacao` in `docker-entrypoint-initdb.d/rinha.dump.sql` performs the atomic balance update and transaction insert.

One important edge case is worth calling out: when a debit would exceed the account limit, the current SQL function returns the existing balance without inserting a transaction. Because the route handler treats the returned balance as a successful result, this case can surface as `200 OK` with an unchanged balance rather than the strict Rinha `422` behavior. Treat this as a known implementation note until the SQL/API contract is changed.
16 changes: 15 additions & 1 deletion docs/wiki/ci-cd-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,23 @@ After a PR is rebased into `main`, the release workflow publishes the runtime im
| Docs site | `https://jonathanperis.github.io/rinha2-back-end-dotnet/` |
| Docs section | `https://jonathanperis.github.io/rinha2-back-end-dotnet/docs/` |

## Build/version matrix

| Path | SDK/runtime | Flags | Purpose |
|------|-------------|-------|---------|
| Local Docker/dev compose | .NET `10.0` SDK/runtime images from `src/WebApi/Dockerfile` | `AOT=true`, `TRIM=false`, `EXTRA_OPTIMIZE=false` | Local stack with OpenTelemetry support |
| PR Build Check | `actions/setup-dotnet` `9.0.x` | `AOT=true`, `TRIM=false`, `EXTRA_OPTIMIZE=true` | Fast compile gate plus compose health check |
| Main Release image | .NET `10.0` Docker build/runtime images | `AOT=true`, `TRIM=false`, `EXTRA_OPTIMIZE=true` | GHCR release image and multi-arch manifest |
| CodeQL | `actions/setup-dotnet` `9.0.x` | Plain Release build | Security analysis |
| Docs deploy | Bun + Astro `^6.4.2` | `NODE_ENV=production` base path | GitHub Pages static site |

The project targets `net9.0` even when Docker build images are `10.0`.

## Documentation deploy

The Pages workflow delegates to Jonathan's shared GitHub Pages workflow and uses Bun as the package manager. The docs package is an Astro static site under `docs/`; Markdown content lives in `docs/wiki/`, and production routes are served under the `/rinha2-back-end-dotnet` base path.
The Pages workflow delegates to Jonathan's shared GitHub Pages workflow and uses Bun as the package manager. The docs package is an Astro 6.4 static site under `docs/`; Markdown content lives in `docs/wiki/`, and production routes are served under the `/rinha2-back-end-dotnet` base path.

The docs site uses Astro's `markdown.processor` API with `@astrojs/markdown-satteri`. That keeps Markdown rendering explicit, but it also means future Remark/Rehype-style extensions should be tested against Sätteri before they are assumed to work.

## Operational notes

Expand Down
Loading