ADR-006: Extract Bates/Stamps to Standalone Service¶
Status¶
Proposed
Date¶
2026-03-19
Context¶
Bates numbering and confidentiality stamping is a core eDiscovery function that applies unique identifiers and confidentiality designations to every page of every document in a case.
Current State¶
The stamping system has two completely different execution paths based on nge_enabled?:
NGE Path (already clean):
BatesStampJob → Attachment#stamp_with_template
→ ExhibitNutrientAction#update_nutrient_bates
→ NextpointNutrient.process_bulk_stamp_update (REST API)
→ PSPDFKit text annotations (readOnly, customData.type='bates_stamp')
→ Done (synchronous, no completion polling)
Legacy Path (worker-dependent):
BatesStampJob → Attachment#stamp_with_template
→ create_image_manipulation_job (ProcessingJob record)
→ Workers pick up → ImageManipulationWorker
→ Stamper.bates_and_confidentiality_stamp_image (GD2 pixel rendering)
→ Burns stamp into TIFF/PNG page image
→ BatesStampCompletionJob polls ProcessingJob until all workers finish
Key Files¶
| File | Lines | Purpose |
|---|---|---|
BatesStampJob |
~150 | Orchestrator: iterates exhibits, delegates to Attachment#stamp |
BatesRemovalJob |
~80 | Removal: delegates to LabelBatesRemover |
BatesStampCompletionJob |
~60 | Legacy-only: polls worker ProcessingJobs |
BatesRemovalCompletionJob |
~50 | Legacy-only: polls worker ProcessingJobs |
ExhibitNutrientAction |
~300 | NGE: Nutrient API annotation CRUD |
Stamper.rb (workers) |
~200 | Legacy: GD2 pixel stamp rendering |
stampStackUtils.js (React) |
~400 | NGE frontend: PSPDFKit annotation positioning |
BatesStampModal.jsx |
~200 | NGE frontend: bates edit modal |
ExhibitStampEditor.jsx |
~300 | Exhibit stamp template config |
BatesManagementService |
~100 | Single-document bates from viewer |
ExhibitStampingService |
~100 | Legacy exhibit stamp placement |
Why Extract?¶
- NGE path is already decoupled — Nutrient API calls are synchronous REST, no worker dependency
- No completion polling needed — NGE stamps are instant (Nutrient API), unlike Legacy worker polling
- Cross-cutting concern — stamps affect export (documentexporter), exchange (documentexchanger), and review
- High page volume — a large case can have millions of pages; independent scaling matters
Decision¶
Extract NGE bates/confidentiality stamping into a standalone Lambda service that encapsulates all Nutrient annotation operations for stamps.
Architecture¶
Rails App (or any NGE module)
│
├── SNS: StampRequested
│ ├── type: bates_stamp | confidentiality_stamp | exhibit_stamp
│ ├── action: apply | remove | update
│ ├── exhibit_ids, case_id, pattern, template
│ │
│ ▼
│ SQS Queue → Lambda: StampProcessor
│ │
│ ├── core/
│ │ ├── bates_processor.py (bates numbering logic)
│ │ ├── confidentiality_processor.py (confidentiality stamp logic)
│ │ ├── exhibit_stamp_processor.py (exhibit stamp logic)
│ │ └── stamp_layout.py (font sizing, word wrap, positioning)
│ │
│ ├── shell/
│ │ ├── nutrient_client.py (PSPDFKit annotation CRUD)
│ │ ├── db/database.py (per-case MySQL for stamp_details)
│ │ └── sns_ops.py (progress/completion events)
│ │
│ ▼
│ Nutrient API (annotation create/delete)
│ │
│ ▼
│ SNS: StampCompleted → PSM
What Moves¶
| From | To | What |
|---|---|---|
ExhibitNutrientAction concern |
core/stamp_layout.py |
Font sizing, word wrap, annotation positioning |
ExhibitNutrientAction concern |
shell/nutrient_client.py |
Nutrient API calls (bulk stamp update, delete) |
BatesStampJob (NGE branch) |
core/bates_processor.py |
Bates numbering iteration, page-count validation |
BatesRemovalJob (NGE branch) |
core/bates_processor.py |
Bates removal logic |
BatesManagementService (NGE) |
Can call stamp service via SNS | Single-doc bates edits |
What Stays in Rails¶
| Component | Why |
|---|---|
BatesStampJob (Legacy branch) |
Legacy cases still need worker-based pixel stamping |
BatesStampCompletionJob |
Legacy-only polling |
Stamper.rb (workers) |
Legacy GD2 rendering — not worth modernizing |
BatesPattern model |
Pattern storage and next-number management stays in Rails DB |
stampStackUtils.js |
Frontend PSPDFKit SDK annotation positioning stays client-side |
ExhibitStampEditor.jsx |
Frontend stamp template configuration |
Migration Strategy¶
- Phase 1: Extract
ExhibitNutrientActionstamp methods into a service object within Rails (zero-risk refactoring, same pattern as ADR-005) - Phase 2: Build Lambda service with Nutrient client, deploy alongside Rails
- Phase 3: Rails sends
StampRequestedSNS event instead of calling Nutrient directly - Legacy cases: No change — Legacy path continues through workers until case migration
Consequences¶
Positive¶
- Single owner for stamp logic — one service handles all stamp types consistently
- documentexporter can call directly — instead of Rails middleware, exporter triggers stamps
- Independent scaling — stamp operations scale separately from Rails Sidekiq
- Cleaner Nutrient integration — all Nutrient stamp API calls in one service
Negative¶
- Dual path persists — Legacy cases still use workers/GD2 (but this is permanent per ADR)
- Pattern management complexity —
BatesPattern(next number) must be thread-safe across Lambda invocations - Frontend unchanged —
stampStackUtils.jsstill does client-side annotation positioning
Risks¶
- Bates number sequencing — bates numbers must be globally sequential per pattern. Lambda concurrency could cause gaps or duplicates. Mitigation: use DB-level
SELECT ... FOR UPDATEonBatesPatternfor atomic increment. - Nutrient API rate limits — bulk stamping millions of pages could hit Nutrient rate limits. Mitigation: SQS-based throttling with configurable concurrency.
Ask the Architecture
×
Ask questions about Nextpoint architecture, patterns, rules, or any module. Powered by Claude Opus 4.6.