Skip to content

Commit 4e2e18b

Browse files
committed
docs: update ADRs
1 parent aac648c commit 4e2e18b

3 files changed

Lines changed: 16 additions & 24 deletions

File tree

docs/adr/001-scanevantrepository-integration-tests-only.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# ADR 001: ScanEventRepository Integration Tests Only, No IDbConnection Factory
1+
# ADR 001: ScanEventRepository - Integration Tests Only, No IDbConnection Factory
22

33
**Status:** Accepted
44
**Date:** 2026-02-21
@@ -17,15 +17,15 @@ Do not introduce an `IDbConnection` factory. Accept that `ScanEventRepository` a
1717

1818
### The meaningful abstraction already exists
1919

20-
`IScanEventRepository` means every consumer `ApiPollerWorker`, `EventProcessorWorker`, and `ScanEventProcessor` can be fully tested with NSubstitute mocks without touching a database. The repository implementation being untestable without a DB is a much smaller problem than the workers being untestable.
20+
`IScanEventRepository` means every consumer - `ApiPollerWorker`, `EventProcessorWorker`, and `ScanEventProcessor` - can be fully tested with NSubstitute mocks without touching a database. The repository implementation being untestable without a DB is a much smaller problem than the workers being untestable.
2121

2222
### Mocking IDbConnection is unsafe under Dapper.AOT
2323

24-
Dapper.AOT generates source-level interceptors that target specific `SqlConnection` call sites at compile time. A mock `IDbConnection` would not trigger those interceptors tests would execute different code paths than production, making them unreliable and potentially misleading.
24+
Dapper.AOT generates source-level interceptors that target specific `SqlConnection` call sites at compile time. A mock `IDbConnection` would not trigger those interceptors - tests would execute different code paths than production, making them unreliable and potentially misleading.
2525

2626
### The SQL is the logic
2727

28-
The interesting correctness properties of `ScanEventRepository` the MERGE semantics, the idempotency guard, the PICKUP/DELIVERY timestamp invariant live in the SQL strings. No amount of `IDbConnection` mocking verifies that the SQL is correct. This is genuine integration-test territory.
28+
The interesting correctness properties of `ScanEventRepository` - the MERGE semantics, the idempotency guard, the PICKUP/DELIVERY timestamp invariant - live in the SQL strings. No amount of `IDbConnection` mocking verifies that the SQL is correct. This is genuine integration-test territory.
2929

3030
### Deadline and risk
3131

@@ -41,7 +41,7 @@ Introducing a new abstraction layer less than two days before the deadline risks
4141

4242
## Consequences
4343

44-
- `ScanEventRepository` and `DatabaseInitializer` are covered by integration tests only (requiring a running SQL Server see Docker Compose setup in README).
44+
- `ScanEventRepository` and `DatabaseInitializer` are covered by integration tests only (requiring a running SQL Server - see Docker Compose setup in README).
4545
- All higher-level components (`ScanEventProcessor`, workers) remain fully unit-testable via `IScanEventRepository`.
4646
- The README documents the integration-test gap.
4747

docs/adr/002-no-saga-pattern.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,16 +18,16 @@ Don't use it. The design already handles failures correctly without a coordinato
1818

1919
## Why
2020

21-
If `SendToSqs` fails, `LastEventId` doesn't advance the next poll re-covers those events.
22-
If `MergeToDb` fails, the message isn't deleted SQS redelivers via visibility timeout.
21+
If `SendToSqs` fails, `LastEventId` doesn't advance - the next poll re-covers those events.
22+
If `MergeToDb` fails, the message isn't deleted - SQS redelivers via visibility timeout.
2323

2424
Both chains self-recover because the MERGE is idempotent:
2525

2626
```sql
2727
incoming.EventId > stored.LatestEventId
2828
```
2929

30-
Replaying the same event is a no-op. Saga exists to handle cases where replay is *not* safe. That case doesn't exist here.
30+
Replaying the same event is a no-op. Saga exists to handle cases where replay is _not_ safe. That case doesn't exist here.
3131

3232
## What Saga Would Have Cost
3333

@@ -41,12 +41,12 @@ All of that is already implicit in `LastEventId` + SQS visibility timeout. Addin
4141

4242
This decision is specific to the current poll-based design. In an event-driven model the calculus changes.
4343

44-
Each scan event arriving could naturally produce side-effects across multiple systems a row written to DynamoDB for real-time lookup, a record appended to S3 for compliance archiving, a downstream notification, an audit trail. Each of those writes is a discrete step with its own failure mode. That's exactly the problem Saga is designed for: coordinating a sequence of steps across systems where partial failure needs explicit handling.
44+
Each scan event arriving could naturally produce side-effects across multiple systems - a row written to DynamoDB for real-time lookup, a record appended to S3 for compliance archiving, a downstream notification, an audit trail. Each of those writes is a discrete step with its own failure mode. That's exactly the problem Saga is designed for: coordinating a sequence of steps across systems where partial failure needs explicit handling.
4545

46-
AWS Step Functions would carry most of the weight here the state machine, retry policies, compensation routing, and execution history are all provided out of the box. The idempotency invariant still applies and still protects against duplicate executions, but Saga would earn its complexity cost in that design.
46+
AWS Step Functions would carry most of the weight here - the state machine, retry policies, compensation routing, and execution history are all provided out of the box. The idempotency invariant still applies and still protects against duplicate executions, but Saga would earn its complexity cost in that design.
4747

4848
The current poll-based single-DB design doesn't have that fan-out problem, so Saga adds nothing today.
4949

5050
## Note on Test Cancellation
5151

52-
The `CancellationToken` complexity in the worker tests is a test harness concern how to drive a `BackgroundService` loop one iteration at a time. It's unrelated to this decision.
52+
The `CancellationToken` complexity in the worker tests is a test harness concern - how to drive a `BackgroundService` loop one iteration at a time. It's unrelated to this decision.

docs/local-setup.md

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,16 @@ This starts:
2828
- **Azure SQL Edge** on `localhost:1433` (ARM64-compatible SQL Server)
2929
- **LocalStack** on `localhost:4566` (local SQS emulator - queues auto-created via `scripts/init-localstack.sh`)
3030

31-
### 3. Create the database
31+
The `ScanEvents` database and schema tables are auto-created by `DatabaseInitialiser` on first worker startup — no manual `sqlcmd` step required.
3232

33-
```bash
34-
docker exec -it $(docker ps -q -f ancestor=mcr.microsoft.com/azure-sql-edge) \
35-
/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P 'YourStr0ngPassw0rd!' \
36-
-Q "CREATE DATABASE ScanEvents"
37-
```
38-
39-
The worker auto-creates the `ProcessingState` and `ParcelSummary` tables on startup via `DatabaseInitialiser`.
40-
41-
### 4. Set dummy AWS credentials (LocalStack doesn't validate them)
33+
### 3. Set dummy AWS credentials (LocalStack doesn't validate them)
4234

4335
```bash
4436
export AWS_ACCESS_KEY_ID=test
4537
export AWS_SECRET_ACCESS_KEY=test
4638
```
4739

48-
### 5. Configure the API base URL (required)
40+
### 4. Configure the API base URL (required)
4941

5042
The worker cannot connect without this. Set it via user-secrets:
5143

@@ -56,13 +48,13 @@ dotnet user-secrets set "ScanEventApi:BaseUrl" "https://your-api-host" \
5648

5749
Or edit `ScanEventApi:BaseUrl` directly in `src/ScanEventWorker/appsettings.json`.
5850

59-
### 6. Run the worker
51+
### 5. Run the worker
6052

6153
```bash
6254
dotnet run --project src/ScanEventWorker/ScanEventWorker.csproj
6355
```
6456

65-
### 7. Verify resumability
57+
### 6. Verify resumability
6658

6759
Stop the worker (`Ctrl+C`) and restart it. The log will show:
6860

0 commit comments

Comments
 (0)