PHPackages                             chrisjenkinson/dynamo-db-read-model - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Database &amp; ORM](/categories/database)
4. /
5. chrisjenkinson/dynamo-db-read-model

ActiveLibrary[Database &amp; ORM](/categories/database)

chrisjenkinson/dynamo-db-read-model
===================================

A DynamoDB ReadModel implementation for Broadway.

v2.1(1mo ago)04.4k↑110%[3 PRs](https://github.com/chrisjenkinson/dynamo-db-read-model/pulls)GPL-3.0-or-laterPHPCI passing

Since Sep 24Pushed 5d ago1 watchersCompare

[ Source](https://github.com/chrisjenkinson/dynamo-db-read-model)[ Packagist](https://packagist.org/packages/chrisjenkinson/dynamo-db-read-model)[ RSS](/packages/chrisjenkinson-dynamo-db-read-model/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (6)Dependencies (24)Versions (13)Used By (0)

DynamoDB Read Model
===================

[](#dynamodb-read-model)

A DynamoDB-backed Broadway read model repository.

Installation
------------

[](#installation)

```
composer require chrisjenkinson/dynamo-db-read-model
```

The package supports `async-aws/dynamo-db` `^1.3`, `^2.0`, and `^3.0`.

DynamoDB Table
--------------

[](#dynamodb-table)

Create one table for the read model store. The table uses a composite primary key:

AttributeTypeKeyDescription`Name`StringPartitionRepository name passed to `RepositoryFactory::create()``Id`StringSortRead model identifier from `Identifiable::getId()``Data`String-JSON-encoded serialized read model payloadThe library queries by `Name` to load all read models for a repository, and uses `Name` + `Id` for single-item reads, writes, and deletes. It does not require secondary indexes.

The DynamoDB `Id` value is required to match the serialized read model's `Identifiable::getId()`. Rows where the physical key and payload id differ are treated as invalid stored data and rejected on read; repair those rows with explicit operational tooling rather than relying on repository normalization.

`find($id)` uses the full primary key (`Name` + `Id`) and is a bounded lookup. `findBy()` also has bounded paths, but only when the criteria contain the exact, lowercase `id` field:

- A non-empty string `id` uses `GetItem`. Any `id` value that is neither a non-empty string nor an array, including an object or resource, returns `[]`without making an AWS request. Arrays follow the list validation below.
- A PHP list of ids uses `BatchGetItem`, split into requests of at most 100 keys. Every entry must be a non-empty string; a non-list array or invalid entry throws `InvalidIdCriterion`. An empty list returns `[]` without making an AWS request.
- Duplicate ids are removed by first occurrence. Results follow that requested order, with ids that do not exist omitted. Additional criteria are applied in PHP only to this bounded set of loaded models.
- For each chunk of at most 100 keys, unprocessed batch keys are retried with jitter for up to four total attempts. If keys remain unresolved, `BatchGetRetriesExhausted` reports the failure.

The field name is case-sensitive. `Id`, `ID`, and other non-empty, non-ID criteria do not select these bounded paths: they query the full repository partition for the configured `Name`, deserialize its models, and filter in PHP unless the application models a separate indexed lookup. `findBy([])` returns `[]` without making an AWS request. `findAll()` queries and returns the full partition. For production-sized collections, avoid `findAll()` and non-ID `findBy()` calls on request paths unless that full-partition work is intentional. These optimizations do not change Broadway's `Repository` interface.

Snapshot Store
--------------

[](#snapshot-store)

This release changes the public constructors for `DynamoDbRepositoryFactory`and `DynamoDbRepository`.

`DynamoDbRepositoryFactory` requires a `ReadModelSnapshotStore` so repositories created by the same factory can suppress unchanged physical writes without an extra DynamoDB read:

```
$factory = new DynamoDbRepositoryFactory($client, $serializer, 'read-models', new ReadModelSnapshotStore());
```

The factory is the recommended construction path. It creates the per-repository storage and matcher internally.

Direct `DynamoDbRepository` construction is still possible, but its constructor now expects explicit storage and matcher collaborators. If you manually construct repositories, move the DynamoDB-specific arguments into a `DynamoDbReadModelStorage` configured for that table, repository name, and read model class, then pass that storage with a `ReadModelFieldMatcher`:

```
$storage = new DynamoDbReadModelStorage(
    $client,
    $inputBuilder,
    $serializer,
    $jsonEncoder,
    $jsonDecoder,
    'read-models',
    'repository-name',
    MyReadModel::class,
    new ReadModelSnapshotStore()
);

$repository = new DynamoDbRepository(
    $storage,
    new ReadModelFieldMatcher()
);
```

Call `$factory->clearSnapshots()` before reusing the same factory after deleting or recreating the backing read-model table.

Deferred Persistence
--------------------

[](#deferred-persistence)

Repositories created with `DynamoDbRepositoryFactory::create()` keep the original immediate behaviour: `save()` writes with `PutItem`, `remove()` writes with `DeleteItem`, and `find()` reads with `GetItem`.

For replay or other batching-sensitive workflows, opt in explicitly to deferred persistence:

```
$repository = $factory->createDeferred('repository-name', MyReadModel::class);

$model = $repository->find($id) ?? new MyReadModel($id);

// Projector handlers can keep using Broadway\Repository methods.
$repository->save($model);

// Write pending removals and saves to DynamoDB.
$repository->flush();
```

`createDeferred()` is package-specific and is not part of Broadway's `RepositoryFactory` interface. Code that needs deferred repository creation should type-hint `DeferredRepositoryFactory` or the concrete `DynamoDbRepositoryFactory`.

A deferred repository still implements `Broadway\ReadModel\Repository`, and also implements `FlushableRepository`:

```
interface FlushableRepository extends Broadway\ReadModel\Repository
{
    public function flush(): void;

    public function flushWithContext(array $context): void;

    public function clear(): void;

    public function pendingOperations(): array;
}
```

In deferred mode, `find($id)` checks an in-memory identity map before DynamoDB. `save()` captures the read model's serialized state at the time `save()` is called and stages that state in memory. Repeated saves for the same read-model id replace the staged state and collapse to one `PutItem` on `flush()`. Mutating a read model after `save()` does not change the staged write unless `save()` is called again. `remove()` marks the id removed in memory, so `find($id)` returns `null` until the item is saved again or `clear()` discards the deferred state. Saving a removed id cancels the pending delete.

`pendingOperations()` returns the currently staged removes and save-time snapshots for inspection. `flush()` writes pending deletes and saves one item at a time through the same DynamoDB storage path as the immediate repository. If a write fails, the failing and remaining pending state is retained for retry or inspection. Successfully flushed entries are no longer pending. `clear()` only discards local managed, dirty, and removed state; it does not write anything.

Failures are reported with `DeferredFlushFailed`, which includes the operation, read-model id, table, repository name, read-model class, and previous exception. The repository does not know application-level details such as tenant id, projector name, or source event id. Pass those at the flush boundary when they are useful:

```
$repository->flushWithContext([
    'tenantId' => $tenantId,
    'projector' => SomeProjector::class,
    'sourceEventId' => $eventId,
]);
```

Deferred `findAll()` and `findBy()` results account for pending local state. `findAll()` and `findBy()` with non-empty non-ID criteria query DynamoDB each time and overlay pending local saves/removes; `findBy([])` returns `[]` without an AWS request. DynamoDB reads use the repository's normal consistency settings. For an ID-constrained `findBy()`, pending removals are excluded, managed and staged models are resolved locally, and only unresolved ids are fetched from DynamoDB. Deferred mode makes this repository's staged changes visible; it does not make broad projection queries transactional or globally fresh.

`findBy()` remains available for Broadway compatibility. Its non-ID form is intended for small read-side collections because it performs the full-partition work described above. Do not use `findBy()` as a write-side invariant or duplicate guard during command handling, seeding, or projection rebuilds. Model those cases as deterministic lookup read models and query them with `find($id)`.

The identity map is scoped to the deferred repository instance. It can return a cached model even if another process changes DynamoDB while the deferred repository is still open. Keep deferred repositories short-lived around a replay, seed, or console unit of work, and call `clear()` before reusing one across independent work. For long replays or seeds, use explicit `flush(); clear();`checkpoints when managed objects are no longer needed.

This is a batching and performance boundary, not a DynamoDB transaction. A failed flush can leave earlier operations persisted and later operations still pending.

Example AWS CLI setup:

```
aws dynamodb create-table \
  --table-name read-models \
  --billing-mode PAY_PER_REQUEST \
  --attribute-definitions \
    AttributeName=Name,AttributeType=S \
    AttributeName=Id,AttributeType=S \
  --key-schema \
    AttributeName=Name,KeyType=HASH \
    AttributeName=Id,KeyType=RANGE
```

Testing
-------

[](#testing)

The test suite expects DynamoDB Local. In GitHub Actions this is provided as a service named `dynamodb-local`; locally, override the endpoint:

```
DYNAMODB_ENDPOINT=http://127.0.0.1:8000 \
AWS_ACCESS_KEY_ID=none \
AWS_SECRET_ACCESS_KEY=none \
composer run-script phpunit
```

CI tests supported PHP versions against each supported AsyncAws DynamoDB major.

Quality Checks
--------------

[](#quality-checks)

```
vendor/bin/ecs check --config ecs.php
composer run-script phpstan
composer run-script infection
```

###  Health Score

48

—

FairBetter than 94% of packages

Maintenance97

Actively maintained with recent releases

Popularity21

Limited adoption so far

Community9

Small or concentrated contributor base

Maturity55

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 62.5% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~278 days

Recently: every ~348 days

Total

6

Last Release

31d ago

Major Versions

v0.3 → v1.02026-06-15

v1.0 → v2.02026-06-30

### Community

Maintainers

![](https://www.gravatar.com/avatar/3810ae075b824735d2ce95e065d7b43494f19f8cb941913dde34829783a98d88?d=identicon)[chrisjenkinson](/maintainers/chrisjenkinson)

---

Top Contributors

[![chrisjenkinson](https://avatars.githubusercontent.com/u/568142?v=4)](https://github.com/chrisjenkinson "chrisjenkinson (20 commits)")[![dependabot[bot]](https://avatars.githubusercontent.com/in/29110?v=4)](https://github.com/dependabot[bot] "dependabot[bot] (12 commits)")

###  Code Quality

TestsPHPUnit

Static AnalysisPHPStan

Code StyleECS

Type Coverage Yes

### Embed Badge

![Health badge](/badges/chrisjenkinson-dynamo-db-read-model/health.svg)

```
[![Health](https://phpackages.com/badges/chrisjenkinson-dynamo-db-read-model/health.svg)](https://phpackages.com/packages/chrisjenkinson-dynamo-db-read-model)
```

###  Alternatives

[jdorn/sql-formatter

a PHP SQL highlighting library

3.8k117.8M121](/packages/jdorn-sql-formatter)[backup-manager/backup-manager

A framework agnostic database backup manager with user-definable procedures and support for S3, Dropbox, FTP, SFTP, and more with drivers for popular frameworks.

1.7k1.6M11](/packages/backup-manager-backup-manager)[propel/propel1

Propel is an open-source Object-Relational Mapping (ORM) for PHP5.

8351.6M88](/packages/propel-propel1)[broadway/event-store-dbal

Event store implementation using doctrine/dbal

29911.1k8](/packages/broadway-event-store-dbal)[insolita/yii2-migration-generator

Set of gii tools for generating files for migration by schema of table , phpdoc or table data

108508.0k5](/packages/insolita-yii2-migration-generator)[xpdo/xpdo

A PDO-based Object/Relational Bridge Library

7088.4k4](/packages/xpdo-xpdo)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
