Skip to content

Commit 326e19d

Browse files
Merge pull request #4 from MarketDataApp/04_change_marketdataclient_ctr
Replace `MarketDataClient` builder with explicit constructors
2 parents 2bd1bf5 + 51c740f commit 326e19d

9 files changed

Lines changed: 125 additions & 130 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Added
1111
- Project scaffold per ADRs 001–006: Gradle Kotlin DSL build, JDK 17 toolchain,
1212
`integrationTest` source set, Spotless + JaCoCo, Vanniktech Maven Publish.
13-
- `MarketDataClient` skeleton with builder, default base URL
14-
(`https://api.marketdata.app`), default API version (`v1`), 99 s request /
15-
2 s connect timeouts, HTTP/2, demo mode, `validateOnStartup` toggle, and a
16-
50-permit concurrency semaphore (wiring lands with the request layer).
17-
- Configuration cascade: explicit builder values → `MARKETDATA_*` environment
18-
variables → `.env` file in CWD → built-in defaults.
13+
- `MarketDataClient` skeleton with two public constructors — a no-arg one
14+
for production (everything resolved from the cascade) and a 4-arg one
15+
(`apiKey`, `baseUrl`, `apiVersion`, `validateOnStartup`) for tests and
16+
short-lived runtimes. Default base URL (`https://api.marketdata.app`),
17+
default API version (`v1`), 99 s request / 2 s connect timeouts, HTTP/2,
18+
demo mode, `validateOnStartup` toggle, and a 50-permit concurrency
19+
semaphore (wiring lands with the request layer).
20+
- Configuration cascade: explicit constructor parameters → `MARKETDATA_*`
21+
environment variables → `.env` file in CWD → built-in defaults.
1922
- Sealed `MarketDataException` hierarchy with the seven canonical subtypes
2023
(`AuthenticationError`, `BadRequestError`, `NotFoundError`, `RateLimitError`,
2124
`ServerError`, `NetworkError`, `ParseError`), each carrying support context

CLAUDE.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,9 +60,9 @@ If you find yourself reaching for Lombok, AutoValue, or an abstract-base excepti
6060
The Java SDK must also satisfy the canonical, cross-language [SDK Requirements](https://www.marketdata.app/docs/sdk/sdk-requirements/) (referenced from inside the ADRs as `../sdk-requirements.md`). The current scaffold applies the **foundational** rules from that doc; per-endpoint and per-request rules land alongside the request layer. Specifically:
6161

6262
**Already wired in:**
63-
- §1.1 client object — `MarketDataClient` builder, 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`.
65-
- §5 demo mode + `validateOnStartup` toggle on the builder; token redaction via `internal.Tokens.redact` (matches the spec example `***…***YKT0`).
63+
- §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`).
6666
- §6 sealed `MarketDataException` hierarchy with the 7 canonical subtypes and full support context (`requestId`, `requestUrl`, `statusCode`, `timestamp`, `exceptionType`) + `getSupportInfo()`.
6767
- §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.
6868
- §12 concurrency: `Semaphore(50)` field on `MarketDataClient` (wiring of acquire/release lands with the request layer).

README.md

Lines changed: 16 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,13 @@
33
Java SDK for the [Market Data API](https://www.marketdata.app/). **Pre-release
44
scaffold** — endpoints are not yet implemented; this iteration sets up the
55
build, package layout, configuration cascade, exception taxonomy, and
6-
Kotlin-interop foundations from the [ADRs](docs/adr/) and the canonical
7-
[SDK Requirements](https://www.marketdata.app/docs/sdk/sdk-requirements/).
6+
Kotlin-interop foundations.
87

98
## Requirements
109

11-
- **JDK 17 or newer** (ADR-002). The published artifact is compiled with
10+
- **JDK 17 or newer**. The published artifact is compiled with
1211
`javac --release 17`. Tests run on JDK 17, 21, and 25.
13-
- **Jackson 2.18+** on the runtime classpath (ADR-005). Pulled transitively;
12+
- **Jackson 2.18+** on the runtime classpath. Pulled transitively;
1413
consumers may align to a newer 2.x.
1514

1615
## Install (planned)
@@ -27,30 +26,31 @@ Coordinates are placeholders until the first publication to Maven Central.
2726
## Quick start
2827

2928
The SDK reads `MARKETDATA_TOKEN` from the environment by default, so the
30-
common path is two lines (per SDK requirements §"Easy Default Requests"):
29+
common path is two lines:
3130

3231
### Java
3332

3433
```java
35-
try (var client = MarketDataClient.builder().build()) {
34+
try (var client = new MarketDataClient()) {
3635
// endpoint methods land in subsequent iterations
3736
}
3837
```
3938

4039
### Kotlin
4140

4241
```kotlin
43-
MarketDataClient.builder().build().use { client ->
42+
MarketDataClient().use { client ->
4443
// endpoint methods land in subsequent iterations
4544
}
4645
```
4746

4847
## Configuration
4948

50-
Values are resolved through this cascade (highest priority first), per
51-
SDK requirements §4:
49+
Values are resolved through this cascade (highest priority first):
5250

53-
1. Explicit builder methods — `apiKey(...)`, `baseUrl(...)`, `apiVersion(...)`
51+
1. Explicit constructor parameters — `apiKey`, `baseUrl`, `apiVersion`
52+
(passed to `new MarketDataClient(apiKey, baseUrl, apiVersion, validateOnStartup)`;
53+
the no-arg `new MarketDataClient()` skips this step and starts at #2)
5454
2. Environment variables (table below)
5555
3. `.env` file in the current working directory
5656
4. Built-in defaults
@@ -97,10 +97,10 @@ try {
9797
}
9898
```
9999

100-
The seven permitted subtypes `AuthenticationError`, `BadRequestError`,
101-
`NotFoundError`, `RateLimitError`, `ServerError`, `NetworkError`,
102-
`ParseError` — match SDK requirements §6.1. The hierarchy is sealed so
103-
`switch` over the subtypes is compile-time exhaustive (ADR-002).
100+
The seven permitted subtypes are `AuthenticationError`, `BadRequestError`,
101+
`NotFoundError`, `RateLimitError`, `ServerError`, `NetworkError`, and
102+
`ParseError`. The hierarchy is sealed so `switch` over the subtypes is
103+
compile-time exhaustive.
104104

105105
## Build
106106

@@ -118,7 +118,7 @@ install — the wrapper downloads the right Gradle version on first run.
118118
./gradlew spotlessApply # auto-format
119119
./gradlew jacocoTestReport # coverage report → build/reports/jacoco/
120120

121-
# Integration tests hit the live API — gated by env var (ADR-003 §13).
121+
# Integration tests hit the live API — gated by env var.
122122
MARKETDATA_RUN_INTEGRATION_TESTS=true ./gradlew integrationTest
123123
```
124124

@@ -132,25 +132,7 @@ com.marketdata.sdk.internal # Tokens, EnvVars, Configuration, Version (do not
132132

133133
Every public package is `@NullMarked` (JSpecify): non-null is the default;
134134
nullable items are tagged explicitly. This is what makes Kotlin's null
135-
safety work against this Java API (ADR-001 §2.1).
136-
137-
## Architectural decisions
138-
139-
All foundational decisions are captured as ADRs and are **Accepted**:
140-
141-
| ADR | Decision |
142-
|-----|----------|
143-
| [001](docs/adr/ADR-001-java-only-vs-multi-language-sdk.md) | Java only; Kotlin consumers via interop, not a Kotlin artifact |
144-
| [002](docs/adr/ADR-002-minimum-jdk-version.md) | Minimum JDK 17; CI matrix `{17, 21, 25}` |
145-
| [003](docs/adr/ADR-003-build-tool.md) | Gradle (Kotlin DSL) + version catalog |
146-
| [004](docs/adr/ADR-004-http-client.md) | `java.net.http.HttpClient` exclusively |
147-
| [005](docs/adr/ADR-005-json-library.md) | Jackson (`jackson-databind`) |
148-
| [006](docs/adr/ADR-006-async-api-surface.md) | Sync + async parity, async-first internally |
149-
150-
Java-specific requirements derived from the ADRs live in
151-
[`docs/java-sdk-requirements.md`](docs/java-sdk-requirements.md). The
152-
canonical, cross-language requirements are at
153-
[marketdata.app/docs/sdk/sdk-requirements](https://www.marketdata.app/docs/sdk/sdk-requirements/).
135+
safety work against this Java API.
154136

155137
## License
156138

docs/java-sdk-requirements.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,12 @@ The README and per-method docs must include at least one Kotlin usage
118118
example alongside the Java example for the quick-start path:
119119

120120
```kotlin
121-
val client = MarketDataClient.builder()
122-
.apiKey("KEY")
123-
.build()
121+
val client = MarketDataClient(
122+
apiKey = "KEY",
123+
baseUrl = null,
124+
apiVersion = null,
125+
validateOnStartup = true,
126+
)
124127

125128
val quote = client.stocks().quote("AAPL")
126129
println(quote)

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

Lines changed: 56 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,22 @@
1616
* Entry point to the Market Data Java SDK.
1717
*
1818
* <p>One {@code MarketDataClient} per application. Holds a single shared {@link HttpClient}
19-
* (HTTP/2, 2 s connect timeout) for connection pooling (ADR-004) and a 50-permit semaphore that
20-
* gates the global concurrency pool required by SDK requirements §12.
19+
* (HTTP/2, 2 s connect timeout) for connection pooling and a 50-permit semaphore that gates the
20+
* global concurrency pool required by SDK requirements §12.
2121
*
22-
* <p>Construction follows the configuration cascade in §4: explicit builder values → {@code
23-
* MARKETDATA_*} environment variables → values in a {@code .env} file in the working directory →
24-
* built-in defaults. Pass no token to enter <em>demo mode</em> (authenticated endpoints will fail;
25-
* the {@code Authorization} header is omitted).
22+
* <p>Two constructors:
23+
*
24+
* <ul>
25+
* <li>{@link #MarketDataClient()} — production path. Resolves everything from the cascade in §4
26+
* ({@code MARKETDATA_*} environment variable → value in a {@code .env} file → built-in
27+
* default). With no token in the cascade, enters <em>demo mode</em> — authenticated endpoints
28+
* will fail and the {@code Authorization} header is omitted.
29+
* <li>{@link #MarketDataClient(String, String, String, boolean)} — explicit-control path for
30+
* tests and short-lived runtimes. Each parameter may still be {@code null} to defer to the
31+
* cascade for that single value.
32+
* </ul>
33+
*
34+
* <p>Instances are immutable: every field is {@code final} and assigned in the constructor.
2635
*/
2736
public final class MarketDataClient implements AutoCloseable {
2837

@@ -48,18 +57,46 @@ public final class MarketDataClient implements AutoCloseable {
4857
private final boolean demoMode;
4958
private final boolean validateOnStartup;
5059

51-
private MarketDataClient(Builder builder) {
60+
/**
61+
* Production constructor. Resolves all settings from the configuration cascade in SDK
62+
* requirements §4 (env var → {@code .env} → built-in default) and enables startup validation.
63+
*
64+
* <p>Equivalent to {@link #MarketDataClient(String, String, String, boolean) new
65+
* MarketDataClient(null, null, null, true)}.
66+
*/
67+
public MarketDataClient() {
68+
this(null, null, null, true);
69+
}
70+
71+
/**
72+
* Explicit-control constructor for tests and short-lived runtimes. Each of {@code apiKey}, {@code
73+
* baseUrl}, and {@code apiVersion} may be {@code null} to defer to the cascade in §4 for that
74+
* single value.
75+
*
76+
* @param apiKey explicit API token, or {@code null} to resolve from {@code MARKETDATA_TOKEN} →
77+
* {@code .env} → demo mode
78+
* @param baseUrl override the API base URL, or {@code null} to resolve to {@link
79+
* Configuration#DEFAULT_BASE_URL}
80+
* @param apiVersion override the API version segment, or {@code null} to resolve to {@link
81+
* Configuration#DEFAULT_API_VERSION}
82+
* @param validateOnStartup whether to validate the token on construction by calling {@code
83+
* /user/} (SDK requirements §5). Pass {@code false} for short-lived runtimes where the
84+
* startup hit is undesirable.
85+
*/
86+
public MarketDataClient(
87+
@Nullable String apiKey,
88+
@Nullable String baseUrl,
89+
@Nullable String apiVersion,
90+
boolean validateOnStartup) {
5291
Configuration config = Configuration.loadFromProcess();
53-
this.token = config.resolve(builder.apiKey, EnvVars.TOKEN);
92+
this.token = config.resolve(apiKey, EnvVars.TOKEN);
5493
this.baseUrl =
5594
trimTrailingSlash(
56-
config.resolveOrDefault(
57-
builder.baseUrl, EnvVars.BASE_URL, Configuration.DEFAULT_BASE_URL));
95+
config.resolveOrDefault(baseUrl, EnvVars.BASE_URL, Configuration.DEFAULT_BASE_URL));
5896
this.apiVersion =
59-
config.resolveOrDefault(
60-
builder.apiVersion, EnvVars.API_VERSION, Configuration.DEFAULT_API_VERSION);
97+
config.resolveOrDefault(apiVersion, EnvVars.API_VERSION, Configuration.DEFAULT_API_VERSION);
6198
this.demoMode = this.token == null;
62-
this.validateOnStartup = builder.validateOnStartup;
99+
this.validateOnStartup = validateOnStartup;
63100
this.userAgent = "marketdata-sdk-java/" + Version.current();
64101

65102
this.httpClient =
@@ -73,23 +110,19 @@ private MarketDataClient(Builder builder) {
73110
LOG.log(
74111
Level.INFO,
75112
"Initialized Market Data SDK {0} (baseUrl={1}, apiVersion={2}, demoMode={3})",
76-
new Object[] {Version.current(), baseUrl, apiVersion, demoMode});
77-
if (demoMode) {
113+
new Object[] {Version.current(), this.baseUrl, this.apiVersion, this.demoMode});
114+
if (this.demoMode) {
78115
LOG.warning(
79116
"No API token provided — running in demo mode. Authenticated endpoints will"
80117
+ " fail; rate-limit initialization is skipped.");
81118
} else if (LOG.isLoggable(Level.FINE)) {
82-
LOG.log(Level.FINE, "Token: {0}", Tokens.redact(token));
119+
LOG.log(Level.FINE, "Token: {0}", Tokens.redact(this.token));
83120
}
84121

85122
// SDK requirements §5: validate on startup by default. The actual
86123
// /user/ call lands with the request layer; this flag is the seam.
87124
}
88125

89-
public static Builder builder() {
90-
return new Builder();
91-
}
92-
93126
public String getBaseUrl() {
94127
return baseUrl;
95128
}
@@ -118,53 +151,12 @@ public boolean isValidateOnStartup() {
118151
@Override
119152
public void close() {
120153
// java.net.http.HttpClient gained explicit close() in JDK 21.
121-
// While the minimum target is JDK 17 (ADR-002), this method is a
122-
// no-op: the JVM releases the executor and connection pool on
123-
// process exit. Revisit if/when the minimum bumps to 21+.
154+
// While the minimum target is JDK 17, this method is a no-op:
155+
// the JVM releases the executor and connection pool on process
156+
// exit. Revisit if/when the minimum bumps to 21+.
124157
}
125158

126159
private static String trimTrailingSlash(String url) {
127160
return url.endsWith("/") ? url.substring(0, url.length() - 1) : url;
128161
}
129-
130-
public static final class Builder {
131-
private @Nullable String apiKey;
132-
private @Nullable String baseUrl;
133-
private @Nullable String apiVersion;
134-
private boolean validateOnStartup = true;
135-
136-
private Builder() {}
137-
138-
/** Override the API token; otherwise resolved from {@code MARKETDATA_TOKEN} or {@code .env}. */
139-
public Builder apiKey(String apiKey) {
140-
this.apiKey = apiKey;
141-
return this;
142-
}
143-
144-
/** Override the base URL (default {@value Configuration#DEFAULT_BASE_URL}). */
145-
public Builder baseUrl(String baseUrl) {
146-
this.baseUrl = baseUrl;
147-
return this;
148-
}
149-
150-
/** Override the API version (default {@value Configuration#DEFAULT_API_VERSION}). */
151-
public Builder apiVersion(String apiVersion) {
152-
this.apiVersion = apiVersion;
153-
return this;
154-
}
155-
156-
/**
157-
* Whether to validate the token at construction by calling {@code /user/} (SDK requirements
158-
* §5). Defaults to {@code true}. Disable for short-lived runtimes where the startup hit is
159-
* undesirable.
160-
*/
161-
public Builder validateOnStartup(boolean validateOnStartup) {
162-
this.validateOnStartup = validateOnStartup;
163-
return this;
164-
}
165-
166-
public MarketDataClient build() {
167-
return new MarketDataClient(this);
168-
}
169-
}
170162
}

src/main/java/com/marketdata/sdk/exception/MarketDataException.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
/**
99
* Root of the SDK exception hierarchy.
1010
*
11-
* <p>Sealed (ADR-002) so consumer {@code switch} statements over its subtypes are compile-time
12-
* exhaustive. Every instance carries the support context fields required by SDK requirements §6.2
13-
* and exposes a {@link #getSupportInfo()} string per §6.3.
11+
* <p>Sealed so consumer {@code switch} statements over its subtypes are compile-time exhaustive.
12+
* Every instance carries the support context fields required by SDK requirements §6.2 and exposes a
13+
* {@link #getSupportInfo()} string per §6.3.
1414
*
1515
* <p>Subtypes use {@link ErrorContext#empty()} for client-side validation errors that occur before
1616
* any HTTP request is dispatched.

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
/**
22
* Sealed exception hierarchy thrown by the SDK.
33
*
4-
* <p>The {@link com.marketdata.sdk.exception.MarketDataException} root is sealed (ADR-002) so
5-
* consumer {@code switch} statements over the known subtypes are compiler-checked for
6-
* exhaustiveness. Adding a new subtype is a breaking change.
4+
* <p>The {@link com.marketdata.sdk.exception.MarketDataException} root is sealed so consumer {@code
5+
* switch} statements over the known subtypes are compiler-checked for exhaustiveness. Adding a new
6+
* subtype is a breaking change.
77
*/
88
@NullMarked
99
package com.marketdata.sdk.exception;

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
/**
22
* Market Data Java SDK — public API entry point.
33
*
4-
* <p>Per ADR-001 §2.1, the entire public API is {@code @NullMarked}: every type, parameter, return,
5-
* and field is non-null by default. Mark nullable items explicitly with {@link
4+
* <p>The entire public API is {@code @NullMarked}: every type, parameter, return, and field is
5+
* non-null by default. Mark nullable items explicitly with {@link
66
* org.jspecify.annotations.Nullable}.
77
*/
88
@NullMarked

0 commit comments

Comments
 (0)