Skip to content

Commit 557cf8e

Browse files
Merge pull request #5 from MarketDataApp/05_implement_single_package_arq
refactor: implement single-package architecture (ADR-007 Option B)
2 parents b3eba44 + c815c85 commit 557cf8e

13 files changed

Lines changed: 36 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11-
- Project scaffold per ADRs 001–006: Gradle Kotlin DSL build, JDK 17 toolchain,
11+
- Project scaffold per ADRs 001–007: Gradle Kotlin DSL build, JDK 17 toolchain,
1212
`integrationTest` source set, Spotless + JaCoCo, Vanniktech Maven Publish.
1313
- `MarketDataClient` skeleton with two public constructors — a no-arg one
1414
for production (everything resolved from the cascade) and a 4-arg one
@@ -27,6 +27,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2727
- `RateLimits` record exposed via `MarketDataClient.getRateLimits()`.
2828
- JSpecify `@NullMarked` on every public package; JSpecify on `compileOnlyApi`
2929
so consumers get the annotations at compile time without a runtime dep.
30-
- Token redaction utility (`internal.Tokens`) for log output.
30+
- Token redaction utility (`Tokens`, package-private in the SDK root) for
31+
log output.
3132
- MIT license; SDK version auto-detected from the JAR manifest
3233
(`Implementation-Version`).
34+
- Single-package architecture per ADR-007: every infra class
35+
(`Configuration`, `EnvVars`, `Tokens`, `Version`) lives in
36+
`com.marketdata.sdk` as package-private. The `internal/` subpackage
37+
was removed; the consumer's compiler cannot reference these types,
38+
closing the "internal type leaks via constructor signature" gap that
39+
every non-modular Java SDK has.

CLAUDE.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ The repo follows a strict **ADR-first** workflow:
2020

2121
When asked to make architectural changes, prefer updating an existing ADR or proposing a new one (status `Proposed`) over silently editing requirements.
2222

23-
## Locked-in tech stack (ADRs 001–006, all Accepted)
23+
## Locked-in tech stack (ADRs 001–007, all Accepted)
2424

2525
These decisions are not up for debate without amending the corresponding ADR:
2626

@@ -31,6 +31,7 @@ These decisions are not up for debate without amending the corresponding ADR:
3131
- **`java.net.http.HttpClient` exclusively.** No third-party HTTP client (OkHttp, Apache) as a runtime dep — ever. HTTP/2 on (default). One shared `HttpClient` per `MarketDataClient`. Timeouts: 99s request, 2s connect. (ADR-004)
3232
- **Jackson (`jackson-databind`) for JSON.** Records-based response models (Jackson record support, 2.12+). The API's parallel-arrays wire format (e.g. `{"s":"ok","symbol":["AAPL","MSFT"],"price":[150.0,400.0]}`) is decoded via custom `JsonDeserializer` classes, *not* default reflection. Jackson is **not shaded** in v1; shading is held in reserve. (ADR-005)
3333
- **Sync + async parity per endpoint.** Every public endpoint exposes both `quote(...)` and `quoteAsync(...)`; async returns `CompletableFuture<T>`. **Internal logic is async-first.** Sync methods are thin wrappers that call `.join()` and unwrap `CompletionException` to surface the underlying cause directly. Both surfaces share validation, retry, rate-limit, and concurrency-pool logic — no parallel implementations. Tests must cover both variants for every endpoint. (ADR-006)
34+
- **Single-package internals.** Every infra and resource-façade class lives in `com.marketdata.sdk` (the root). The "internal" boundary is enforced by Java's package-private visibility — types not meant for consumers (`Configuration`, `EnvVars`, `Tokens`, `Version`, and the future `HttpTransport`, `RequestSpec`, `AsyncSemaphore`, etc.) drop the `public` modifier so the consumer's compiler simply cannot reference them. Resource façades (`MarketsResource`, etc.) stay `public final class` but with package-private constructors. Response DTOs and exceptions stay in their public subpackages (`com.marketdata.sdk.markets`, `com.marketdata.sdk.exception`); response records do not carry `@JsonDeserialize` annotations — wire-format deserializers register programmatically via a package-private Jackson `SimpleModule` on `HttpTransport`'s `ObjectMapper`. (ADR-007)
3435

3536
## Kotlin-interop rules for the public API
3637

@@ -61,8 +62,8 @@ The Java SDK must also satisfy the canonical, cross-language [SDK Requirements](
6162

6263
**Already wired in:**
6364
- §1.1 client object — `MarketDataClient` with two public constructors: a no-arg one for production (everything from the cascade) and a 4-arg `(apiKey, baseUrl, apiVersion, validateOnStartup)` for tests and short-lived runtimes. All fields `final` (immutable). Default base URL `https://api.marketdata.app`, default API version `v1`, single shared `HttpClient`, `User-Agent: marketdata-sdk-java/{version}` (version auto-detected from JAR manifest), `close()` for resource release, `getRateLimits()` accessor.
64-
- §4 configuration cascade — `Configuration.resolve(...)` does explicit → `MARKETDATA_*` env var → `.env` in CWD → default. Env var names live in `internal.EnvVars`. The 4-arg constructor's parameters feed step 1; the no-arg constructor skips it and starts at step 2.
65-
- §5 demo mode + `validateOnStartup` parameter on the 4-arg constructor (defaults to `true` via the no-arg constructor); token redaction via `internal.Tokens.redact` (matches the spec example `***…***YKT0`).
65+
- §4 configuration cascade — `Configuration.resolve(...)` does explicit → `MARKETDATA_*` env var → `.env` in CWD → default. Env var names live in `EnvVars` (package-private, in the SDK root package). The 4-arg constructor's parameters feed step 1; the no-arg constructor skips it and starts at step 2.
66+
- §5 demo mode + `validateOnStartup` parameter on the 4-arg constructor (defaults to `true` via the no-arg constructor); token redaction via `Tokens.redact` (matches the spec example `***…***YKT0`).
6667
- §6 sealed `MarketDataException` hierarchy with the 7 canonical subtypes and full support context (`requestId`, `requestUrl`, `statusCode`, `timestamp`, `exceptionType`) + `getSupportInfo()`.
6768
- §10 timeouts: `REQUEST_TIMEOUT = 99s` and `CONNECT_TIMEOUT = 2s` exposed as constants on `MarketDataClient`. Connect timeout is wired into the `HttpClient`; the per-request 99 s timeout is a constant ready to be applied to `HttpRequest.Builder#timeout` when the request layer lands.
6869
- §12 concurrency: `Semaphore(50)` field on `MarketDataClient` (wiring of acquire/release lands with the request layer).

README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,10 @@ MARKETDATA_RUN_INTEGRATION_TESTS=true ./gradlew integrationTest
125125
## Package layout
126126

127127
```
128-
com.marketdata.sdk # MarketDataClient, RateLimits (public surface)
128+
com.marketdata.sdk # MarketDataClient + RateLimits (public);
129+
# Configuration, EnvVars, Tokens, Version
130+
# are package-private and not part of the API
129131
com.marketdata.sdk.exception # Sealed MarketDataException hierarchy + ErrorContext
130-
com.marketdata.sdk.internal # Tokens, EnvVars, Configuration, Version (do not depend on)
131132
```
132133

133134
Every public package is `@NullMarked` (JSpecify): non-null is the default;

src/main/java/com/marketdata/sdk/internal/Configuration.java renamed to src/main/java/com/marketdata/sdk/Configuration.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.marketdata.sdk.internal;
1+
package com.marketdata.sdk;
22

33
import java.io.IOException;
44
import java.nio.file.Files;
@@ -12,13 +12,13 @@
1212
* Resolves SDK configuration values per the cascade in SDK requirements §4: {@code explicit value →
1313
* MARKETDATA_* env var → .env file in CWD → built-in default}.
1414
*
15-
* <p>The only public construction path is {@link #loadFromProcess()}, which snapshots the live
15+
* <p>The single canonical construction path is {@link #loadFromProcess()}, which snapshots the live
1616
* environment and the {@code .env} file once. The constructor is strictly private — there is no
1717
* production-callable backdoor for injecting arbitrary maps. Tests reach the private constructor
1818
* via reflection (see {@code ConfigurationTest}); this is by design so a developer can't
1919
* accidentally take a shortcut around the canonical load path.
2020
*/
21-
public final class Configuration {
21+
final class Configuration {
2222

2323
public static final String DEFAULT_BASE_URL = "https://api.marketdata.app";
2424
public static final String DEFAULT_API_VERSION = "v1";

src/main/java/com/marketdata/sdk/internal/EnvVars.java renamed to src/main/java/com/marketdata/sdk/EnvVars.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
package com.marketdata.sdk.internal;
1+
package com.marketdata.sdk;
22

33
/**
44
* Names of the {@code MARKETDATA_*} environment variables consulted by the SDK. Mirrors SDK
55
* requirements §4.
66
*/
7-
public final class EnvVars {
7+
final class EnvVars {
88

99
public static final String TOKEN = "MARKETDATA_TOKEN";
1010
public static final String BASE_URL = "MARKETDATA_BASE_URL";

src/main/java/com/marketdata/sdk/MarketDataClient.java

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,5 @@
11
package com.marketdata.sdk;
22

3-
import com.marketdata.sdk.internal.Configuration;
4-
import com.marketdata.sdk.internal.EnvVars;
5-
import com.marketdata.sdk.internal.Tokens;
6-
import com.marketdata.sdk.internal.Version;
73
import java.net.http.HttpClient;
84
import java.time.Duration;
95
import java.util.concurrent.Semaphore;

src/main/java/com/marketdata/sdk/internal/Tokens.java renamed to src/main/java/com/marketdata/sdk/Tokens.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
package com.marketdata.sdk.internal;
1+
package com.marketdata.sdk;
22

33
import org.jspecify.annotations.Nullable;
44

55
/**
66
* Token redaction helpers. SDK requirements §5 / §16: API tokens must never appear in log output
77
* verbatim.
88
*/
9-
public final class Tokens {
9+
final class Tokens {
1010

1111
/**
1212
* Minimum number of asterisks emitted before the trailing 4 chars, matching the SDK requirements

src/main/java/com/marketdata/sdk/internal/Version.java renamed to src/main/java/com/marketdata/sdk/Version.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package com.marketdata.sdk.internal;
1+
package com.marketdata.sdk;
22

33
/**
44
* Reads the SDK's version from the JAR manifest's {@code Implementation-Version} attribute (SDK
@@ -7,7 +7,7 @@
77
* <p>Falls back to {@code "0.0.0-dev"} when the class is not loaded from a JAR (e.g. running tests
88
* from class files).
99
*/
10-
public final class Version {
10+
final class Version {
1111

1212
private static final String FALLBACK = "0.0.0-dev";
1313

src/main/java/com/marketdata/sdk/internal/package-info.java

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/main/java/com/marketdata/sdk/package-info.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
/**
2-
* Market Data Java SDK — public API entry point.
2+
* Market Data Java SDK — public API root.
33
*
4-
* <p>The entire public API is {@code @NullMarked}: every type, parameter, return, and field is
4+
* <p>This package hosts both the public API surface ({@link com.marketdata.sdk.MarketDataClient},
5+
* {@link com.marketdata.sdk.RateLimits}, and the resource façades) and every package-private
6+
* internal class (configuration cascade, env-var keys, token redaction, version detection, and the
7+
* HTTP/wire-format infrastructure). Per ADR-007, the "internal" boundary is enforced by Java's
8+
* package-private visibility: types not meant for consumers omit the {@code public} modifier so the
9+
* consumer's compiler simply cannot reference them.
10+
*
11+
* <p>{@code @NullMarked} applies at the package level — every type, parameter, return, and field is
512
* non-null by default. Mark nullable items explicitly with {@link
613
* org.jspecify.annotations.Nullable}.
714
*/

0 commit comments

Comments
 (0)