Design · 004-query-endpoint

Position query endpoint

How we will build it — the first entry point, and a feature that never mentions the decision.

Status Accepted Spec 004-query-endpoint Owner Position Keeping team Branch build-out Created Read time

A GET /positions/{instrument} served by the JDK's own com.sun.net.httpserver — no web framework — reading through PositionService.current and so through the persistence adapter. The contract is an OpenAPI sidecar authored first (P-7). Main arrives here: it opens the H2 file, wires the bus, the adapter, the two consumers and the HTTP server. Biggest risk: a read path that accidentally writes — the test asserts zero events and an unchanged store across every request.

1 · Principles check

Verified against v0.1.0. No exception, no violation.

PrincipleComplianceNotes
P-1 · Clarity over clevernessOKOne handler, one JSON writer, no framework or reflection.
P-2 · Small, reversible changesOKA new package and a Main; nothing existing changes.
P-3 · Explicit over implicitOKRoutes are registered by hand; the port and DB path are named environment variables with defaults.
P-4 · One set of conventions, enforcedOK001's floor.
P-5 · Record the whyOKThree decisions below.
P-6 · Tests accompany behaviorOKPositionsEndpointTest drives a real server on an ephemeral port (SC-001).
P-7 · Contract-first interfacesOKOpenAPI 3.1 sidecar at specs/004-query-endpoint/contracts/positions.openapi.yaml, declared in §3, before the handler exists.
P-8 · Library-first boundariesOKMain is wiring only; HttpApi is a thin adapter over the use case.
P-9 · Secure by defaultOKBinds localhost by default; no credentials in code. Authentication is deferred to the gateway by the spec.
P-10 · Least privilegeOKThe handler holds a PositionService only — it cannot reach a connection.
P-11 · Semantic versioningOKThe contract carries info.version: 0.1.0, in step with the build.
P-12 · Supply-chain hygieneOKZero new dependencies — the HTTP server and JSON writing are JDK + 40 lines.

Governing decisions acknowledged

This plan adds nothing under src/main/java/xyz/briancorbin/pk/hex/persistence/, yet the plan-constraint names 002-downstream-consumers/D-007 (ADR-0007 — only the persistence adapter touches the data store), because its scope src/**/persistence/** reduces to the static prefix src and every path in §9 lives under it. Acknowledged on the merits anyway: the endpoint reads through the port and the existing adapter, holds no connection, routes only GET, and its test asserts no PositionChanged is emitted by any request — it adds no writer. The over-eager trigger is recorded as a tooling gap (meta-repo inbox), not worked around by narrowing §9.

2 · Technical context

Language(s)
Java 21 on the Gradle build (001/D-001).
Frameworks
None — com.sun.net.httpserver.HttpServer from the jdk.httpserver module (D-001); Gradle's application plugin for ./gradlew run.
Architecture
Hexagonal (001/D-002): hex.http is an inbound adapter over the use case.
Storage
None new; reads reach the H2 store through the existing adapter (003) — spec NFR-001.
Testing
JUnit 5 (001/D-003); the endpoint test uses java.net.http.HttpClient against a server on port 0.
Coverage
Not in scope (standard profile).
Perf targets
Not in scope — the spec names none; a single-threaded JDK server is adequate for a worked example.
Constraints
Read-only: only GET is routed on /positions/; anything else is 405 (spec FR-002).

Toolchain / enforcement

No new category. build.gradle gains the application plugin and mainClass = 'xyz.briancorbin.pk.Main'; spectastic enforce is unchanged. The H2 file Main opens by default lives under data/, which *.mv.db already ignores.

3 · Data model & contracts

The spec's Position view is the wire shape {"instrument": "AAPL", "quantity": 100} — 001's Position record, serialised. Nothing is stored; the projection is computed per request from PositionService.current.

Conceptual entityPhysical shapeNotes
Position viewapplication/json object {instrument: string, quantity: int64}404 {"error": "unknown instrument"} when nothing was ever booked; 405 for any method but GET.

One resource, one verb. A consumer may rely on: a known instrument returns 200 with the current signed quantity as a JSON integer; an unknown one returns 404 with an error string, never an empty body; and no request on this surface — whatever the method — changes a position. The contract is authored at the proposed location (specs/004-query-endpoint/contracts/) while the design is in flight and promoted to api/ when the bundle lands.

openapi: 3.1.0 info: title: Position keeper — positions query version: 0.1.0 description: >- Read-only view of current positions (004-query-endpoint). Reads go through the position use case and its persistence adapter; no request on this surface mutates a position. paths: /positions/{instrument}: get: operationId: getPosition summary: Current quantity for an instrument parameters: - name: instrument in: path required: true schema: { type: string, minLength: 1, maxLength: 32 } responses: "200": description: The instrument is known content: application/json: schema: { $ref: "#/components/schemas/Position" } example: { instrument: AAPL, quantity: 100 } "404": description: No position has ever been booked for the instrument content: application/json: schema: { $ref: "#/components/schemas/Error" } example: { error: unknown instrument } "405": description: Any method but GET — the surface is read-only components: schemas: Position: type: object required: [instrument, quantity] properties: instrument: { type: string } quantity: { type: integer, format: int64, description: Signed whole units } Error: type: object required: [error] properties: error: { type: string }

5 · Grounding & evidence

Facts this plan rests on. No spike was needed: the one runtime question — is the JDK's HTTP server available to a Java 21 compile with no dependency — was settled by 001's spike.

Claim the design rests onSourceStatusFinding / note
com.sun.net.httpserver.HttpServer compiles on the Java 21 toolchain with no dependency 001 §5 — spike row 4 (jdk.httpserver) verified Grounded once in 001 so the stack was settled before this feature.
The read path is PositionService.current(instrument) → PositionRepository.find → the adapter's SELECT src/main/java/xyz/briancorbin/pk/hex/app/PositionService.java — current; JdbcPositionRepositoryAdapter.java — find verified Already exists; the handler adds no data path (spec NFR-001). current returns a flat position for an unknown instrument, so the handler distinguishes 404 by repository.find being empty — exposed as PositionService.lookup returning Optional.
Every write emits exactly one PositionChanged (002), so “no request mutates a position” is observable as zero events src/main/java/xyz/briancorbin/pk/hex/persistence/InMemoryPositionRepository.java — write verified The endpoint test subscribes a recorder and asserts it stays empty across every request — the observable, not the property.
A request-response <spec-contract> must carry path=; only shape="none" may omit it @spectastic/schema · rules/contract-declaration-shape.ts:80-84 verified Rules out declaring the HTTP surface without a contract; the OpenAPI sidecar follows (D-002).
OpenAPI 3.1.0 is the format the interview seeds for request/response .claude/commands/spectastic.design.md — “Contract format … request/response → OpenAPI” verified Nothing in the project argues for another format; no crown was needed.
HttpServer.create(new InetSocketAddress("localhost", 0), 0) binds an ephemeral port readable from getAddress().getPort() JDK 21 API — com.sun.net.httpserver.HttpServer#create, #getAddress verified Lets the test run with no fixed port; confirmed when PositionsEndpointTest runs.

6 · Approach

hex.http.HttpApi wraps an HttpServer: start(), stop(), port(), and a context per handler. PositionsHandler takes the instrument from the path, refuses anything but GET with 405, asks PositionService.lookup, and writes 200 {instrument, quantity} or 404 {error} through a ten-line Json helper. Main reads PK_PORT (default 8080) and PK_DB (default data/pk), opens H2Database, builds a SyncEventBus, the JDBC adapter, the RiskExposureView and PnlRevaluer, the PositionService, and starts the API. It imports no java.sql.

The temptation is a framework — Javalin, Helidon — for the routing and JSON. Two routes and one object do not earn a dependency tree, and the example's supply-chain principle (P-12) is easier to keep than to restore.
HttpApi GET /positions/{i} · 405 else PositionService lookup(i) → Optional Jdbc adapter find → SELECT (no write) Main wires H2 · bus · consumers · API
The read path. The handler never sees a repository, let alone a connection.

7 · Alternatives considered

The HTTP stack was the only pick with contenders. Criteria: dependencies added, code a reader must hold, and whether the example's zero-framework domain stays believable.

OptionDependenciesCode to readFits the exampleTotal
JDK com.sun.net.httpserver — chosen3 — none2 — ~60 lines of handler + JSON38
Javalin 61 — Jetty tree315
Spring Boot020 — the domain would stop being JDK-only in spirit2

8 · Decisions

D-001 · The JDK's HTTP server, no web framework

Status
Accepted
Context
Two routes (this and 006's health) and one JSON object. 001's spike compiled HttpServer on the toolchain with no dependency; P-12 asks that adding one be a reviewed decision — this is the review.
Decision drivers
zero dependencies · code volume · the JDK-only story
Considered options
  • JDK com.sun.net.httpserver — chosen — + nothing to add; ephemeral ports for tests. − Hand-rolled routing and JSON.
  • Javalin — + pleasant API. − Jetty and Jackson for two routes.
  • Spring Boot — + conventional. − a framework the rest of the example deliberately lacks.
Decision
HttpServer behind hex.http.HttpApi; a Json helper for the two object shapes; Gradle application plugin for ./gradlew run.
Consequences
+ ./gradlew run starts with one jar on the classpath (H2). − Anything beyond a handful of routes would want a framework; that is a later decision if it comes.

D-002 · Contract first: an OpenAPI 3.1 sidecar, promoted to api/ when the bundle lands

Status
Accepted
Context
P-7 requires a checked-in contract before the code behind an interface, and the validator will not accept a request/response declaration without a path (contract-declaration-shape.ts:80). The interview seeds OpenAPI for request/response.
Decision
specs/004-query-endpoint/contracts/positions.openapi.yaml now; declared effective path api/positions.openapi.yaml, coordinate name positions; moved to the effective path on the bundle's last tick. The handler is written to it, not the other way round.
Consequences
+ A consumer (the reconciliation service, 010) has a target before the code exists. − Two copies exist for a moment at promotion; the move is a single git mv.

D-003 · Read-only by construction: GET-only routing over PositionService.lookup

Status
Accepted
Context
Spec FR-002 forbids mutation; NFR-001 wants the existing adapter. PositionService.current exists; it flattens an unknown instrument to zero, so the handler needs an Optional-returning lookup to answer 404.
Decision
Add Optional<Position> lookup(String) to the use case (delegating to find); the handler holds only the service; non-GET → 405 before any lookup.
Consequences
+ The handler cannot write: it has nothing to write with. + The test observes “no write” as zero events and an unchanged store. − One more method on the use case.

9 · Project structure

Only the new or changed paths.

build.gradle                                                          # + application plugin, mainClass
specs/004-query-endpoint/contracts/positions.openapi.yaml             # the contract, proposed location (→ api/positions.openapi.yaml)
src/main/java/xyz/briancorbin/pk/Main.java                            # wiring: env PK_PORT, PK_DB; H2 · bus · adapter · consumers · API
src/main/java/xyz/briancorbin/pk/hex/http/HttpApi.java                # HttpServer lifecycle; start/stop/port
src/main/java/xyz/briancorbin/pk/hex/http/PositionsHandler.java       # GET /positions/{instrument} → 200 | 404; else 405
src/main/java/xyz/briancorbin/pk/hex/http/Json.java                   # the two object shapes, escaped
src/main/java/xyz/briancorbin/pk/hex/app/PositionService.java         # + lookup(String): Optional<Position>
src/test/java/xyz/briancorbin/pk/hex/http/PositionsEndpointTest.java  # SC-001: 200 / 404 / 405; zero events, store unchanged

10 · Risks & mitigations

RiskLikelihoodImpactMitigation
The handler and the contract drift apartMMThe test's expected bodies are copied from the contract's examples; a schema-validating test is deferred with the contract tooling.
A path like /positions/ or /positions/a/b hits an unexpected branchMLAnything that is not exactly one non-empty segment is 404.
Port 8080 is taken on a developer machineMLPK_PORT; the test never uses a fixed port.

11 · Complexity tracking

Hand-written routing and JSON are the only complexity beyond the spec, and they replace a dependency tree; justified in D-001.

The store's contents are read as the adapter's role (pk_adapter), because NFR-001 asks for the existing adapter and 003's adapter owns that connection. The reader role is for probes that bypass the port (006).

12 · Open questions

None outstanding — the stack, the contract format and the read path were anchored against 001's spike, the validator's own rule and the existing use case.

13 · Change log

  1. Status flipped Draft → Accepted on 18 Sep 2026 — zero remaining unchecked tasks (9/9); ./gradlew build green with PositionsEndpointTest (SC-001) verified passing and the process exercised with curl per author pre-confirmation for 003–007; contract promoted to api/positions.openapi.yaml. Sibling bundle (REQ-LIFECYCLE-005).
  2. Initial plan — JDK HTTP server, OpenAPI 3.1 sidecar authored first, GET-only routing over a new lookup; six facts verified against source, no spike needed.