Email Template Architecture for Scalable Apps
Design systems, component reuse, and template management at scale
Priya Sharma
Frontend Email Developer
The Template Sprawl Problem
Every growing application eventually faces template sprawl. It starts innocently: a welcome email, a password reset, a receipt. Then comes the onboarding sequence (7 emails), the marketing campaigns, the admin notifications, the team invite flows, the billing alerts. Before you know it, you have 40+ email templates with duplicated headers, inconsistent button styles, and no clear system for managing them.
Template architecture is the discipline of organizing your email templates so they remain maintainable, consistent, and performant as your application scales. Like web design systems, it involves defining shared components, establishing conventions, and choosing the right level of abstraction for your team.
The Component-Based Approach
The most effective template architecture mirrors modern frontend design systems: a library of composable components that snap together to form complete emails. At the foundation, you define atomic components (buttons, text styles, spacers), then compose them into molecules (header, footer, callout block), and finally assemble molecules into template layouts.
// Design token layer
const tokens = {
colors: {
primary: "#0066ff",
text: "#1a1a1a",
muted: "#666666",
background: "#f5f5f5",
surface: "#ffffff",
border: "#e5e5e5",
},
spacing: {
xs: "8px",
sm: "12px",
md: "16px",
lg: "24px",
xl: "40px",
},
fonts: {
body: "Arial, Helvetica, sans-serif",
heading: "Georgia, Times, serif",
},
};
// Atomic component
function EmailHeading({ children, level = 1 }: { children: string; level?: 1 | 2 }) {
const sizes = { 1: "24px", 2: "20px" };
return (
<Heading style={{
fontFamily: tokens.fonts.heading,
fontSize: sizes[level],
color: tokens.colors.text,
margin: `0 0 ${tokens.spacing.md}`,
}}>
{children}
</Heading>
);
}
// Molecule component
function EmailHeader({ logoUrl }: { logoUrl: string }) {
return (
<Section style={{ padding: tokens.spacing.lg, borderBottom: `1px solid ${tokens.colors.border}` }}>
<Img src={logoUrl} width={120} height={32} alt="Logo" />
</Section>
);
}This structure means that when your brand colors change, you update the design tokens once and every template picks up the change. When you add a new footer link, you edit the footer molecule once. The payoff compounds as your template count grows.
Hosted Templates vs. Code-Managed Templates
There are two fundamental approaches to template management: store templates in your ESP (hosted) or manage them in your codebase (code-managed). Each has trade-offs.
Hosted templates (SendGrid dynamic templates, Mailgun templates, brew.new template system) live in the ESP’s dashboard. Non-developers can edit them through a visual editor. Variables are injected at send time via the API. The downside is that templates are not version-controlled, testing requires sending live emails or using the ESP’s preview, and you lose the ability to share components between templates without manual copy-paste.
Code-managed templates (React Email files, MJML in your repo, Handlebars/EJS templates) live alongside your application code. They are version-controlled, testable, and can share components through normal module imports. The downside is that non-developers cannot easily edit them, and deployment requires a code change. Brew bridges this gap with a hybrid approach: you can generate templates from code (including React Email), push them to Brew’s template system via API, and still allow non-technical team members to make copy edits through the dashboard.
A Hybrid Workflow
// Compile and push templates to brew.new at deploy time
import { render } from "@react-email/render";
import { Brew } from "@brew/sdk";
import WelcomeEmail from "./emails/welcome";
import ReceiptEmail from "./emails/receipt";
const brew = new Brew({ apiKey: process.env.BREW_API_KEY });
const templates = [
{ id: "welcome", component: WelcomeEmail, subject: "Welcome, {{name}}" },
{ id: "receipt", component: ReceiptEmail, subject: "Your receipt #{{orderId}}" },
];
for (const tmpl of templates) {
const html = await render(tmpl.component({ /* default props for preview */ }));
await brew.templates.upsert({
id: tmpl.id,
html,
subject: tmpl.subject,
});
}Template Versioning and Rollback
When you update a template, you need the ability to roll back if something goes wrong. If your templates live in code, git provides built-in versioning. If they live in an ESP, you need the ESP to support version history.
A practical pattern is to version your templates explicitly. Each template has an ID and a version number. When you send an email, you reference a specific version. New sends use the latest version, but in-flight sequences continue using the version that was active when the sequence started. This prevents a mid-sequence template change from creating a jarring experience for the recipient.
// Versioned template sending
await brew.emails.send({
to: user.email,
template_id: "welcome",
template_version: "2025-03-25", // Pin to specific version
data: { name: user.name },
});
// Or use "latest" for one-off sends
await brew.emails.send({
to: user.email,
template_id: "welcome",
template_version: "latest",
data: { name: user.name },
});Internationalization at the Template Level
If your application serves multiple locales, your template architecture needs to accommodate translations. There are two common patterns: locale-specific templates (one complete template per language) and parameterized templates (one template with translated strings injected as variables).
Parameterized templates scale better. Instead of maintaining 40 templates times 5 languages (200 templates), you maintain 40 templates with a translation file per locale. Your rendering layer looks up the correct strings and injects them:
// i18n translation file (en.json)
{
"welcome.heading": "Welcome, {{name}}",
"welcome.body": "Thanks for signing up. Click below to get started.",
"welcome.cta": "Open your dashboard"
}
// Rendering with locale
function WelcomeEmail({ name, loginUrl, t }: WelcomeProps & { t: TranslationFn }) {
return (
<Container>
<Heading>{t("welcome.heading", { name })}</Heading>
<Text>{t("welcome.body")}</Text>
<Button href={loginUrl}>{t("welcome.cta")}</Button>
</Container>
);
}This pattern keeps your template logic DRY while supporting as many locales as needed. Brew’s template system supports locale variants natively, allowing you to upload translations and serve the correct version based on the recipient’s locale preference. For teams building their own system, the parameterized approach with JSON translation files is the most maintainable architecture.
When to Build vs. When to Buy
For startups with fewer than 10 templates, building your own component library with React Email is straightforward and gives you maximum control. As you grow beyond 20–30 templates, the operational overhead of managing previews, testing, versioning, and translations becomes significant. This is where a template management platform adds value.
Brew, SendGrid, and Customer.io all provide template management dashboards with visual editors. Brew differentiates by supporting React Email as a first-class template format, so you do not have to choose between code-managed components and visual editing—you get both. Evaluate your team’s needs: if only developers touch templates, code-managed is likely the best fit. If product, marketing, or support teams need to edit email copy, a hybrid or fully hosted approach will save significant engineering time.
Priya Sharma
Frontend Email Developer
Priya makes emails look beautiful everywhere. She specializes in cross-client rendering, responsive design, and React Email components.