0017. Notification Service Template Storage
Status: Accepted
Date: 2026-01-23
Context: Where and how notification templates (email, SMS, push) are stored, versioned, and scaled.
Context
The Notification Service requires template storage for email, SMS, and push notifications. Templates need to:
- Be versioned and managed (draft → active)
- Support HTML and text formats
- Scale to high throughput (thousands of notifications per minute)
- Integrate with existing platform infrastructure (AWS, LocalStack)
Template Storage Options:
- PostgreSQL - Relational database, simple queries
- DynamoDB - NoSQL, scalable, serverless
- S3 - Object storage, versioning support
- Hybrid - Metadata in DB, assets in S3
Note: Template engine selection is covered in ADR-0018.
Decision
Template Storage: AWS DynamoDB + S3 (Hybrid)
Primary Storage: DynamoDB
- Store template metadata and content in DynamoDB table
notification-templates
- Partition Key:
template_id (String)
- Sort Key:
version (Number) for version history
- Attributes: channel, activation_status, metadata, content (subject, html_body, text_body, placeholders)
Large Assets: S3
- Store large HTML bodies, images, attachments in S3
- DynamoDB item contains S3 reference (optional)
- S3 Object Versioning for asset versioning
Why DynamoDB:
- Scalability - Fully managed, horizontally scalable NoSQL
- Security - Fine-grained AWS IAM permissions
- Concurrency - Conditional writes prevent overwrite conflicts
- Integration - Native AWS integration (Lambda, AppConfig, API Gateway)
- Reliability - Point-in-Time Recovery (PITR)
- Existing Infrastructure - Platform already uses DynamoDB (via LocalStack)
Why S3 for Assets:
- Large Content - Better for large HTML bodies, images
- Versioning - S3 Object Versioning for asset history
- Cost - Lower cost for large files
Rationale
Why DynamoDB (Not PostgreSQL):
- Scalability - Better suited for high-throughput read patterns (template lookups)
- Serverless - Fully managed, no connection pooling concerns
- Existing Infrastructure - Platform already uses DynamoDB
- Versioning - Natural fit for versioned templates (PK/SK pattern)
- Multi-Region - Easier multi-region deployment
Why Not PostgreSQL:
- Read-Heavy - Templates are read-heavy (lookup by templateId), DynamoDB optimized for this
- Simple Queries - No complex joins needed, DynamoDB sufficient
- Scaling - DynamoDB scales automatically, PostgreSQL requires more management
Consequences
Positive:
- Scalability - DynamoDB scales automatically for high throughput
- Integration - Native AWS integration (DynamoDB, S3)
- Versioning - Natural versioning support (DynamoDB + S3)
- Cost - Efficient storage (DynamoDB for metadata, S3 for large assets)
- Existing Infrastructure - Platform already uses DynamoDB
Negative / Tradeoffs:
- DynamoDB Learning Curve - Team must understand DynamoDB patterns
- Query Limitations - DynamoDB has limited query capabilities (GSI required for complex queries)
- S3 Integration - Requires S3 client setup for large assets
- LocalStack - Must ensure LocalStack DynamoDB support for local development
Mitigations:
- Documentation - Clear DynamoDB schema and access patterns
- GSI - Use Global Secondary Indexes for query patterns (e.g., by channel)
- LocalStack - LocalStack fully supports DynamoDB
- Examples - Provide examples of template storage and retrieval
Implementation
DynamoDB Schema:
Table: notification-templates
- Partition Key:
template_id (String)
- Sort Key:
version (Number)
- Attributes:
channel (String) - EMAIL, SMS, PUSH
activation_status (String) - ACTIVE, DRAFT, ARCHIVED
metadata (Map) - tags, owner, last_modified
content (Map) - subject, html_body, text_body, placeholders
s3_reference (String) - Optional S3 key for large assets
GSI: channel-index
- Partition Key:
channel
- Sort Key:
template_id
- For listing templates by channel
Qute Template Rendering:
// Retrieve template from DynamoDB
Template template = templateRepository.getTemplate("welcome-email-v1");
// Render with Qute
String rendered = Qute.fmt(template.getHtmlBody())
.data("firstName", "John")
.data("activationLink", "https://example.com/activate?token=abc123")
.render();
Template Storage Pattern:
- Small Templates - Store directly in DynamoDB
content.html_body
- Large Templates - Store in S3, reference in DynamoDB
s3_reference
- Versioning - New version = new DynamoDB item (same template_id, different version)
- Activation -
activation_status field indicates active version
- ADR-0012: Clean Architecture Package Structure (package organization)
- ADR-0015: Fire-and-Forget / Asynchronous Messaging Pattern (async processing)
- ADR-0018: Notification Service Template Engine Selection (template rendering engine)
Future Considerations
Template Versioning:
- Current: Immutable templates (new version = new item)
- Future: May add explicit versioning strategy if needed
Template Management UI:
- Future: Admin UI for template management
- Current: API-only or seed scripts
Multi-Language Support:
- Future: Per-template locales or locale variable
- Current: Single language