Version Gate is a standalone service for coordinating coherent writes and reads across distributed data. It assigns ordered coordinator versions, prevents incompatible live operations from overlapping, and can also maintain immutable snapshots as an optional secondary capability.
Clients can obtain a coherent view in two ways: register a live read that temporarily blocks writers, or retrieve an already stored immutable snapshot. Snapshot providers register their generation window so Version Gate can bind the result to the correct version and prevent an incoherent snapshot from being stored.
This repository is a Maven monorepo for one standalone service product. Its
modules are internal responsibility and dependency boundaries, not
independently published Java libraries. The deployable product is the
executable version-gate-server distribution, which explicitly selects the
official adapter modules it packages.
The PostgreSQL-control and S3-snapshot modules are explicit placeholders in
this refactoring change. They establish ownership and dependency boundaries but
do not yet contain drivers, migrations, SDKs, or production storage beans.
Consequently, the executable server currently fails fast until exactly one
ControlStore and one SnapshotStore implementation are supplied.
The current code is a snapshot-first prototype and does not yet implement the accepted four-flow model or its configurable business policies. The target client behavior is recorded in Business rules and policy model. That document is informative until the SPI, HTTP API, configuration, and tests are revised in a later phase.
Any future adapter must implement the semantic contract in
Architecture and consistency. Infrastructure types
never cross the SPI, and neither version-gate-spi nor version-gate-core
depends on Spring, JDBC, an object-storage SDK, or an adapter.
Version Gate distinguishes four flows:
| Flow | Purpose |
|---|---|
| Coordinated write | Allocates the next version and prevents incompatible live operations from overlapping the write. |
| Coordinated live read | Blocks writers while a client reads live distributed data. |
| Snapshot generation | Binds an external aggregation session to the active version; policy chooses whether an arriving writer waits or invalidates it. |
| Stored snapshot retrieval | Reads immutable data without acquiring a live-data lock. |
Snapshots are optional. A resource can use only write/live-read coordination, require a current snapshot before admitting another writer, or allow snapshot gaps. Stored snapshot readers can request an explicit version, the current version, or the latest available snapshot, and can independently reject reads while a write is in progress.
The complete compatibility matrix, policy combinations, lifecycle rules, and conceptual rejection outcomes are maintained in Business rules and policy model. Future configuration documentation will be derived from that contract.
- Writes never overlap other writes or coordinated live reads.
- Policy may allow a writer to invalidate snapshot generation, but the invalidated snapshot is never stored.
- Successful write completion immediately activates its version; the target
model has no
READYbusiness state. - Stored snapshots are immutable and explicit by version.
- Stored snapshot retrieval blocks no live operation.
- All coordination depends on client participation and durable leases/fencing.
Snapshot bodies remain opaque. Version Gate verifies representation identity and completeness but does not interpret, join, restore, or business-validate client data.
flowchart LR
Clients --> Server
Server --> Core
Core --> SPI
SPI --> ControlAdapter
SPI --> SnapshotAdapter
Module responsibilities and dependency direction:
version-gate/
├── version-gate-spi domain types, stable errors, infrastructure ports
├── version-gate-core lifecycle and application use cases
├── version-gate-control-postgres official PostgreSQL ControlStore module
├── version-gate-snapshot-s3 official S3-compatible SnapshotStore module
├── version-gate-server HTTP, OpenAPI, callbacks, scheduling, bootstrap
└── version-gate-testkit reusable adapter contracts and deterministic stores
An adapter is not correct merely because it implements the Java methods. It must preserve the SPI's transactional and failure semantics:
- one atomic non-terminal build per resource;
- a storage-authoritative lease clock read after the relevant lock;
- atomic fence, lease, and lifecycle-transition checks;
- compare-and-set activation against
baseActiveVersion; - no manifest/component visibility through public lookups before activation;
- immutable, bounded-memory payload writes and reads;
- full object key, byte-length, SHA-256, content-type, and content-encoding identity checks rather than trust in provider metadata alone; and
- stable conflict, missing-object, corruption, crash, and retry behavior.
See Architecture and consistency before implementing or selecting an adapter.
version-gate-server produces an executable Spring Boot JAR and declares the
selected official adapter modules. Spring Boot auto-configuration constructs
the use cases, scheduler, HTTP API, and participant callback gateway from the
public ports. Until the placeholder adapters are implemented, a local
composition can supply test or experimental storage beans:
@Configuration(proxyBeanMethods = false)
class DistributionStorageConfiguration {
@Bean
ControlStore controlStore(/* adapter-specific dependencies */) {
return /* a contract-compliant implementation */;
}
@Bean
SnapshotStore snapshotStore(/* adapter-specific dependencies */) {
return /* a contract-compliant implementation */;
}
}The selected adapter modules own credentials, migrations, health checks, backup, retention, and deployment configuration. Starting without the required beans fails fast instead of silently using an in-memory or unsafe fallback.
Core configuration keys are illustrated in config/application-local.example.yml. They do not configure a backing store. A composed distribution may add its own adapter-specific namespace.
Coordinated callback fan-out defaults to eight participants per resource and can be configured only up to the domain hard limit of 32. V1 issues bounded, synchronous callback requests; operators should keep the participant count and request timeout conservative for their ingress timeout budget.
When a composed distribution is running, it exposes:
- liveness/readiness according to that distribution's health policy;
- OpenAPI JSON at
/v3/api-docs; and - Swagger UI at
/swagger-ui.html.
The following CLIENT_MANAGED example assumes a correctly composed distribution
is already listening at http://localhost:8080. It documents the API currently
implemented on main; it is not the endpoint or configuration contract for the
accepted four-flow target model.
set -euo pipefail
API=http://localhost:8080
RESOURCE=catalog
curl --fail-with-body --silent --show-error \
-X POST "$API/resources" \
-H 'Content-Type: application/json' \
-d '{
"resourceId": "catalog",
"snapshotPolicy": "CLIENT_MANAGED",
"requiredComponentIds": ["products", "prices"]
}' | jq .
BUILD_JSON="$(
curl --fail-with-body --silent --show-error \
-X POST "$API/resources/$RESOURCE/builds" \
-H 'Content-Type: application/json' \
-d '{
"owner": "catalog-publisher",
"leaseSeconds": 300
}'
)"
BUILD_ID="$(printf '%s' "$BUILD_JSON" | jq -r .buildId)"
VERSION="$(printf '%s' "$BUILD_JSON" | jq -r .targetVersion)"
FENCING_TOKEN="$(printf '%s' "$BUILD_JSON" | jq -r .fencingToken)"
curl --fail-with-body --silent --show-error \
-X POST "$API/builds/$BUILD_ID/snapshot" \
-H "X-Fencing-Token: $FENCING_TOKEN" | jq .
printf '%s\n' \
'{"id":"p-1","name":"Coffee"}' \
'{"id":"p-2","name":"Tea"}' >/tmp/version-gate-products.ndjson
printf '%s\n' \
'{"productId":"p-1","amount":"12.50","currency":"USD"}' \
'{"productId":"p-2","amount":"8.25","currency":"USD"}' \
>/tmp/version-gate-prices.ndjson
PRODUCTS_SHA="$(
openssl dgst -sha256 -r /tmp/version-gate-products.ndjson | awk '{print $1}'
)"
PRICES_SHA="$(
openssl dgst -sha256 -r /tmp/version-gate-prices.ndjson | awk '{print $1}'
)"
curl --fail-with-body --silent --show-error \
-X PUT "$API/builds/$BUILD_ID/components/products" \
-H "X-Fencing-Token: $FENCING_TOKEN" \
-H "X-Checksum-SHA256: $PRODUCTS_SHA" \
-H 'Content-Type: application/x-ndjson' \
-H 'X-Schema-Version: catalog-products/1' \
--data-binary @/tmp/version-gate-products.ndjson | jq .
curl --fail-with-body --silent --show-error \
-X PUT "$API/builds/$BUILD_ID/components/prices" \
-H "X-Fencing-Token: $FENCING_TOKEN" \
-H "X-Checksum-SHA256: $PRICES_SHA" \
-H 'Content-Type: application/x-ndjson' \
-H 'X-Schema-Version: catalog-prices/1' \
--data-binary @/tmp/version-gate-prices.ndjson | jq .
curl --fail-with-body --silent --show-error \
-X POST "$API/builds/$BUILD_ID/complete" \
-H "X-Fencing-Token: $FENCING_TOKEN" | jq .
curl --fail-with-body --silent --show-error \
-X POST "$API/builds/$BUILD_ID/activate" \
-H "X-Fencing-Token: $FENCING_TOKEN" | jq .
curl --fail-with-body --silent --show-error \
"$API/resources/$RESOURCE/versions/active/manifest" | jq .
curl --fail-with-body --silent --show-error \
"$API/resources/$RESOURCE/versions/$VERSION/components/products"Uploads require Content-Length; curl --data-binary @file supplies it for a
regular file. Supported media types are application/json,
application/x-ndjson, and application/octet-stream.
X-Checksum-SHA256 is optional but recommended. The hash covers the exact
transmitted bytes, including any content encoding.
Submitting an existing component with the same object key, byte length,
SHA-256, content type, and content encoding returns its stable prior result.
Changing any member of that immutable representation tuple returns
409 Conflict; bytes and representation metadata must never be overwritten.
Lease renewal uses the same fence:
curl --fail-with-body --silent --show-error \
-X POST "$API/builds/$BUILD_ID/renew" \
-H "X-Fencing-Token: $FENCING_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"leaseSeconds":300}' | jq .For COORDINATED_QUIESCE, renewal is permitted only while the build remains
BUILDING; changing the lease after quiescence would change an idempotent
callback body.
State-changing build requests require X-Fencing-Token. A token identifies an
ownership generation, not an authentication credential.
V1 uses operation-specific idempotency:
- exact-representation component retries return the prior component;
- a committed snapshot-phase transition retry returns its prior result even if its lease expired after the transition committed;
- completion and activation retries return their prior successful result; and
- after an ambiguous
beginBuildtimeout, a client queries the resource's current build before deciding whether to begin again.
Errors use application/problem+json with stable type, title, status,
detail, instance, and code fields.
| Status | Meaning |
|---|---|
400 Bad Request |
Malformed or invalid input |
404 Not Found |
Requested resource, build, active version, or component is absent |
409 Conflict |
Concurrent build, expired lease, immutable-content conflict, or invalid transition |
412 Precondition Failed |
Wrong or stale fencing token |
415 Unsupported Media Type |
Unsupported component media type |
422 Unprocessable Content |
Body checksum or resource/component requirements do not match |
502 Bad Gateway |
Coordinated participant callback failed |
503 Service Unavailable |
A configured adapter is unavailable or reports inconsistent storage |
Version Gate does not claim one transaction across the control and payload ports. The ordered protocol is:
write and verify an immutable payload
register component metadata
finalize an immutable READY manifest
atomically compare-and-set the active pointer
A correct adapter composition guarantees:
- the previous active version remains public until activation commits;
- failed or abandoned builds never move the active pointer;
- stored lifecycle state survives coordinator process restarts;
- stale fencing tokens cannot mutate a later ownership generation;
- an orphan payload may remain after metadata failure but is not public;
- missing or corrupt payloads fail closed;
- activation conflict changes neither the pointer nor candidate visibility; and
- cleanup never deletes payloads referenced by an active manifest.
V1 has no durable callback or orphan-cleanup reconciliation worker. Recovery after an ambiguous crash may require an explicit client/operator retry. Adapter documentation must describe any stronger reconciliation it provides.
The V1 production S3-compatible storage profile requires bucket versioning. Cleanup first verifies the exact immutable reference and then deletes that specific object version, so a racing or replacement version cannot be removed by mistake.
V1 intentionally has:
- one candidate build per resource and no multi-resource atomic activation;
- placeholder PostgreSQL-control and S3-snapshot modules, but no concrete production adapter implementation yet;
- no snapshot merging, business-schema validation, or restoration;
- no messaging system, event sourcing, Java client SDK, or generic lock API;
- no proof that coordinated participants stopped writes;
- no built-in end-user authentication or TLS termination; and
- no performance or capacity claim without adapter-specific benchmarks.
Requirements are JDK 21 and a POSIX-like shell. The Maven Wrapper pins Maven:
./mvnw clean verifyThe command verifies every internal module. version-gate-testkit exposes
reusable ControlStoreContract and SnapshotStoreContract suites. Its
authoritative-time fixture is an explicit test capability rather than an
assumption that production stores use the coordinator JVM clock. Each concrete
adapter must also add its own concurrency, failure-injection, migration, and
provider integration tests.
See CONTRIBUTING.md for change expectations.
Version Gate is licensed under the Apache License 2.0.