Email Template Management System — System Design

 


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_v1
  • invoice_reminder_v2
  • marketing_newsletter_v1

For user-specific overrides, append the user identifier:

{category}_{purpose}_{version}_user_{userId}

Localization Strategy

  1. Locale Identification:

    • Use standard locale codes (e.g., 'en-US', 'es-ES', 'fr-FR')
    • Support fallback chains (e.g., 'fr-CA' → 'fr' → default)
  2. Content Storage:

    -- Template content stored per locale
    template_content = {
      "en-US": "Welcome to {{company}}!",
      "es-ES": "¡Bienvenido a {{company}}!",
      "fr-FR": "Bienvenue à {{company}}!"
    }
    
  3. 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:

  1. Original system template remains unchanged
  2. User-specific version is stored separately
  3. Override is only visible to the creating user
  4. System updates don't affect user overrides
  5. 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

  1. Request Processing:

    • Validate template code and locale
    • Check user permissions
    • Validate required variables
  2. Template Resolution:

    • Check for user override
    • Fallback to system template
    • Resolve locale with fallback chain
  3. Snippet Processing:

    • Load referenced snippets
    • Process snippet variables
    • Cache rendered snippets
  4. Variable Substitution:

    • Validate all required variables
    • Apply default values where needed
    • Process nested variables
  5. 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

  1. Access Control:

    • Role-based access for template management
    • User-level permissions for overrides
    • API key authentication for rendering
  2. Content Security:

    • Template content validation
    • Variable escape/sanitization
    • HTML sanitization for rich templates
  3. Rate Limiting:

    • Per-user template creation limits
    • Rendering rate limits
    • API usage quotas

Monitoring and Maintenance

  1. Metrics:

    • Template render times
    • Cache hit rates
    • Template usage statistics
    • Error rates and types
  2. 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
);

Vikash Chauhan

C# & .NET experienced Software Engineer with a demonstrated history of working in the computer software industry.

Post a Comment

Previous Post Next Post

Contact Form