Portable API Surface¶
The Multicloud DB SDK's portable API surface covers capabilities that work
identically across all three providers. The features listed below require no
runtime capability checks - they are guaranteed to work on Azure Cosmos DB,
Amazon DynamoDB, and Google Cloud Spanner. Some providers offer additional
capabilities (e.g., CROSS_PARTITION_QUERY, ORDER_BY, LIKE); use
client.capabilities() to discover what the current provider supports.
What Works Everywhere¶
Every capability listed below is fully supported on all providers. There are no asterisks, no provider-specific caveats, and no runtime checks required.
CRUD Operations¶
| Operation | Description |
|---|---|
| Create | Insert a new document (fails if the key already exists) |
| Read | Point-read by partition key + sort key |
| Update | Replace an existing document (fails if not found) |
| Upsert | Create or replace - always succeeds |
| Delete | Remove by key (idempotent) |
Query - Portable Expression DSL¶
Write a WHERE-clause filter once. The SDK translates it to the native query language of whichever provider is configured - Cosmos SQL, DynamoDB PartiQL, or Spanner GoogleSQL.
| Feature | Operators / Functions | Example |
|---|---|---|
| Comparison | =, !=, <, >, <=, >= |
status = 'active' |
| Logical | AND, OR, NOT |
age > 18 AND active = true |
| String functions | STARTS_WITH, CONTAINS |
STARTS_WITH(name, 'A') |
| Field introspection | FIELD_EXISTS |
FIELD_EXISTS(metadata) |
| Length functions | STRING_LENGTH, COLLECTION_SIZE |
STRING_LENGTH(name) > 3 |
| Named parameters | @paramName |
price > @minPrice |
QueryRequest query = QueryRequest.builder()
.expression("STARTS_WITH(name, @prefix) AND age >= @minAge")
.parameter("prefix", "J")
.parameter("minAge", 21)
.maxPageSize(50)
.build();
QueryPage page = client.query(address, query);
Pagination¶
| Feature | Description |
|---|---|
| Cursor-based paging | Continuation-token pagination across all providers |
| Page size control | maxPageSize to limit results per page |
Data Management¶
| Feature | Description |
|---|---|
| Schema provisioning | provisionSchema() creates databases, containers, and tables portably |
| Transactions | Multi-document transactional operations |
| Batch operations | Batch read/write for throughput efficiency |
| Strong consistency | Strongly-consistent reads |
| Change feed | Change feed / change streams |
Diagnostics & Error Handling¶
| Feature | Description |
|---|---|
| Structured diagnostics | Latency, request charge, and provider correlation IDs per operation |
| Portable error categories | All provider exceptions mapped to MulticloudDbErrorCategory |
| Capability introspection | client.capabilities() reports what the current provider supports |
Portable Error Mapping¶
All provider exceptions are mapped to portable MulticloudDbErrorCategory values.
The raw HTTP or gRPC status code is also available via error.statusCode().
| Category | Cosmos DB | DynamoDB | Spanner |
|---|---|---|---|
INVALID_REQUEST |
HTTP 400 | ValidationException, HTTP 400 | INVALID_ARGUMENT, FAILED_PRECONDITION |
AUTHENTICATION_FAILED |
HTTP 401 | UnrecognizedClientException, HTTP 401/403 | UNAUTHENTICATED |
AUTHORIZATION_FAILED |
HTTP 403 | AccessDeniedException | PERMISSION_DENIED |
NOT_FOUND |
HTTP 404 | ResourceNotFoundException, HTTP 404 | NOT_FOUND |
CONFLICT (409 - duplicate key) |
HTTP 409 | ConditionalCheckFailedException from create() - attribute_not_exists guard fails when the item already exists |
ALREADY_EXISTS |
CONFLICT (412 - precondition) |
HTTP 412 | ConditionalCheckFailedException from update()/upsert() with a condition expression¹ |
ABORTED |
THROTTLED |
HTTP 429 | ProvisionedThroughputExceededException, ThrottlingException | RESOURCE_EXHAUSTED |
TRANSIENT_FAILURE |
HTTP 449, 500, 502, 503 | HTTP 500–5xx | UNAVAILABLE |
PERMANENT_FAILURE |
- | ItemCollectionSizeLimitExceededException | - |
UNSUPPORTED_CAPABILITY |
- | - | UNIMPLEMENTED |
PROVIDER_ERROR |
Other | Other | INTERNAL, Other |
¹ DynamoDB uses
ConditionalCheckFailedExceptionfor both the 409 (duplicate-key oncreate) and 412 (precondition failure on conditionalupdate/upsert) cases - both currently map toCONFLICT. The portable API does not yet expose ETag-based conditional updates; when it does, the 412-equivalent path will be split into a dedicatedPRECONDITION_FAILEDcategory (tracked in issue #29).
Default Sort-Key Ordering¶
All Cosmos DB and DynamoDB query paths return results sorted by the document's sort key ascending.
Design note: The default
ORDER BYis applied to all Cosmos queries (both partition-scoped and cross-partition), not just partition-scoped ones. This gives the strongest consistency guarantee: every query, on every provider, returns items sorted by sort key. The early PR description mentioned partition-scoped only as a starting point; the final implementation was intentionally broadened to cover all queries.
Cosmos DB¶
Cosmos DB appends ORDER BY c.id ASC to every query that does not already carry
an explicit ORDER BY clause (and is not an aggregate / GROUP BY query). This
is applied server-side, so the order is globally consistent across all pages.
⚠️ Custom indexing policy - composite index required If your Cosmos container uses a custom indexing policy that does not include a composite index on
(filterField ASC, id ASC), Cosmos DB will throw a400 Bad Requestat runtime for cross-partition queries that combineWHEREand the defaultORDER BY c.id ASC. The default indexing policy includes all paths and supports this automatically. If you have tuned your indexing policy, add the composite index for every field you filter on:{ "compositeIndexes": [ [{ "path": "/filterField", "order": "ascending" }, { "path": "/id", "order": "ascending" }] ] }⚠️ RU cost Appending
ORDER BY c.id ASCto all Cosmos queries incurs an additional RU charge versus unordered queries, proportional to result-set size. This cost is the price of cross-provider consistency and is expected behavior.⚠️ Aggregates and GROUP BY Cosmos DB rejects
ORDER BYon aggregate expressions (COUNT,SUM,MIN,MAX,AVG) andGROUP BYqueries. The SDK automatically detects these patterns and omits the defaultORDER BYfor them.
DynamoDB¶
DynamoDB results are sorted in memory per page after fetching (client-side). Within a single page, items are returned sorted by sort key ascending. For multi-page scans the overall order across pages is determined by DynamoDB's internal token-based traversal, not sort key - this is a known limitation.
Spanner¶
The Spanner provider does not yet implement default sort-key ordering. Consumers relying on consistent cross-provider sort behavior should not use the Spanner provider until this gap is addressed.
Tracking: A follow-up issue will be filed to implement default sort-key ordering for the Spanner provider. Until resolved, do not mix Spanner with Cosmos or DynamoDB in conformance-sensitive workloads.
Escape Hatch Policy¶
The SDK does not expose a nativeClient() method. Direct access to the
underlying provider client is intentionally omitted to enforce portability
guarantees - code written against the SDK must remain switchable between
providers by configuration alone.