# Multicloud DB SDK for Java
> **⚠️ Public Preview Notice**
> This repository is currently available as a **public preview** and is **not yet fully ready for production use**.
> Expect breaking changes, incomplete features, and limited support during this phase.
A **portable database SDK** that lets you write CRUD and query logic once and run it
against **Azure Cosmos DB**, **Amazon DynamoDB**, or **Google Cloud Spanner** -
switch providers by changing a single properties file, with zero code changes.
```
┌───────────────────────────────────────────────────┐
│ Your Application │
│ (code against Multicloud DB API) │
└────────────────────────┬──────────────────────────┘
│
┌────────────▼─────────────┐
│ MulticloudDbClient │ Portable contract
│ (multiclouddb-api) │ CRUD · Query · Capabilities
└────────────┬─────────────┘
│ ServiceLoader
┌────────────┼───────────────┐
▼ ▼ ▼
┌─────────┐ ┌──────────┐ ┌───────────┐
│ Cosmos │ │ DynamoDB │ │ Spanner │
│ Provider│ │ Provider │ │ Provider │
└─────────┘ └──────────┘ └───────────┘
```
---
## Table of Contents
- [Why Multicloud DB?](#why-clouddb)
- [Quick Start](#quick-start)
- [Portable Query DSL](#portable-query-dsl)
- [Architecture](#architecture)
- [Modules](#modules)
- [API Surface](#api-surface)
- [SPI (Provider Interface)](#spi-provider-interface)
- [Provider Discovery](#provider-discovery)
- [Design Decisions](#design-decisions)
- [Why Key Is an Explicit Parameter](#why-key-is-an-explicit-parameter)
- [Supported Providers](#supported-providers)
- [Configuration](#configuration)
- [Capabilities & Portability](#capabilities--portability)
- [Result Set Control](#result-set-control)
- [Document TTL](#document-ttl)
- [Document Metadata](#document-metadata)
- [Document Size Enforcement](#document-size-enforcement)
- [Provider Diagnostics](#provider-diagnostics)
- [Sample Applications](#sample-applications)
- [Building from Source](#building-from-source)
- [Testing](#testing)
- [Project Structure](#project-structure)
- [Prerequisites](#prerequisites)
- [Documentation](#documentation)
- [License](#license)
---
## Why Multicloud DB?
| Problem | Multicloud DB Solution |
|---------|------------------|
| Vendor lock-in - each cloud DB has its own SDK, data model, and query language | Single `MulticloudDbClient` interface with portable CRUD + query |
| Each provider has a different query language (Cosmos SQL, PartiQL, GoogleSQL) | **Portable query DSL** - write `status = @status AND priority > @min`, auto-translated per provider |
| Migrating between providers requires rewriting data-access code | Change **one property** (`multiclouddb.provider=dynamo` → `cosmos`) |
| Understanding which features are portable vs. provider-specific | Runtime `CapabilitySet` introspection; `PortabilityWarning` on non-portable use |
| Testing across providers | Conformance test suite runs identical tests against every provider |
---
## Quick Start
### 1. Build
```bash
# Requires JDK 17+
mvn clean install -DskipTests
```
### 2. Add dependencies
```xml
com.microsoft.multiclouddb
multiclouddb-api
0.1.0-SNAPSHOT
com.microsoft.multiclouddb
multiclouddb-provider-cosmos
0.1.0-SNAPSHOT
runtime
```
### 3. Write portable code
```java
import com.multiclouddb.api.*;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
// Configure - provider selected entirely by config, not code
Properties props = new Properties();
props.load(getClass().getResourceAsStream("/todo-app-cosmos.properties"));
String providerName = props.getProperty("multiclouddb.provider"); // "cosmos", "dynamo", etc.
ProviderId provider = ProviderId.fromId(providerName);
MulticloudDbClientConfig config = MulticloudDbClientConfig.builder()
.provider(provider)
.connection("endpoint", props.getProperty("multiclouddb.connection.endpoint"))
.connection("key", props.getProperty("multiclouddb.connection.key"))
.build();
// Create client via ServiceLoader discovery
MulticloudDbClient client = MulticloudDbClientFactory.create(config);
// CRUD - same code for every provider
ObjectMapper mapper = new ObjectMapper();
ObjectNode doc = mapper.createObjectNode();
doc.put("title", "Buy groceries");
doc.put("completed", false);
ResourceAddress todos = new ResourceAddress("mydb", "todos");
Key key = Key.of("todo-1", "todo-1"); // partitionKey + sortKey
client.upsert(todos, key, doc); // Create or replace (upsert)
DocumentResult result = client.read(todos, key); // Point read → returns DocumentResult
ObjectNode document = result.document(); // The document payload
client.delete(todos, key); // Delete
// Query with portable expressions - automatically translated per provider
QueryRequest query = QueryRequest.builder()
.expression("status = @status AND category = @cat")
.parameters(Map.of("status", "active", "cat", "shopping"))
.pageSize(25)
.build();
QueryPage page = client.query(todos, query);
for (JsonNode item : page.items()) {
System.out.println(item);
}
// Cosmos → SELECT * FROM c WHERE (c.status = @status AND c.category = @cat)
// DynamoDB → SELECT * FROM "todos" WHERE (status = ? AND category = ?)
// Spanner → SELECT * FROM `todos` WHERE (status = @status AND category = @cat)
```
### 4. Native query escape hatch
When you need provider-specific query syntax, use `nativeExpression()`:
```java
// Cosmos SQL (only works with Cosmos provider)
QueryRequest cosmosQuery = QueryRequest.builder()
.nativeExpression("SELECT * FROM c WHERE c.title LIKE '%flight%'")
.pageSize(25)
.build();
// DynamoDB PartiQL (only works with DynamoDB provider)
QueryRequest dynamoQuery = QueryRequest.builder()
.nativeExpression("SELECT * FROM \"todos\" WHERE begins_with(title, 'Ship')")
.pageSize(25)
.build();
// Spanner GoogleSQL (only works with Spanner provider)
QueryRequest spannerQuery = QueryRequest.builder()
.nativeExpression("SELECT * FROM todos WHERE STARTS_WITH(title, 'Ship')")
.pageSize(25)
.build();
```
### 5. Switch providers
Change **only** the properties file - no code changes:
```properties
# Cosmos DB
multiclouddb.provider=cosmos
multiclouddb.connection.endpoint=https://localhost:8081
multiclouddb.connection.key=...
# --- OR ---
# DynamoDB
multiclouddb.provider=dynamo
multiclouddb.connection.endpoint=http://localhost:8000
multiclouddb.connection.region=us-east-1
multiclouddb.auth.accessKeyId=fakeMyKeyId
multiclouddb.auth.secretAccessKey=fakeSecretAccessKey
# --- OR ---
# Google Cloud Spanner
multiclouddb.provider=spanner
multiclouddb.connection.projectId=my-gcp-project
multiclouddb.connection.instanceId=my-instance
multiclouddb.connection.databaseId=my-database
# multiclouddb.connection.emulatorHost=localhost:9010 # Optional - for emulator
```
---
## Portable Query DSL
Multicloud DB includes a **portable query expression language** that lets you write WHERE-clause filters once and have them automatically translated to each provider's native query language.
### Expression Syntax
```
@ Comparison (=, !=, <, <=, >, >=)
AND Logical AND
OR Logical OR
NOT Logical NOT
BETWEEN @low AND @high Range check
IN (@a, @b, @c) Set membership
starts_with(, @param) String prefix
contains(, @param) Substring search
field_exists() Field existence check
string_length() > @n String length
collection_size() > @n Array/collection size
```
Parameters use `@name` syntax and are passed as a `Map`. Expressions support arbitrary nesting with parentheses.
### Translation Examples
| Portable Expression | Cosmos DB SQL | DynamoDB PartiQL | Spanner GoogleSQL |
|---|---|---|---|
| `status = @status` | `c.status = @status` | `status = ?` | `status = @status` |
| `starts_with(title, @prefix)` | `STARTSWITH(c.title, @prefix)` | `begins_with(title, ?)` | `STARTS_WITH(title, @prefix)` |
| `priority > @min AND category = @cat` | `(c.priority > @min AND c.category = @cat)` | `(priority > ? AND category = ?)` | `(priority > @min AND category = @cat)` |
| `field BETWEEN @lo AND @hi` | `c.field BETWEEN @lo AND @hi` | `field BETWEEN ? AND ?` | `field BETWEEN @lo AND @hi` |
| `tag IN (@a, @b, @c)` | `c.tag IN (@a, @b, @c)` | `tag IN (?, ?, ?)` | `tag IN (@a, @b, @c)` |
### Pipeline
When you call `client.query()` with a portable expression:
1. **Parse** - `ExpressionParser` converts the string to a typed AST
2. **Validate** - `ExpressionValidator` checks parameter bindings and function signatures
3. **Translate** - Provider-specific `ExpressionTranslator` generates native query syntax
4. **Execute** - Provider runs the translated query against the database
This is fully transparent - you never see the translated SQL. For direct control, use `nativeExpression()` to bypass the pipeline entirely.
---
## Architecture
### Modules
| Module | Artifact | Description |
|--------|----------|-------------|
| **multiclouddb-api** | `com.microsoft.multiclouddb:multiclouddb-api` | Portable client interface, types, error model, factory, and SPI contracts. The only compile-time dependency your app needs. |
| **multiclouddb-provider-cosmos** | `com.microsoft.multiclouddb:multiclouddb-provider-cosmos` | Azure Cosmos DB adapter (Java SDK v4) |
| **multiclouddb-provider-dynamo** | `com.microsoft.multiclouddb:multiclouddb-provider-dynamo` | Amazon DynamoDB adapter (AWS SDK v2) |
| **multiclouddb-provider-spanner** | `com.microsoft.multiclouddb:multiclouddb-provider-spanner` | Google Cloud Spanner adapter (Google Cloud Spanner 6.62.0) |
| **multiclouddb-conformance** | `com.microsoft.multiclouddb:multiclouddb-conformance` | Cross-provider integration tests |
Samples: see the [separate samples repo](https://github.com/microsoft/multiclouddb-sdk-for-java-samples).
### API Surface
All application code depends on `multiclouddb-api`. The core types are:
| Type | Purpose |
|------|---------|
| `MulticloudDbClient` | Portable interface: `create`, `read`, `update`, `delete`, `upsert`, `query`, `provisionSchema`, `capabilities`, `nativeClient` |
| `MulticloudDbClientFactory` | Creates a `MulticloudDbClient` by discovering providers via `ServiceLoader` |
| `MulticloudDbClientConfig` | Builder-pattern config: provider selection, connection, auth, feature flags |
| `ResourceAddress` | `(database, collection)` pair targeting a container/table |
| `Key` | `(partitionKey, sortKey)` pair - every document needs at least a partition key |
| `QueryRequest` | Portable expression, native expression, parameters, page size, continuation token, partition key scoping, `limit`, `orderBy` |
| `QueryPage` | Result page: items + optional continuation token + optional `OperationDiagnostics` |
| `SortOrder` | `(field, direction)` sort specification for `orderBy` — validates field names against injection |
| `SortDirection` | `ASC` or `DESC` |
| `DocumentResult` | Result of `read()`: document payload + optional `DocumentMetadata` |
| `DocumentMetadata` | Write-metadata on demand: `lastModified`, `ttlExpiry`, `version` |
| `CapabilitySet` | Runtime introspection of provider capabilities |
| `Capability` | Named capability with `supported` flag and notes |
| `MulticloudDbException` | Structured error with `MulticloudDbError` (category, provider, native code) |
| `PortabilityWarning` | Signals when an operation uses non-portable behavior |
| `OperationOptions` | Timeout, TTL (`ttlSeconds`), metadata flag (`includeMetadata`) |
| `OperationDiagnostics` | Latency, request units/charge, request ID, ETag, item count |
| `Expression` | AST node interface for parsed query expressions |
| `ExpressionParser` | Parses portable expression strings into an AST |
| `ExpressionValidator` | Validates parameter bindings and function usage |
| `ExpressionTranslator` | SPI - translates AST to provider-native query syntax |
| `TranslatedQuery` | Result of translation: query string + bound parameters |
### SPI (Provider Interface)
Provider modules implement two SPI contracts without importing each other:
| SPI Interface | Responsibility |
|---------------|---------------|
| `MulticloudDbProviderAdapter` | Factory - creates a `MulticloudDbProviderClient` from config; registered via `META-INF/services` |
| `MulticloudDbProviderClient` | CRUD + query + provisioning + capabilities - called by `DefaultMulticloudDbClient` |
### Provider Discovery
Providers are discovered at runtime via Java's `ServiceLoader`:
1. Your app calls `MulticloudDbClientFactory.create(config)`
2. The factory scans `META-INF/services/com.multiclouddb.spi.MulticloudDbProviderAdapter`
3. The matching adapter's `createClient()` builds a native SDK client
4. A `DefaultMulticloudDbClient` wraps it with error mapping, diagnostics, and the portable contract
**No provider imports in application code.** Just drop the provider JAR on the classpath (or add it as a `runtime` Maven dependency).
---
## Design Decisions
### Why Key Is an Explicit Parameter
You may notice that every CRUD operation requires an explicit `Key` parameter,
even on writes where the key material could theoretically be extracted from the
document:
```java
// Key is always explicit - never extracted from the document
client.upsert(addr, Key.of("tenant-1", "pos-42"), doc);
```
Some database SDKs (notably the Azure Cosmos DB SDK) extract the partition key
and ID from the document body automatically. Multicloud DB deliberately does **not**
do this, for several reasons:
1. **Each provider maps Key fields differently.** Cosmos DB stores `Key.sortKey()` as the built-in `id` field, while DynamoDB and Spanner store it as a `sortKey` attribute/column. A convention-based extractor would need provider-specific logic, undermining portability.
2. **`read()` and `delete()` have no document.** These operations require a Key with nothing to extract from. Making writes work differently would create an inconsistent API.
3. **The Key is always authoritative.** Providers overwrite any `id`/`partitionKey` fields in the document with the Key values (see [Document Field Injection](docs/guide.md#document-field-injection) in the developer guide). This prevents accidental mismatches.
4. **Compile-time safety.** A missing Key is a compiler error. A missing field in a JSON document is a runtime error deep in the provider layer.
See the [developer guide](docs/guide.md#why-key-is-an-explicit-parameter) for the full rationale and per-provider field mapping details.
---
## Supported Providers
| Provider | Module | Status | Native SDK |
|----------|--------|--------|------------|
| **Azure Cosmos DB** | `multiclouddb-provider-cosmos` | Full | Azure Cosmos Java SDK 4.60.0 |
| **Amazon DynamoDB** | `multiclouddb-provider-dynamo` | Full | AWS SDK for Java 2.25.16 |
| **Google Cloud Spanner** | `multiclouddb-provider-spanner` | Full | Google Cloud Spanner 6.62.0 |
---
## Configuration
All configuration flows through `MulticloudDbClientConfig` or a `.properties` file:
| Property | Description | Example |
|----------|-------------|---------|
| `multiclouddb.provider` | Provider ID | `cosmos`, `dynamo`, `spanner` |
| `multiclouddb.connection.*` | Connection properties | `endpoint`, `key`, `region`, `connectionMode` |
| `multiclouddb.auth.*` | Authentication properties | `accessKeyId`, `secretAccessKey` |
| `multiclouddb.feature.*` | Feature flags | Provider-specific opt-ins |
### Cosmos DB connection properties
| Key | Value |
|-----|-------|
| `multiclouddb.connection.endpoint` | `https://localhost:8081` (emulator) or your Cosmos account URI |
| `multiclouddb.connection.key` | Master key or Cosmos emulator well-known key |
| `multiclouddb.connection.connectionMode` | `gateway` or `direct` |
### DynamoDB connection properties
| Key | Value |
|-----|-------|
| `multiclouddb.connection.endpoint` | `http://localhost:8000` (DynamoDB Local) or omit for AWS |
| `multiclouddb.connection.region` | AWS region, e.g. `us-east-1` |
| `multiclouddb.auth.accessKeyId` | AWS access key (or any string for DynamoDB Local) |
| `multiclouddb.auth.secretAccessKey` | AWS secret key (or any string for DynamoDB Local) |
### Spanner connection properties
| Key | Value |
|-----|-------|
| `multiclouddb.connection.projectId` | GCP project ID |
| `multiclouddb.connection.instanceId` | Spanner instance ID |
| `multiclouddb.connection.databaseId` | Spanner database ID |
| `multiclouddb.connection.emulatorHost` | `localhost:9010` (Spanner Emulator) or omit for GCP |
---
## Resource Provisioning
The SDK provides a single method to provision an entire schema of databases and
containers/tables. Parallelism is handled internally - the SDK creates all
databases concurrently, waits for completion, then creates all containers
concurrently. Application code does not need to manage threading.
```java
// Define your schema: database name → list of collection/table names
Map> schema = Map.of(
"admin-db", List.of("tenants"),
"acme-risk-db", List.of("portfolios", "positions", "risk_metrics")
);
// Single call - SDK handles parallel creation internally
client.provisionSchema(schema);
```
| Provider | Database Phase | Container/Table Phase |
|----------|---------------|----------------------|
| **Cosmos DB** | Creates databases in parallel (management SDK for cloud, data-plane for emulator) | Creates containers in parallel via data-plane SDK |
| **DynamoDB** | No-op (DynamoDB has no native database concept) | Creates tables in parallel, waits for ACTIVE status |
| **Spanner** | No-op (database set at client construction time) | Creates tables in parallel |
You can also call `ensureDatabase()` and `ensureContainer()` individually if
you need fine-grained control, but `provisionSchema()` is the recommended
approach for provisioning multiple resources.
---
## Capabilities & Portability
Each provider declares which cross-cutting features it supports. Query at runtime:
```java
CapabilitySet caps = client.capabilities();
if (caps.supports(Capability.TRANSACTIONS)) {
// safe to use transactions
}
for (Capability cap : caps.all()) {
System.out.printf("%-30s %s %s%n",
cap.name(),
cap.supported() ? "✓" : "✗",
cap.notes() != null ? cap.notes() : "");
}
```
| Capability | Cosmos DB | DynamoDB | Spanner |
|------------|:---------:|:--------:|:-------:|
| **Portable query DSL** | ✓ | ✓ | ✓ |
| Native expression passthrough | ✓ (SQL) | ✓ (PartiQL) | ✓ (GoogleSQL) |
| Continuation token paging | ✓ | ✓ | ✓ |
| Cross-partition query | ✓ | ✗ | ✓ |
| Transactions | ✓ | ✓ | ✓ |
| Batch operations | ✓ | ✓ | ✓ |
| Strong consistency | ✓ | ✓ | ✓ |
| **Change feed (CDC)** | ✓ | ✓ | ✓ |
| ↳ Point-in-time start (`StartPosition.atTime`) | ✓ | ✗ | ✓ |
| ↳ Logical-partition scope (`FeedScope.logicalPartition`) | ✓ | ✗ | ✗ |
| **Result limit** (`Top N`) | ✓ | ✓ (per-page) | ✓ |
| **ORDER BY** | ✓ | ✗ | ✓ |
| **Row-level TTL** | ✓ | ✓ | ✗ |
| **Write timestamp / metadata** | ✓ | ✗ | ✗ |
---
## Result Set Control
Limit and sort results portably across providers:
```java
QueryRequest q = QueryRequest.builder()
.expression("status = @s")
.parameter("s", "active")
.limit(25) // top 25 results
.orderBy("createdAt", SortDirection.DESC) // newest first
.build();
QueryPage page = client.query(address, q);
```
Check capabilities before using `ORDER BY` — DynamoDB does not support server-side ordering:
```java
if (client.capabilities().supports(Capability.ORDER_BY)) {
// use orderBy()
}
```
---
## Document TTL
Set a per-document TTL at write time using `OperationOptions`:
```java
OperationOptions opts = OperationOptions.builder()
.ttlSeconds(3_600) // expire in 1 hour
.build();
client.create(address, key, doc, opts);
client.upsert(address, key, doc, opts);
client.update(address, key, updatedDoc, opts);
```
TTL requires collection-level configuration first (enable "Default TTL" on the
Cosmos DB container; enable TTL on the DynamoDB table using `ttlExpiry` as the
attribute name). Spanner ignores `ttlSeconds` (`ROW_LEVEL_TTL=false`).
---
## Document Metadata
Read write-metadata (last-modified timestamp, TTL expiry, version/ETag) on demand:
```java
OperationOptions opts = OperationOptions.builder()
.includeMetadata(true)
.build();
DocumentResult result = client.read(address, key, opts);
DocumentMetadata meta = result.metadata(); // null if provider doesn't support it
if (meta != null) {
System.out.println("Last modified: " + meta.lastModified());
System.out.println("Expires at : " + meta.ttlExpiry());
System.out.println("ETag/version : " + meta.version());
}
```
| Metadata field | Cosmos DB | DynamoDB | Spanner |
|----------------|:---------:|:--------:|:-------:|
| `lastModified` | ✓ (`_ts`) | ✗ | ✗ |
| `ttlExpiry` | ✗ | ✓ | ✗ |
| `version` | ✓ (ETag) | ✗ | ✗ |
---
## Document Size Enforcement
All write operations are validated against a **399 KB** limit before any network
call is made. Documents that exceed the limit are rejected with
`MulticloudDbErrorCategory.INVALID_REQUEST`:
```java
try {
client.create(address, key, largeDoc);
} catch (MulticloudDbException e) {
if (e.error().category() == MulticloudDbErrorCategory.INVALID_REQUEST) {
System.out.println("Document exceeds 399 KB limit");
}
}
```
The limit is 399 KB (not 400 KB) because providers inject additional fields
before writing — see [Developer Guide](docs/guide.md#document-size-enforcement)
for details.
---
## Provider Diagnostics
`QueryPage` carries `OperationDiagnostics` with latency, request charge, and
provider correlation IDs:
```java
QueryPage page = client.query(address, q);
OperationDiagnostics diag = page.diagnostics();
if (diag != null) {
System.out.printf("%s %s latency=%dms ruCharge=%.2f%n",
diag.provider().id(), diag.operation(),
diag.duration().toMillis(), diag.requestCharge());
}
```
---
## Sample Applications
Sample applications are maintained in a **separate repository**:
**[microsoft/multiclouddb-sdk-for-java-samples](https://github.com/microsoft/multiclouddb-sdk-for-java-samples)**
| Sample | Description | Port | Guide |
|--------|-------------|------|-------|
| **Portable CRUD + Query** | Minimal end-to-end CRUD and query sample | — | [README](https://github.com/microsoft/multiclouddb-sdk-for-java-samples#portable-crud--query-sample) |
| **TODO App** | Simple CRUD web app with browser UI | `8080` | [README-todo-app.md](https://github.com/microsoft/multiclouddb-sdk-for-java-samples/blob/main/README-todo-app.md) |
| **Risk Analysis Platform** | Multi-tenant portfolio risk analytics with executive dashboard | `8090` | [README-risk-platform.md](https://github.com/microsoft/multiclouddb-sdk-for-java-samples/blob/main/README-risk-platform.md) |
### Quick Start
```bash
git clone https://github.com/microsoft/multiclouddb-sdk-for-java-samples.git
cd multiclouddb-sdk-for-java-samples
mvn clean install -DskipTests
```
### TODO App
```
┌─────────────────────────────────┐
│ Browser UI (localhost:8080) │
│ Create · Read · Update · Delete│
└──────────────┬──────────────────┘
│ REST API
┌──────────────▼──────────────────┐
│ Embedded Java HttpServer │
│ TodoApp.java │
└──────────────┬──────────────────┘
│ MulticloudDbClient
┌─────────┼──────────┐
▼ ▼ ▼
Cosmos DB DynamoDB Spanner
Emulator Local Emulator
```
```powershell
# Cosmos DB
mvn exec:java `
"-Dexec.mainClass=com.multiclouddb.samples.todo.TodoApp" `
"-Dtodo.config=todo-app-cosmos.properties" `
"-Djavax.net.ssl.trustStore=$PWD/.tools/cacerts-local" `
"-Djavax.net.ssl.trustStorePassword=changeit"
# DynamoDB
mvn exec:java `
"-Dexec.mainClass=com.multiclouddb.samples.todo.TodoApp" `
"-Dtodo.config=todo-app-dynamo.properties"
```
Then open **http://localhost:8080** in your browser.
### Risk Analysis Platform
A multi-tenant SaaS application with database-per-tenant isolation,
portfolio risk analytics, and an executive dashboard. Demonstrates:
- **Database-per-tenant isolation** via `ResourceAddress` routing
- **Partition-scoped queries** via `QueryRequest.partitionKey()` for efficient
within-partition reads (e.g., positions within a portfolio)
- **Auto-provisioning** of databases/containers/tables on startup via `provisionSchema()`
- **Provider portability** - switch between Cosmos DB and DynamoDB with zero
code changes
```powershell
# Cosmos DB (port 8090)
mvn exec:java `
"-Dexec.mainClass=com.multiclouddb.samples.riskplatform.RiskPlatformApp" `
"-Drisk.config=risk-platform-cosmos.properties" `
"-Djavax.net.ssl.trustStore=$PWD/.tools/cacerts-local" `
"-Djavax.net.ssl.trustStorePassword=changeit"
# DynamoDB (port 8090)
mvn exec:java `
"-Dexec.mainClass=com.multiclouddb.samples.riskplatform.RiskPlatformApp" `
"-Drisk.config=risk-platform-dynamo.properties"
```
Then open **http://localhost:8090** in your browser.
For full setup instructions, see the [Risk Platform guide](https://github.com/microsoft/multiclouddb-sdk-for-java-samples/blob/main/README-risk-platform.md).
For full emulator setup instructions, see the [samples README](https://github.com/microsoft/multiclouddb-sdk-for-java-samples/blob/main/README.md).
---
## Building from Source
```bash
# Full build (compile + test + package)
mvn clean verify
# Skip tests for faster iteration
mvn clean install -DskipTests
# Build a single module
mvn -pl multiclouddb-provider-dynamo clean install
```
> **Note**: JDK 17+ is required. Set `JAVA_HOME` accordingly:
> ```powershell
> $env:JAVA_HOME = 'C:\Program Files\Eclipse Adoptium\jdk-17.0.10.7-hotspot'
> $env:PATH = "$env:JAVA_HOME\bin;$env:PATH"
> ```
---
## Testing
### Unit tests
```bash
mvn test
```
Runs 281+ tests across the API and provider modules, including the portable
query expression parser, validator, translator, partition-key-scoped queries,
and cross-provider conformance and integration tests.
### Integration / conformance tests
```bash
# Requires Cosmos DB emulator on localhost:8081, DynamoDB Local on localhost:8000,
# and Spanner Emulator on localhost:9010
mvn -pl multiclouddb-conformance verify
```
The conformance suite runs identical CRUD + portable query tests against each
provider emulator, verifying portable behavior with real data.
| Suite | Provider | Tests |
|-------|----------|-------|
| API unit tests | - | 16 |
| Cosmos Provider unit tests | Cosmos DB | 23 |
| DynamoDB Provider unit tests | DynamoDB | 33 |
| Spanner Provider unit tests | Spanner | 24 |
| Expression parser tests | - | 43 |
| Expression translation tests | - | 26 |
| Expression validator tests | - | 8 |
| Native expression tests | - | 8 |
| Portable query conformance | - | 16 |
| Cosmos CRUD conformance | Cosmos DB emulator | 13 |
| DynamoDB CRUD conformance | DynamoDB Local | 13 |
| Spanner CRUD conformance | Spanner Emulator | 13 |
| Cosmos query integration | Cosmos DB emulator | 10 |
| DynamoDB query integration | DynamoDB Local | 10 |
| Spanner query integration | Spanner Emulator | 10 |
| Cosmos US2 tests (Capabilities, Diagnostics, NativeClient, Portability) | Cosmos DB emulator | 15 |
| DynamoDB US2 tests | DynamoDB Local | 20 |
| Unsupported Capability conformance | - | 3 |
| **Total** | | **281+** |
> **Note**: The CRUD conformance suites each include 3 partition-key-scoped
> query tests (queryByPartitionKey, queryWithoutPartitionKey,
> queryNonexistentPartition) that validate the `QueryRequest.partitionKey()`
> API across all providers.
---
## Project Structure
```
multiclouddb-sdk-java/
├── pom.xml # Parent POM (aggregator)
├── multiclouddb-api/ # Portable API + SPI contracts
│ └── src/main/java/com/multiclouddb/
│ ├── api/ # Public types (MulticloudDbClient, Key, etc.)
│ │ ├── internal/ # DefaultMulticloudDbClient
│ │ └── query/ # Portable expression AST, parser, validator, translator SPI
│ └── spi/ # Provider SPI interfaces
├── multiclouddb-provider-cosmos/ # Azure Cosmos DB adapter
│ └── src/main/
│ ├── java/.../cosmos/ # CosmosProviderClient, error mapper, capabilities
│ └── resources/META-INF/services/ # ServiceLoader registration
├── multiclouddb-provider-dynamo/ # Amazon DynamoDB adapter
│ └── src/main/
│ ├── java/.../dynamo/ # DynamoProviderClient, item mapper, error mapper
│ └── resources/META-INF/services/
├── multiclouddb-provider-spanner/ # Google Cloud Spanner adapter
├── multiclouddb-conformance/ # Cross-provider integration test suite
└── specs/ # Design documents
# Sample applications are in a separate repo:
# https://github.com/microsoft/multiclouddb-sdk-for-java-samples
```
---
## Prerequisites
| Tool | Version | Required For |
|------|---------|-------------|
| JDK | 17+ | Build and run |
| Maven | 3.9+ | Build |
| Azure Cosmos DB Emulator | Latest | Cosmos integration tests |
| DynamoDB Local | Latest | DynamoDB integration tests |
| Docker | 20+ | Spanner Emulator (`gcr.io/cloud-spanner-emulator/emulator`) |
| Node.js + npm | 18+ | `dynamodb-admin` GUI (optional) |
---
## Documentation
| Document | Description |
|----------|-------------|
| [Developer Guide](docs/guide.md) | Comprehensive reference - partition keys, CRUD semantics, query DSL, multi-tenant patterns |
| [Provider Compatibility](docs/compatibility.md) | Capability matrix, error mapping, native escape hatch, async guidance |
---
## Contributing
We welcome contributions! Please read [CONTRIBUTING.md](CONTRIBUTING.md) for
guidelines on reporting issues, requesting features, setting up a development
environment, and submitting pull requests.
---
## Code of Conduct
This project has adopted the
[Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/).
See [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) for details.
---
## CLA
Contributions require a signed
[Microsoft Contributor License Agreement](https://cla.opensource.microsoft.com).
The CLA bot will guide you through the process when you open a pull request.
See [CLA.md](CLA.md) for more information.
---
## Security
Please see [SECURITY.md](SECURITY.md) for reporting security vulnerabilities.
---
## License
This project is licensed under the **MIT License** — see the [LICENSE](LICENSE)
file for details.
Copyright © Microsoft Corporation. All rights reserved.