The design of an Email Template Management System that supports system-wide templates, user-defined custom templates, localization, and template snippets with unique identifiers.
Overview
An Email Template Management System enables organizations to create, manage, and use email templates with support for both system-wide templates and user-specific customizations. The system supports internationalization, localization, and reusable snippets, making it highly flexible and maintainable.
The primary goals of such a system are:
- Efficient template management and versioning
- Support for system and user-specific templates
- Robust localization and internationalization
- Reusable template snippets
- Template override management
- High performance template rendering
Requirements
Functional Requirements
- Create Template: Accept template content, metadata, type (system/custom), localization info, and unique template code
- Override System Template: Allow users to create personal versions of system templates
- Manage Snippets: Create and manage reusable template snippets
- Render Template: Process template with variables and render in specified locale
- List Templates: View available templates (system + user-specific)
- Version Control: Track template changes and maintain version history
- Template Migration: Import/Export templates across environments
Non-Functional Requirements
- High Performance: Fast template rendering and variable substitution
- Scalability: Handle large numbers of templates and concurrent renderings
- Security: Role-based access control for template management
- Audit Trail: Track template modifications and usage
- High Availability: Ensure template service is always accessible
Scale & Assumptions
- Daily Active Users: 1000-100,000
- System Templates: 100-1000
- Custom Templates per User: 5-50
- Template Size: 2-20 KB
- Supported Locales: 10-50
- Template Snippets: 50-500
- Concurrent Template Renders: 100-1000/second
Design Considerations
Template Identifier Design
Each template must have a unique identifier (code) that follows this pattern:
{category}_{purpose}_{version}
Example codes:
notification_welcome_v1invoice_reminder_v2marketing_newsletter_v1
For user-specific overrides, append the user identifier:
{category}_{purpose}_{version}_user_{userId}
Localization Strategy
Locale Identification:
- Use standard locale codes (e.g., 'en-US', 'es-ES', 'fr-FR')
- Support fallback chains (e.g., 'fr-CA' → 'fr' → default)
Content Storage:
-- Template content stored per locale template_content = { "en-US": "Welcome to {{company}}!", "es-ES": "¡Bienvenido a {{company}}!", "fr-FR": "Bienvenue à {{company}}!" }Resource Bundles:
- Store translations in language-specific bundles
- Support nested key structures for organization
- Enable dynamic loading based on locale
Template Override Management
System templates can be overridden by users with the following rules:
- Original system template remains unchanged
- User-specific version is stored separately
- Override is only visible to the creating user
- System updates don't affect user overrides
- Users can revert to system version
Data Storage
1) Template Metadata Store (RDBMS)
CREATE TABLE templates (
id UUID PRIMARY KEY,
template_code VARCHAR(100) NOT NULL UNIQUE,
category VARCHAR(50) NOT NULL,
name VARCHAR(200) NOT NULL,
description TEXT,
type VARCHAR(20) NOT NULL, -- 'system' or 'custom'
owner_id UUID NULL, -- NULL for system templates
parent_template_id UUID NULL, -- Reference to system template if override
version INT NOT NULL,
status VARCHAR(20) NOT NULL, -- 'draft', 'active', 'deprecated'
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
created_by UUID NOT NULL,
updated_by UUID NOT NULL
);
CREATE TABLE template_localizations (
id UUID PRIMARY KEY,
template_id UUID NOT NULL REFERENCES templates(id),
locale VARCHAR(10) NOT NULL,
content TEXT NOT NULL,
subject_template TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(template_id, locale)
);
CREATE TABLE template_snippets (
id UUID PRIMARY KEY,
snippet_code VARCHAR(100) NOT NULL UNIQUE,
name VARCHAR(200) NOT NULL,
description TEXT,
content TEXT NOT NULL,
scope VARCHAR(20) NOT NULL, -- 'global', 'category', 'template'
category VARCHAR(50) NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE template_variables (
id UUID PRIMARY KEY,
template_id UUID NOT NULL REFERENCES templates(id),
variable_name VARCHAR(100) NOT NULL,
description TEXT,
required BOOLEAN NOT NULL DEFAULT false,
default_value TEXT,
validation_regex TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
2) Template Cache (Redis)
- Cache rendered templates
- Store frequently used snippets
- Cache template metadata
Cache Structure:
template:metadata:{template_code} → JSON metadata
template:content:{template_code}:{locale} → Template content
template:rendered:{template_code}:{locale}:{hash(variables)} → Rendered content
snippet:{snippet_code} → Snippet content
High-Level Architecture
┌─────────────────┐
│ Clients │
│ (API/Web/SDK) │
└────────┬────────┘
│
┌────────▼────────┐
│ API Gateway │
└────────┬────────┘
│
┌───────────────┴──────────────┐
│ │
┌───────▼─────────┐ ┌────────▼─────────┐
│ Template │ │ Template │
│ Management API │ │ Rendering API │
└───────┬─────────┘ └────────┬────────┘
│ │
┌───────▼─────────┐ ┌───────▼────────┐
│ Template │ │ Template │
│ Service │ │ Engine │
└───────┬─────────┘ └───────┬───────┘
│ │
┌───────────┴──────────────┬────────────┘
│ │
┌───────▼───────┐ ┌──────▼─────┐
│ Database │ │ Redis │
│ (Postgres) │ │ Cache │
└───────────────┘ └────────────┘
Template Rendering Flow
Request Processing:
- Validate template code and locale
- Check user permissions
- Validate required variables
Template Resolution:
- Check for user override
- Fallback to system template
- Resolve locale with fallback chain
Snippet Processing:
- Load referenced snippets
- Process snippet variables
- Cache rendered snippets
Variable Substitution:
- Validate all required variables
- Apply default values where needed
- Process nested variables
Caching:
- Cache rendered templates
- Use variable hash for cache key
- Implement cache invalidation strategy
API Design
1. Create Template
POST /v1/templates
Request:
{
"templateCode": "notification_welcome_v1",
"category": "notification",
"name": "Welcome Email",
"description": "Template for new user welcome emails",
"type": "system",
"content": {
"en-US": {
"subject": "Welcome to {{company}}!",
"body": "Dear {{name}},\n\nWelcome to {{company}}..."
}
},
"variables": [
{
"name": "company",
"required": true
},
{
"name": "name",
"required": true
}
]
}
2. Override System Template
POST /v1/templates/{templateCode}/override
Request:
{
"content": {
"en-US": {
"subject": "Welcome to {{company}} - Custom",
"body": "Dear {{name}},\n\nThank you for joining {{company}}..."
}
}
}
3. Render Template
POST /v1/templates/{templateCode}/render
Request:
{
"locale": "en-US",
"variables": {
"company": "ACME Corp",
"name": "John Doe"
}
}
Security Considerations
Access Control:
- Role-based access for template management
- User-level permissions for overrides
- API key authentication for rendering
Content Security:
- Template content validation
- Variable escape/sanitization
- HTML sanitization for rich templates
Rate Limiting:
- Per-user template creation limits
- Rendering rate limits
- API usage quotas
Monitoring and Maintenance
Metrics:
- Template render times
- Cache hit rates
- Template usage statistics
- Error rates and types
Maintenance:
- Regular template cleanup
- Version deprecation
- Cache invalidation
- Performance optimization
Low-Level System Design (C# Implementation)
Core Domain Models
public class Template
{
public Guid Id { get; set; }
public string TemplateCode { get; set; }
public string Category { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public TemplateType Type { get; set; } // System or Custom
public Guid? OwnerId { get; set; } // null for system templates
public Guid? ParentTemplateId { get; set; } // for overrides
public int Version { get; set; }
public TemplateStatus Status { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public Guid CreatedBy { get; set; }
public Guid UpdatedBy { get; set; }
public virtual ICollection<TemplateLocalization> Localizations { get; set; }
public virtual ICollection<TemplateVariable> Variables { get; set; }
}
public class TemplateLocalization
{
public Guid Id { get; set; }
public Guid TemplateId { get; set; }
public string Locale { get; set; }
public string Content { get; set; }
public string SubjectTemplate { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
public virtual Template Template { get; set; }
}
public class TemplateSnippet
{
public Guid Id { get; set; }
public string SnippetCode { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public string Content { get; set; }
public SnippetScope Scope { get; set; }
public string Category { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime UpdatedAt { get; set; }
}
public class TemplateVariable
{
public Guid Id { get; set; }
public Guid TemplateId { get; set; }
public string Name { get; set; }
public string Description { get; set; }
public bool Required { get; set; }
public string DefaultValue { get; set; }
public string ValidationRegex { get; set; }
public DateTime CreatedAt { get; set; }
public virtual Template Template { get; set; }
}
Template Service Implementation
public interface ITemplateService
{
Task<Template> CreateTemplateAsync(CreateTemplateRequest request);
Task<Template> CreateOverrideAsync(string templateCode, Guid userId, CreateOverrideRequest request);
Task<RenderedTemplate> RenderTemplateAsync(string templateCode, string locale, Dictionary<string, object> variables);
Task<IEnumerable<Template>> ListTemplatesAsync(TemplateListOptions options);
}
public class TemplateService : ITemplateService
{
private readonly ITemplateRepository _templateRepo;
private readonly ITemplateRenderer _renderer;
private readonly ISnippetService _snippetService;
private readonly ICacheService _cacheService;
private readonly ILogger<TemplateService> _logger;
public async Task<IEnumerable<Template>> ListTemplatesAsync(TemplateListOptions options)
{
// Combine system templates and user templates using UNION ALL approach
var templates = await _templateRepo.GetTemplatesAsync(sql: @"
SELECT t.*
FROM Templates t
WHERE t.Type = 'System'
AND t.Status = 'Active'
UNION ALL
SELECT t.*
FROM Templates t
WHERE t.Type = 'Custom'
AND t.OwnerId = @UserId
AND t.Status = 'Active'
ORDER BY t.Category, t.Name",
new { options.UserId });
return templates;
}
public async Task<Template> CreateTemplateAsync(CreateTemplateRequest request)
{
// Validate template code format
if (!IsValidTemplateCode(request.TemplateCode))
throw new ValidationException("Invalid template code format");
var template = new Template
{
Id = Guid.NewGuid(),
TemplateCode = request.TemplateCode,
Category = request.Category,
Name = request.Name,
Type = request.Type,
// ... other properties
};
// Add localizations
foreach (var localization in request.Content)
{
template.Localizations.Add(new TemplateLocalization
{
Id = Guid.NewGuid(),
Locale = localization.Key,
Content = localization.Value.Body,
SubjectTemplate = localization.Value.Subject
});
}
// Add variables
foreach (var variable in request.Variables)
{
template.Variables.Add(new TemplateVariable
{
Id = Guid.NewGuid(),
Name = variable.Name,
Required = variable.Required,
// ... other properties
});
}
await _templateRepo.CreateAsync(template);
await _cacheService.InvalidateTemplateCache(template.TemplateCode);
return template;
}
public async Task<RenderedTemplate> RenderTemplateAsync(string templateCode, string locale, Dictionary<string, object> variables)
{
// Try get from cache
var cacheKey = GetTemplateCacheKey(templateCode, locale, variables);
var cached = await _cacheService.GetAsync<RenderedTemplate>(cacheKey);
if (cached != null) return cached;
// Get template with override check
var template = await GetEffectiveTemplateAsync(templateCode);
if (template == null)
throw new TemplateNotFoundException(templateCode);
// Get localized content
var localization = await GetEffectiveLocalizationAsync(template, locale);
if (localization == null)
throw new LocaleNotFoundException(locale);
// Process snippets
var processedContent = await ProcessSnippetsAsync(localization.Content);
// Validate variables
ValidateRequiredVariables(template, variables);
// Render template
var rendered = await _renderer.RenderAsync(processedContent, variables);
var renderedSubject = await _renderer.RenderAsync(localization.SubjectTemplate, variables);
var result = new RenderedTemplate
{
Subject = renderedSubject,
Body = rendered
};
// Cache the result
await _cacheService.SetAsync(cacheKey, result, TimeSpan.FromMinutes(30));
return result;
}
private async Task<string> ProcessSnippetsAsync(string content)
{
// Find all snippet references like {{snippet:code}}
var snippetRegex = new Regex(@"\{\{snippet:([\w-]+)\}\}");
var matches = snippetRegex.Matches(content);
foreach (Match match in matches)
{
var snippetCode = match.Groups[1].Value;
var snippet = await _snippetService.GetSnippetAsync(snippetCode);
if (snippet != null)
{
content = content.Replace(match.Value, snippet.Content);
}
}
return content;
}
}
Snippet Service Implementation
public interface ISnippetService
{
Task<TemplateSnippet> GetSnippetAsync(string snippetCode);
Task<TemplateSnippet> CreateSnippetAsync(CreateSnippetRequest request);
Task<IEnumerable<TemplateSnippet>> ListSnippetsAsync(SnippetScope? scope = null, string category = null);
}
public class SnippetService : ISnippetService
{
private readonly ISnippetRepository _snippetRepo;
private readonly ICacheService _cacheService;
private readonly ILogger<SnippetService> _logger;
public async Task<TemplateSnippet> GetSnippetAsync(string snippetCode)
{
// Try get from cache
var cacheKey = $"snippet:{snippetCode}";
var cached = await _cacheService.GetAsync<TemplateSnippet>(cacheKey);
if (cached != null) return cached;
// Get from database
var snippet = await _snippetRepo.GetByCodeAsync(snippetCode);
if (snippet != null)
{
await _cacheService.SetAsync(cacheKey, snippet, TimeSpan.FromHours(1));
}
return snippet;
}
}
Cache Service Implementation
public interface ICacheService
{
Task<T> GetAsync<T>(string key);
Task SetAsync<T>(string key, T value, TimeSpan expiry);
Task InvalidateAsync(string key);
}
public class RedisCacheService : ICacheService
{
private readonly IConnectionMultiplexer _redis;
private readonly ILogger<RedisCacheService> _logger;
public async Task<T> GetAsync<T>(string key)
{
var db = _redis.GetDatabase();
var value = await db.StringGetAsync(key);
if (!value.HasValue) return default;
return JsonSerializer.Deserialize<T>(value);
}
public async Task SetAsync<T>(string key, T value, TimeSpan expiry)
{
var db = _redis.GetDatabase();
var serialized = JsonSerializer.Serialize(value);
await db.StringSetAsync(key, serialized, expiry);
}
}
Usage Example
// Creating a new template
var request = new CreateTemplateRequest
{
TemplateCode = "notification_welcome_v1",
Category = "notification",
Name = "Welcome Email",
Type = TemplateType.System,
Content = new Dictionary<string, LocalizedContent>
{
["en-US"] = new LocalizedContent
{
Subject = "Welcome to {{company}}!",
Body = @"Dear {{name}},
{{snippet:common_greeting}}
Welcome to {{company}}!
{{snippet:footer}}"
}
},
Variables = new[]
{
new TemplateVariable { Name = "company", Required = true },
new TemplateVariable { Name = "name", Required = true }
}
};
var template = await _templateService.CreateTemplateAsync(request);
// Rendering a template
var variables = new Dictionary<string, object>
{
["company"] = "ACME Corp",
["name"] = "John Doe"
};
var rendered = await _templateService.RenderTemplateAsync(
"notification_welcome_v1",
"en-US",
variables
);