Palash Chauhan
← Blog

phoenix-adapters: DynamoDB on Phoenix

phoenix dynamodb announcement

DynamoDB is awesome, until it isn’t. Eventually something pushes you to look elsewhere: a second cloud to support, a bill that keeps climbing, or simply wanting a different backend. The hard part is not picking the new database. It is the rewrite: a different client, a different data model, all of your query code redone.

Apache Phoenix on HBase is a seriously capable store: documents, secondary indexes, TTL, change streams, and more. If it is new to you, this blog walks through it in three series, Phoenix Fundamentals for how it turns Apache HBase into a SQL database, Phoenix Features for some of its core features, and Phoenix and DynamoDB Parity for the features that make this adapter possible. phoenix-adapters is the piece that puts a DynamoDB API in front of all of it, so you can keep your existing app and change exactly one thing: the endpoint URL.

🏛️ The architecture

flowchart TB
  subgraph clients ["Your apps (AWS SDK)"]
    direction LR
    c1["Java / Python"]
    c2["Node / Go"]
  end
  lb["load balancer"]
  subgraph rest ["phoenix-adapters REST (stateless)"]
    direction LR
    r1["instance 1"]
    r2["instance 2"]
    r3["instance 3"]
  end
  px["Apache Phoenix"]
  hb["HBase"]
  c1 --> lb
  c2 --> lb
  lb --> r1
  lb --> r2
  lb --> r3
  r1 --> px
  r2 --> px
  r3 --> px
  px --> hb
  style r1 stroke:#f59e0b,stroke-width:4px
  style r2 stroke:#f59e0b,stroke-width:4px
  style r3 stroke:#f59e0b,stroke-width:4px

The REST process is stateless: all the state lives in HBase. So you run as many instances as you need behind any load balancer, with no session affinity and no coordination between them.

🧩 Why this is possible now

None of this happened overnight. Over the last year and a half, the Apache Phoenix community built these DynamoDB-parity features on top of an already deep foundation. Each DynamoDB concept lands on one of them:

DynamoDBPhoenix feature
Item (document)BSON column
Condition and update expressionsBSON expressions
Global secondary indexEventually consistent index
DynamoDB StreamsCDC change stream
Time to liveConditional TTL

Closing those gaps is what lets the adapter implement a faithful DynamoDB API rather than a lookalike. And because the store underneath is a full SQL database, the same data your DynamoDB calls write is just a Phoenix table, so you can also reach it with plain SQL whenever you want.

✅ Supported APIs

The adapter covers the operations real applications lean on, across DDL, reads, writes, and streams:

GroupOperations
DDLCreateTable, DeleteTable, DescribeTable, ListTables, UpdateTable, UpdateTimeToLive, DescribeTimeToLive
ReadsGetItem, BatchGetItem, Query, Scan
WritesPutItem, UpdateItem, DeleteItem, BatchWriteItem
StreamsListStreams, DescribeStream, GetShardIterator, GetRecords

The Streams API works with the DynamoDB Streams Kinesis Adapter and the Kinesis Client Library (KCL) v1, so existing DynamoDB Streams consumers work too.

🚀 Try it in a few minutes

The repo ships a Docker setup that brings up HBase, Phoenix, and the REST server together. From a clone of the repo:

# bring up the full stack and wait until every container is healthy
docker compose -f docker/docker-compose.yml up -d --build --wait

# validate it end to end
bash docker/scripts/smoke.sh
# -> Result: 21 checks PASSED across 18 API calls

# the DynamoDB-compatible endpoint is now at http://localhost:8842
curl -s -X POST http://localhost:8842/ \
  -H 'Content-Type: application/x-amz-json-1.0' \
  -H 'X-Amz-Target: DynamoDB_20120810.ListTables' -d '{}'

# tear it down when you are done
docker compose -f docker/docker-compose.yml down -v

Point any AWS SDK at that endpoint and the rest of your code is unchanged. It also makes a handy little backend when you just want something DynamoDB-shaped behind a project without standing up the real thing. Spin it up, kick the tires, and let me know what breaks.

📚 Further reading