Skip to content

Application and data design

The application is a small FastAPI service chosen to make release and resilience decisions inspectable. Its design separates durable correctness, optional acceleration, and authorization ownership. PostgreSQL owns the mapping. Redis accelerates lookup. A dedicated signer owns payer keys. The application owns server payment requirements and facilitator orchestration. Each boundary gives the gate a different hypothesis to test.

This is the source-derived application design, not a proposal to add capabilities. The existing lab deployment and public records support bounded testnet observations; they do not turn the service into a production payment or high-availability storage platform.

Runtime application, PostgreSQL, Redis, signer, facilitator, and observation boundaries

Enlarge diagram: Runtime application, PostgreSQL, Redis, signer, facilitator, and observation boundaries · Version-controlled diagram source

Component Owned responsibility Explicit boundary
Settings Environment-derived URL, code length, dependency, cache, and payment settings. Payment mode requires facilitator, recipient, and asset settings together.
create_app ASGI lifespan, route orchestration, injected boundaries, instrumentation. Construction imports safely without opening dependency connections.
Database Asyncpg pool, idempotent schema setup, queries, bounded ping/reconnect. Converts handled storage faults into DatabaseUnavailable.
Cache Redis get/set/ping and expiry. Converts faults into CacheUnavailable; never becomes authoritative.
FacilitatorClient HTTP /verify then /settle, endpoint durations and call outcomes. Sends separate server-owned requirements; no payer keys or chain submission code.
SignerService Validated key slots, allowance bootstrap, local Permit2 signing. RPC belongs to startup; request-time signing never settles.
k6 client Authorization retrieval, paid creation, redirect, bounded traffic summaries. Can fail before the application sees a creation request.

Dependency injection supplies database/cache doubles, a deterministic code generator, or a payment processor for tests. That makes route, recovery, and failure contracts reproducible without credentials. It does not emulate the entire live payment network or Kubernetes scheduling.

The urls table contains code VARCHAR(16) as primary key, a unique non-null destination url, and a creation timestamp defaulting to the database clock. Optional payment fields are settlement identifier, payer, and settlement timestamp. A partial unique index covers non-null settlement identifiers, allowing unpaid rows while preventing one recorded settlement from creating another mapping.

The schema is initialized during database connection with CREATE TABLE IF NOT EXISTS, additive ALTER TABLE ... ADD COLUMN IF NOT EXISTS, and an idempotent index statement. The code does not provide a versioned migration framework. The application’s database user consequently needs schema-altering permissions for this startup path.

Destination normalization removes surrounding whitespace but does not canonicalize equivalent hostnames, query strings, fragments, or trailing slashes. Uniqueness therefore means the same validated string, rather than a semantic web-resource identity. Validation rejects non-HTTP(S), missing network location, and embedded credentials. It does not enforce public destination addresses or fetch destination content.

Redis uses url:<code> keys and configurable SETEX expiry, default 3600 seconds. There is no update or deletion API in the application, so no implemented cache invalidation protocol for mapping edits. Creation does not fill Redis; database-backed resolution does. This explains why first reads normally increment the miss counter.

POST /shorten performs the validated destination lookup first. An existing destination returns 200 without payment processing or allocating a code. A new unpaid destination proceeds to code generation and insertion. A paid destination first completes payment handling and then takes the paid insert path.

Codes use cryptographically random ASCII letters and digits, with default length eight and configured range four through sixteen. The route makes up to three attempts with ON CONFLICT DO NOTHING. After an unsuccessful insertion, it rechecks destination reuse; for a paid attempt it also checks whether the settlement identifier is already associated with a different mapping.

Successful persistence returns 201 and increments created-URL count. Destination reuse returns 200; settlement replay returns 409; allocation exhaustion or handled storage failure returns 503. Database uniqueness decides races at persistence time. The initial destination lookup does not lock out concurrent paid attempts, so the service does not guarantee that every duplicate race avoids an additional settlement.

Payment correctness has a cross-system limit

Section titled “Payment correctness has a cross-system limit”

The challenge uses x402 v2, exact atomic-unit terms, and Permit2 transfer method. The application validates header encoding and facilitator response structure while sending the configured server requirements independently of client terms. It relies on the facilitator to validate the signed authorization, accepted terms, and settlement. Cryptographic signing and signature recovery belong to the signer and its tests, not the application’s header decoder.

Verification must succeed before settlement. Settlement needs a true success flag and canonical transaction identifier before insertion. The settlement identifier is an audit key, not a success token to echo in the application response. A replay constraint protects a second application association; complementary nonce-consumption and facilitator-idempotency behavior live outside the database boundary.

There is no distributed transaction across facilitator and PostgreSQL. Settle-then-persist ordering avoids creating an unpaid mapping, while allowing a settled payment without a mapping if the insertion fails. There is no refund endpoint, durable reconciliation queue, pre-settlement journal, or implemented recovery endpoint. The paid request narrative distinguishes required operational reconciliation from capabilities actually present.

One design-document mismatch remains explicit: structurally invalid facilitator object fields produce invalid_response and currently map to 402, while the payment contract generally expects malformed upstream response failures to be 5xx. Transport, HTTP, malformed JSON, and non-object failures map to unavailable 503. This website records that discrepancy without modifying payment behavior.

Redirects trade cache speed for essential-storage work

Section titled “Redirects trade cache speed for essential-storage work”

GET /{code} returns a cached 302 without touching PostgreSQL if Redis returns a destination. Otherwise it increments the combined miss/fallback count and queries the database. A found row returns 302, whether or not the best-effort cache fill succeeds. An absent row returns 404; an unavailable required database lookup returns 503.

The design keeps availability during cache loss by shifting work to the database. Its costs are cache timeout, extra storage queries, and additional latency. The Redis workflow measures those effects within a finite lab window. A popular cached mapping can survive a PostgreSQL outage, but creation and uncached resolution cannot, and readiness remains storage-dependent.

Construction opens no sockets. The ASGI lifespan attempts database connection/schema setup, connects Redis, and creates the HTTP payment client when payment is enabled. Failed database or Redis startup connection does not immediately crash the process. Shutdown closes HTTP, cache, and database clients.

/livez is process-only. /ready pings PostgreSQL every time and Redis until first successful readiness. Database connection and ping retries are bounded; handled query failures close a poisoned pool so later readiness can reconnect. The process-local readiness flag permits established cache fallback but requires a cold process to pass Redis again.

Kubernetes startup/readiness probes call /ready; liveness calls /livez. Docker’s app health check calls /, so it does not establish the same dependency contract. The health and recovery page explains probe budgets and why process-only liveness does not preclude restart after a failed startup probe budget.

The signer validates chain/network agreement and contiguous key slots before bootstrap. It becomes ready with any successful slot, preserves partial bootstrap, and never retries bootstrap on a signing request. Health and balance/allowance data are cached; no HTTP bootstrap retry or periodic background refresh is implemented.

Application counters cover cache hits, combined misses/fallbacks, new mappings, payment outcomes, and replay rejections. Dependency gauges reflect the latest readiness check, so Redis’s gauge can retain an initial healthy value after the process stops checking it. HTTP instrumentation excludes probes and metrics.

Payment settled increments before database insertion; it is not synonymous with successful paid creation. Facilitator histograms measure individual endpoint calls, while generic HTTP and k6 end-to-end timings measure broader but different durations. The payment design mentions a total settlement-duration signal; this checkout has no separate dedicated total-settlement histogram beyond the implemented endpoint and request/client timings.

Signer metrics distinguish approval and signing outcomes. Wallet balance and allowance gauges include addresses in source; raw exposition is not suitable public evidence. Although configured wallet slots are bounded, the signing route uses submitted wallet_index as its outcome label even for out-of-range indices. The source’s comment describing all per-wallet labels as bounded is therefore broader than this implemented route guarantees. No policy or metric behavior is changed here.

Database tests, creation tests, cache tests, health/recovery tests, payment tests, replay tests, signer bootstrap tests, and Permit2 tests cover deterministic mechanisms. Dockerfiles and signer container source use locked Python dependencies and non-root numeric user 10001.

The code supplies no authentication, rate limiting, backup/restore automation, high-availability storage mechanism, or payment reconciliation service. Lab namespace boundaries share cluster resources. Retained staging failures and recovery support their exact candidates and windows, while the later release report describes its own observations. See API reference, delivery design, and gate design to connect this application boundary to the release decision.

Maintained by Satyam Agnihotri · DevOps & Cloud Engineer