Design · 001-position-core

Position keeping core

How we will build it — and the Gradle build every later feature compiles into.

Status Accepted Spec 001-position-core Owner Position Keeping team Branch build-out Created Read time

A JDK-only hexagonal core — Position and TradeEvent records, a PositionRepository port, a PositionService use case that decides idempotency against the repository's applied-tradeId record — plus the Gradle 9 build (Groovy DSL, Java 21 toolchain, JUnit 5, ArchUnit, Spotless, Checkstyle) that this and the six later features compile into. The single biggest risk is the build itself: it is the first real one in this repository, and spectastic enforce only recognises a Gradle build written in the Groovy DSL.

1 · Principles check

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

PrincipleComplianceNotes
P-1 · Clarity over clevernessOKRecords, one interface, one use case; no framework, no reflection.
P-2 · Small, reversible changesOKThe core is one package; the build bootstrap is four files.
P-3 · Explicit over implicitOKIdempotency is a named check in the use case, not a side effect of a map put.
P-4 · One set of conventions, enforcedOKSpotless (google-java-format) + Checkstyle wired in the same bootstrap; see Toolchain.
P-5 · Record the whyOKEvery stack pick is a decision below with its driver named.
P-6 · Tests accompany behaviorOKTDD — SC-001 and SC-002 each have a test written first.
P-7 · Contract-first interfacesOKThis feature exposes no interface (§3); the ports are typed Java signatures.
P-8 · Library-first boundariesOKThe core has no entry point at all; Main arrives with 004.
P-9 · Secure by defaultOKNo credentials, no I/O in this slice.
P-10 · Least privilegeOKNothing granted yet; the DB grant is 002/003's concern.
P-11 · Semantic versioningOKversion = '0.1.0' in the build; no release is cut in this slice.
P-12 · Supply-chain hygieneOKThree test-scope dependencies, each pinned to an exact version; zero runtime dependencies.

Governing decisions acknowledged

This plan's surface includes src/main/java/xyz/briancorbin/pk/hex/persistence/, governed by 002-downstream-consumers/D-007 (ADR-0007 — only the persistence adapter touches the data store; every position change emits PositionChanged). The plan honours it: the only persistence-package additions are the in-memory adapter and the moved JDBC adapter, both inside the permitted package, and the use case reaches the store through the port alone. spectastic adrs --for src/main/java/xyz/briancorbin/pk/hex/persistence/InMemoryPositionRepository.java returns D-007; this section is its acknowledgment.

2 · Technical context

Language(s)
Java 21 (Gradle toolchain; the build runs on any JDK 17+ host JVM).
Frameworks
None in the domain (NFR-001). Build: Gradle 9.2.1 wrapper, Groovy DSL — D-001.
Architecture
Hexagonal ports-and-adapters — D-002; shapes Project structure.
Storage
In-memory only (InMemoryPositionRepository); durable storage is deferred to 003.
Testing
JUnit 5.11.4 + ArchUnit 1.4.1, test-first — D-003.
Coverage
Not in scope — the standard profile's floor has no coverage category (spectastic enforce requires formatter · linter · test-runner).
Perf targets
Not in scope — no NFR quantifies latency; end-of-day volumes.
Constraints
The domain core imports the JDK only (NFR-001, checked by ArchUnit). The build file is build.gradle, never .kts, because spectastic enforce detects Java tooling from build.gradle alone.

Toolchain / enforcement

spectastic enforce on the standard profile reports covered: contract-first · missing: formatter, linter, test-runner. The three gaps resolve on the first rung of the escalation ladder — the ecosystem's standard gates, all in build.gradle so detection sees them: Spotless 8.10.2 running google-java-format 1.36.1 (formatter), Checkstyle 14.1.0 with a minimal config/checkstyle/checkstyle.xml (linter), and Gradle's test task on the JUnit Platform (test-runner). The CI gate for all three is the spectastic-managed workflow spectastic init --tools --ci github writes (validate → enforce → merge verdict on every pull request); a second, project-owned workflow runs ./gradlew build. spectastic gitignore --stack ran before the build existed and detected no stack; it re-runs in the setup phase once build.gradle is present, and .gradle/ is added by hand outside the managed block because the Java entry set (build/ target/ *.class) does not include it.

3 · Data model & contracts

The spec's two entities land as Java records in the core package: Position is (String instrument, long quantity); Trade event is (String tradeId, String instrument, long quantity). Quantity is a signed long — whole units, no fractional lots in this timeline. The applied-tradeId record is the repository's, not the entity's: the port answers wasApplied(tradeId), and write records the tradeId together with the new quantity so the two can never disagree.

Conceptual entityPhysical shapeNotes
Positionrecord Position(String instrument, long quantity)Immutable; the repository returns a fresh value per read.
Trade eventrecord TradeEvent(String tradeId, String instrument, long quantity)tradeId is the idempotency key (FR-002).
Repository portinterface PositionRepository { Optional<Position> find(String); boolean wasApplied(String); void write(String instrument, long quantity, String tradeId); }write keeps the signature the existing adapter already implements.

No interface is exposed: the core is a library reached through its ports (FR-003). The HTTP surface is 004's and the event stream 002's; each declares its own contract there.

5 · Grounding & evidence

Every design-bearing fact this plan rests on, and how it was confirmed (per REQ-LIFECYCLE-006). Two facts needed a spike; it was run on this host before the interview and its claim was written down first: “Gradle 9.2.1 with toolchain 21 finds the asdf-installed Temurin 21 on a JDK 25 host, and a JUnit 5.11.4 test that imports ArchUnit 1.4.1 and opens an H2 2.3.232 connection runs green on 21.”

Claim the design rests onSourceStatusFinding / note
spectastic enforce detects Java tooling from build.gradle only — never build.gradle.kts; the test-runner signal is the literal test @spectastic/cli@0.1.0-pre.28 · packages/core/src/enforce/detect.ts:118-170 verified Every Java signal names file: 'build.gradle'; no .kts entry. Forces the Groovy DSL (D-001).
The Gradle toolchain resolver finds JDK 21 via asdf on this JDK 25 host Spike — scratch project, gradle javaToolchains verified Location: ~/.asdf/installs/java/temurin-21.0.9+10.0.LTS · Detected by: asdf-vm; gradle test exit 0 with java.version starting 21. No JAVA_HOME needed.
JUnit 5.11.4, ArchUnit 1.4.1 and H2 2.3.232 resolve from Maven Central and load on 21 Spike — same scratch project; org.junit:junit-bom:5.11.4, com.tngtech.archunit:archunit:1.4.1, com.h2database:h2:2.3.232 verified ClassFileImporter().importPackages(..) and DriverManager.getConnection("jdbc:h2:mem:x") both ran inside a green test.
com.sun.net.httpserver.HttpServer is available to a JDK 21 compile without a dependency Spike — jdk.httpserver module; a class referencing HttpServer.class compiled verified Grounds 004/006's zero-dependency HTTP choice now, so the stack is settled once.
The existing adapter implements PositionRepository.write(String, long, String) and emits after the write src/xyz/briancorbin/pk/hex/persistence/JdbcPositionRepositoryAdapter.java:16-29 verified The port keeps that signature so the file moves to the Gradle layout unchanged in shape; find/wasApplied are additive.
Spotless 8.10.2, google-java-format 1.36.1 and Checkstyle 14.1.0 are the current releases plugins.gradle.org … com.diffplug.spotless/maven-metadata.xml; repo1.maven.org … checkstyle/maven-metadata.xml verified <latest> read from each metadata file this turn.
002-downstream-consumers/D-007 scopes src/**/persistence/**, and the plan-constraint reduces that glob to the static prefix src specs/002-downstream-consumers/design.html#D-007; packages/core/src/guardrail/plan-constraint.ts:22-28,62-70 verified Any Draft design declaring a src/… path overlaps the decision; this design acknowledges it (§1).

6 · Approach

Bootstrap the build first — settings.gradle, build.gradle, the wrapper, Spotless and Checkstyle — because nothing else in this repository has ever compiled and every later feature needs the same four files. Then the core: xyz.briancorbin.pk.hex.core holds the two records and the PositionRepository port; xyz.briancorbin.pk.hex.app.PositionService is the one use case, book(TradeEvent): ask the repository whether the tradeId was applied, return the current position unchanged if so, otherwise add the quantity and write the new quantity and the tradeId together. xyz.briancorbin.pk.hex.persistence.InMemoryPositionRepository is the first adapter — a map and a set. The existing JDBC adapter moves into the Gradle layout as-is; making it compile against H2 is 003's work.

Idempotency could have lived in the adapter (“ignore a write whose tradeId you have seen”). It lives in the use case instead so that FR-002 holds for every adapter, including the JDBC one that 003 writes and the reconciliation path 007 adds.
TradeEvent record · tradeId, instrument, qty PositionService book() · idempotent on tradeId PositionRepository port · find / wasApplied / write InMemory adapter map + applied set (003: JDBC)
The slice 001 builds. The domain never sees the adapter; the adapter implements the port. Dependencies point inward.

7 · Alternatives considered

The build tool was the only pick with real contenders; the criteria were whether spectastic enforce can see the gates, whether the toolchain can provision JDK 21 without host configuration, and how much build code a reader must hold.

OptionSeen by enforceJDK 21 without JAVA_HOMEBuild code to readTotal
Gradle 9, Groovy DSL — chosen3339
Gradle 9, Kotlin DSL0 — .kts is never read336
Maven 3.91 — only the test-runner (pom.xml)1 — no toolchain provisioning; the sibling example exports JAVA_HOME24

8 · Decisions

D-001 · Gradle 9 with the Groovy DSL and a Java 21 toolchain

Status
Accepted
Context
The project has never had a build. spectastic enforce reads Java signals from build.gradle only (detect.ts:118-170), and the spike showed Gradle's toolchain resolver finds the asdf Temurin 21 on the JDK 25 host with no configuration.
Decision drivers
enforce detection · zero-config JDK 21 · least build code
Considered options
  • Gradle 9, Groovy DSL — chosen — + every gate visible to enforce; toolchain provisions 21. − Groovy is the less type-safe DSL.
  • Gradle 9, Kotlin DSL — + type-safe. − enforce would report the floor as uncovered forever.
  • Maven — + ubiquitous. − no toolchain provisioning; formatter/linter invisible to enforce.
Decision
settings.gradle + build.gradle (Groovy), Gradle 9.2.1 wrapper committed, java.toolchain.languageVersion = 21.
Consequences
+ One build for all seven features and, later, the consumer subproject. + ./gradlew works on any JDK 17+ host. − Groovy DSL; − the wrapper jar is a committed binary.

D-002 · Hexagonal ports-and-adapters

Status
Accepted
Context
FR-003 requires the domain be reachable only through ports, and the existing adapter already implements PositionRepository.write (JdbcPositionRepositoryAdapter.java:16). 002's D-007 depends on this shape: the adapter package is the boundary its rule names.
Decision drivers
FR-003 · the boundary D-007 enforces · simplest structure that isolates the store
Considered options
  • Hexagonal ports-and-adapters — chosen — + the store is behind one port; the adapter package is a checkable boundary. − One more interface than a layered service needs.
  • Layered (service → repository class) — + familiar. − the repository is a class, not a port; nothing stops a second writer.
  • Vertical slice — + feature-local. − seven features would each own a slice of the same store.
Decision
Packages hex.core (records + ports, JDK-only), hex.app (use cases), hex.persistence (adapters). Dependencies point inward; ArchUnit asserts it.
Consequences
+ Swapping in-memory for JDBC (003) touches only hex.persistence. + D-007's scope is a real package. − Ceremony for a small domain.

D-003 · JUnit 5 + ArchUnit, test-first

Status
Accepted
Context
SC-002 is observed by an ArchUnit rule, and D-007's fitness function is already written against ArchUnit (enforcement/DataAccessRulesTest.java). The spike resolved junit-bom:5.11.4 and archunit:1.4.1 and ran both.
Decision drivers
SC-002's stated observation · reuse of the existing rule · the standard profile's test-runner floor
Considered options
  • JUnit 5 + ArchUnit, TDD — chosen — + the JUnit Platform is what Gradle's test runs; ArchUnit is the only mainstream Java architecture checker. − ArchUnit has no SARIF output of its own.
  • JUnit 4 — − ArchUnit's JUnit 4 support is legacy; no reason to prefer it.
  • Integration-first — − there is nothing to integrate against until 003/004.
Decision
Tests under src/test/java, written before the code they exercise; ArchUnit rules are plain @Test methods that evaluate a rule (so later a SARIF writer can sit beside the assertion).
Consequences
+ SC-001/SC-002 each have a test that fails before its implementation task. − Plain @Test forgoes ArchUnit's @ArchTest field syntax.

D-004 · Enforcement floor: Spotless + Checkstyle + Gradle test, gated by the managed CI workflow

Status
Accepted
Context
spectastic enforce lists formatter, linter and test-runner missing (this turn). All three are first-rung ecosystem gates; versions from the plugin-portal and Maven Central metadata read this turn.
Decision
Spotless 8.10.2 (google-java-format 1.36.1) · Checkstyle 14.1.0 with a minimal config · test { useJUnitPlatform() }. CI: spectastic init --tools --ci github installs the managed .github/workflows/spectastic.yml (validate → enforce → verdict); a project-owned build.yml runs ./gradlew build.
Consequences
+ enforce reports the floor covered; ./gradlew build fails on an unformatted or lint-failing file. + The CI gate is a file spectastic owns and drift-checks, pinned to the installing CLI. − Two workflows rather than one, because the managed file is not to be edited.

D-005 · Idempotency is decided in the use case against the repository's applied-tradeId record

Status
Accepted
Context
FR-002 must hold for every adapter. The existing adapter's write(instrument, quantity, tradeId) already receives the tradeId (JdbcPositionRepositoryAdapter.java:20), so recording it beside the quantity costs the port nothing.
Decision
PositionService.book checks wasApplied(tradeId) before computing; write stores quantity and tradeId together (one map put + one set add here; one transaction in 003).
Consequences
+ One idempotency rule, adapter-independent. + 007's reconciliation tradeIds reuse it. − The port grows by two read methods.

9 · Project structure

Only the new or changed paths, grouped by the boundaries D-002 draws.

settings.gradle                                                   # rootProject.name = 'position-keeper'
build.gradle                                                      # java · spotless · checkstyle · junit platform (Groovy DSL, D-001)
gradlew, gradlew.bat, gradle/wrapper/                             # Gradle 9.2.1 wrapper
config/checkstyle/checkstyle.xml                                  # minimal linter config (D-004)
.github/workflows/spectastic.yml                                  # managed CI gate, written by `spectastic init --tools --ci github`
.github/workflows/build.yml                                       # project-owned: ./gradlew build
src/main/java/xyz/briancorbin/pk/hex/core/Position.java           # record
src/main/java/xyz/briancorbin/pk/hex/core/TradeEvent.java         # record
src/main/java/xyz/briancorbin/pk/hex/core/PositionRepository.java # port
src/main/java/xyz/briancorbin/pk/hex/app/PositionService.java     # the use case: book()
src/main/java/xyz/briancorbin/pk/hex/persistence/InMemoryPositionRepository.java  # first adapter
src/main/java/xyz/briancorbin/pk/hex/persistence/JdbcPositionRepositoryAdapter.java  # moved from src/xyz/…; compiles against H2 in 003
src/test/java/xyz/briancorbin/pk/hex/app/PositionServiceTest.java # SC-001
src/test/java/xyz/briancorbin/pk/enforcement/CoreRulesTest.java   # SC-002: core imports JDK + core only; core never imports java.sql

10 · Risks & mitigations

RiskLikelihoodImpactMitigation
The moved JDBC adapter does not compile until 003 supplies H2 and the event portHMMove it under src/main/java in 001 but keep it compiling against an EventBus stub only if 002 is not yet present — otherwise defer the move to 002/003. Decided in tasks: the move lands with 003, when both dependencies exist.
google-java-format reformats the existing Java files, changing bytes the demonstrations quoteMHdocs/priya-pr/ and demonstrations/8-consumer-service/ are outside Spotless's target, which names only the Gradle source sets src/main/java and src/test/java.
Toolchain provisioning differs on CI (no asdf)LLsetup-java` temurin 21 in build.yml; Gradle's foojay resolver is deliberately not added.

11 · Complexity tracking

The only complexity beyond the spec is the build bootstrap itself — Spotless, Checkstyle, wrapper — which the enforcement floor requires and every later feature reuses.

The build is bootstrapped inside 001's setup phase rather than as a separate slice, so the stack picks are recorded here as decisions instead of appearing from nowhere. Later designs cite D-001/D-003/D-004 rather than re-deciding them.

12 · Open questions

None outstanding — the stack was anchored by the spike and the enforce output, the architecture by FR-003 and the existing adapter, and the contract shape (none) by the spec's out-of-scope list.

13 · Change log

  1. Status flipped Draft → Accepted on 18 Sep 2026 — zero remaining unchecked tasks (13/13); ./gradlew clean build green with PositionServiceTest (SC-001) and CoreRulesTest (SC-002) verified passing per author confirmation. Sibling bundle (REQ-LIFECYCLE-005).
  2. Initial plan — Gradle 9 Groovy DSL + Java 21 toolchain, hexagonal core, JUnit 5 + ArchUnit, Spotless + Checkstyle floor with the managed CI gate; seven facts grounded (six verified against source, one spike run and recorded).