Chapter 13: Technology Architecture
Introduction: Principles Over Products
"Software development has evolved dramatically from text-based displays on wired office networks to today's mobile, cloud-based reality. Technologies change, but good architecture principles endure."
A warning before we begin: This chapter describes technology as of 2025. By the time you read this, some of these specific technologies may be obsolete. React might be replaced. PostgreSQL might have a successor. Node.js might be old news.
That's okay.
This chapter teaches you how to think about architecture, not just what tools to use today. We'll cover: - Timeless principles (separation of concerns, loose coupling, etc.) - Current best practices (as of 2025) - How to evaluate new technologies when they arise - Architecture patterns that transcend specific tech
The goal: In 10 years, when everything has changed, you'll still be able to build excellent systems by applying these principles to whatever technologies exist then.
Section 1: Timeless Architecture Principles
These principles have been true since the 1970s and will remain true in 2050:
Principle 1: Separation of Concerns
What it means: Different parts of your system should handle different responsibilities. Don't mix presentation logic with business logic with data access.
Why it matters: When business rules change (and they will), you shouldn't have to rewrite your UI. When you switch databases (and you might), you shouldn't have to change business logic.
Classic pattern: Three-tier architecture - Presentation Layer (UI): What users see - Business Logic Layer (Application): Rules, workflows, validation - Data Access Layer (Persistence): Database operations
Example of GOOD separation:
// Presentation Layer (React component)
function ExpenseForm({ onSubmit }) {
const [amount, setAmount] = useState('');
const [error, setError] = useState(null);
const handleSubmit = async () => {
try {
await onSubmit({ amount: parseFloat(amount) });
} catch (err) {
setError(err.message);
}
};
return (
<form onSubmit={handleSubmit}>
<input value={amount} onChange={e => setAmount(e.target.value)} />
{error && <span>{error}</span>}
<button>Submit</button>
</form>
);
}
// Business Logic Layer (service)
class ExpenseService {
async submitExpense(expense, employeeLevel) {
// Business rule: Check against policy limits
this.validateAmount(expense.amount, employeeLevel);
// Save to database
const saved = await this.expenseRepository.save(expense);
// Trigger workflow
await this.workflowEngine.start('expense_approval', saved);
return saved;
}
validateAmount(amount, employeeLevel) {
const limit = this.getPolicyLimit(employeeLevel);
if (amount > limit) {
throw new Error(`Amount exceeds ${employeeLevel} limit of $${limit}`);
}
}
}
// Data Access Layer (repository)
class ExpenseRepository {
async save(expense) {
return await db.expenses.insert({
amount: expense.amount,
category: expense.category,
created_at: new Date()
});
}
}
Example of BAD mixing:
// Everything in one place - DON'T DO THIS
function ExpenseForm() {
const [amount, setAmount] = useState('');
const handleSubmit = async () => {
// Business logic mixed with UI
if (amount > 1000) {
alert('Too high!');
return;
}
// Database access mixed with UI
await fetch('/api/db', {
method: 'POST',
body: JSON.stringify({
sql: `INSERT INTO expenses VALUES (${amount})` // SQL injection!
})
});
};
return <form onSubmit={handleSubmit}>...</form>;
}
How to apply: When writing code, ask "Which layer am I in?" Keep each layer pure.
Why this survives technology changes: Whether you're using React in 2025, WebAssembly components in 2030, or neural interfaces in 2040, you'll still need to separate presentation from business logic.
Principle 2: Loose Coupling
What it means: Components should depend on abstractions, not concrete implementations. When one part changes, other parts shouldn't break.
Why it matters: Technology changes. Today's best database might be tomorrow's legacy. If your business logic is tightly coupled to PostgreSQL, you can't switch to whatever comes next.
Pattern: Dependency Injection + Interface/Contract
Example of GOOD loose coupling:
// Define interface (contract)
interface IEmailService {
send(to: string, subject: string, body: string): Promise<void>;
}
// Implementation 1: SendGrid (current)
class SendGridEmailService implements IEmailService {
async send(to: string, subject: string, body: string) {
await sendgrid.send({ to, subject, body });
}
}
// Implementation 2: AWS SES (future)
class SESEmailService implements IEmailService {
async send(to: string, subject: string, body: string) {
await ses.sendEmail({ to, subject, body });
}
}
// Business logic depends on interface, not implementation
class PermitService {
constructor(private emailService: IEmailService) {}
async approvePermit(permit) {
permit.status = 'approved';
await this.permitRepository.save(permit);
// Doesn't care if it's SendGrid or SES
await this.emailService.send(
permit.applicantEmail,
'Permit Approved',
`Your permit #${permit.id} has been approved.`
);
}
}
// Dependency injection: Choose implementation at runtime
const emailService = new SendGridEmailService(); // or SESEmailService
const permitService = new PermitService(emailService);
Example of BAD tight coupling:
// Business logic directly depends on SendGrid
class PermitService {
async approvePermit(permit) {
permit.status = 'approved';
await this.permitRepository.save(permit);
// Tightly coupled to SendGrid - can't switch easily
await sendgrid.send({
to: permit.applicantEmail,
subject: 'Permit Approved',
text: `Your permit #${permit.id} has been approved.`
});
}
}
How to apply: Depend on interfaces/contracts, not concrete classes. Inject dependencies rather than hard-coding them.
Why this survives technology changes: The interface stays the same even when implementations change. In 2030, you might have a QuantumEmailService, but it still implements IEmailService.
Principle 3: Single Source of Truth
What it means: Every piece of data should have ONE authoritative source. Don't duplicate data across systems unless you have a very good reason.
Why it matters: When data lives in multiple places, it gets out of sync. Then you don't know which version is correct.
Pattern: Master Data Management + API Access
Example of GOOD single source:
// Customer data lives ONLY in CustomerService
class CustomerService {
async getCustomer(customerId) {
return await db.customers.findById(customerId);
}
async updateCustomer(customerId, updates) {
const customer = await db.customers.findById(customerId);
return await db.customers.update(customerId, { ...customer, ...updates });
}
}
// OrderService doesn't store customer data
class OrderService {
constructor(private customerService: CustomerService) {}
async createOrder(customerId, items) {
// Fetch customer data when needed
const customer = await this.customerService.getCustomer(customerId);
return await db.orders.insert({
customer_id: customerId, // Store ID only, not customer data
customer_email: customer.email, // Only if needed for performance
items: items
});
}
}
Example of BAD duplication:
// Customer data duplicated everywhere - DON'T DO THIS
await db.customers.update(customerId, { email: newEmail });
await db.orders.updateMany({ customer_id: customerId }, { customer_email: newEmail });
await db.invoices.updateMany({ customer_id: customerId }, { customer_email: newEmail });
// Now they're out of sync because one update failed...
Exceptions where duplication is okay: - Caching (temporary, invalidated when source changes) - Reporting/Analytics (read-only copies for performance) - Event Sourcing (every change is an event, replayed to build state)
How to apply: Before storing data, ask "Am I the authoritative source for this?" If no, fetch it when needed or cache temporarily.
Why this survives technology changes: Distributed systems are hard in any era. Single source of truth will always be a winning strategy.
Principle 4: Fail Gracefully
What it means: When something breaks (and it will), don't take down the whole system. Isolate failures, provide fallbacks, keep core functionality working.
Why it matters: External APIs go down. Databases timeout. Network connections fail. Your system should degrade gracefully, not crash completely.
Pattern: Circuit Breaker + Fallbacks
Example of GOOD failure handling:
class AddressValidator {
private failureCount = 0;
private circuitOpen = false;
private lastFailureTime = 0;
async validateAddress(address) {
// Circuit breaker: Stop trying if it's failing
if (this.circuitOpen) {
if (Date.now() - this.lastFailureTime < 60000) {
// Circuit still open, use fallback
return this.fallbackValidation(address);
} else {
// Try again after cooldown
this.circuitOpen = false;
this.failureCount = 0;
}
}
try {
const result = await uspsApi.validate(address);
this.failureCount = 0; // Reset on success
return result;
} catch (error) {
this.failureCount++;
this.lastFailureTime = Date.now();
if (this.failureCount >= 3) {
this.circuitOpen = true;
console.error('USPS API circuit breaker opened');
}
// Use fallback validation
return this.fallbackValidation(address);
}
}
fallbackValidation(address) {
// Basic validation when USPS API is down
return {
valid: /^\d{5}(-\d{4})?$/.test(address.zipCode),
warnings: ['Address not verified with USPS (service unavailable)']
};
}
}
Example of BAD failure handling:
// Crashes entire form when API is down - DON'T DO THIS
async function submitForm(data) {
// If USPS is down, this throws and breaks everything
const validatedAddress = await uspsApi.validate(data.address);
await saveToDatabase(data);
return 'Success';
}
How to apply: Wrap external calls in try-catch. Implement circuit breakers. Provide fallback behavior. Never let external failures crash your system.
Why this survives technology changes: Systems will always have failures. Graceful degradation will always be better than total failure.
Principle 5: Design for Change
What it means: Assume everything will change. Business rules, regulations, technology, integrations. Build systems that can evolve.
Why it matters: Software has evolved from text-based terminals to mobile apps. Tomorrow might bring AR glasses or brain interfaces. Your architecture should accommodate change.
Pattern: Plugin Architecture + Configuration over Code
Example of GOOD design for change:
// Validation rules as configuration (can change without code deploy)
const validationRules = {
expense_amount: {
type: 'number',
min: 0,
max: (context) => context.employeeLevel === 'executive' ? 10000 : 1000,
message: 'Amount exceeds policy limit'
},
expense_category: {
type: 'enum',
values: ['meals', 'travel', 'lodging', 'other'],
message: 'Invalid expense category'
}
};
// Generic validator that reads configuration
class Validator {
validate(fieldName, value, context) {
const rule = validationRules[fieldName];
if (!rule) return { valid: true };
if (rule.type === 'number') {
const max = typeof rule.max === 'function'
? rule.max(context)
: rule.max;
if (value > max) {
return { valid: false, message: rule.message };
}
}
// More validation logic...
return { valid: true };
}
}
Example of BAD hard-coded logic:
// Business rules hard-coded - requires code change for every policy update
function validateExpense(amount, employeeLevel) {
if (employeeLevel === 'staff' && amount > 1000) {
throw new Error('Staff limit is $1,000');
}
if (employeeLevel === 'manager' && amount > 5000) {
throw new Error('Manager limit is $5,000');
}
if (employeeLevel === 'executive' && amount > 10000) {
throw new Error('Executive limit is $10,000');
}
}
// When policy changes, you have to edit code, test, deploy...
How to apply: - Put business rules in configuration/database, not code - Use plugin architectures for extensibility - Version your APIs (v1, v2) for backward compatibility - Feature flags for gradual rollouts
Why this survives technology changes: Change is the only constant. Systems designed for change will outlive rigid systems every time.
Section 2: Current Best Practices (2025)
Now let's discuss specific technologies as of 2025. Remember: these are TODAY's best practices. In 10 years, specific tools will change, but the architectural patterns won't.
Frontend Architecture (2025)
Component-Based UI Frameworks
Why component-based: Reusability, encapsulation, testability.
Current options (2025): - React (most popular, huge ecosystem) - Vue (easier learning curve, great for medium projects) - Svelte (compiler-based, excellent performance) - Solid (fine-grained reactivity, React-like syntax)
Choosing between them: - Large team, need ecosystem? → React - Medium team, faster development? → Vue - Performance critical? → Svelte or Solid - Small team, rapid prototyping? → Vue
But remember: In 2030, these might all be replaced by something new. The principle (component-based architecture) survives.
Example React component (Pattern 3: Inline Validation):
import { useState } from 'react';
interface EmailFieldProps {
value: string;
onChange: (value: string) => void;
label?: string;
}
export function EmailField({ value, onChange, label = 'Email' }: EmailFieldProps) {
const [error, setError] = useState<string | null>(null);
const validateEmail = (email: string): string | null => {
if (!email) return 'Email is required';
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return 'Please enter a valid email address';
}
return null;
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value;
setError(validateEmail(newValue));
onChange(newValue);
};
return (
<div className="field">
<label htmlFor="email">{label}</label>
<input
id="email"
type="email"
value={value}
onChange={handleChange}
className={error ? 'error' : ''}
aria-invalid={!!error}
aria-describedby={error ? 'email-error' : undefined}
/>
{error && (
<span id="email-error" className="error-message" role="alert">
{error}
</span>
)}
</div>
);
}
State Management
Why you need it: Complex apps have shared state. Prop drilling (passing props through many levels) becomes unmaintainable.
Current options (2025): - React Context + useReducer (built-in, good for simple apps) - Zustand (minimal, easy to use) - Redux Toolkit (most mature, good for large apps) - Jotai / Recoil (atomic state management)
Architectural principle: Regardless of tool, separate state management from UI rendering.
Example with Zustand:
// Store definition (state management layer)
import create from 'zustand';
interface FormState {
formData: Record<string, any>;
errors: Record<string, string>;
setField: (field: string, value: any) => void;
setError: (field: string, error: string) => void;
validateField: (field: string) => void;
}
const useFormStore = create<FormState>((set, get) => ({
formData: {},
errors: {},
setField: (field, value) => {
set(state => ({
formData: { ...state.formData, [field]: value }
}));
get().validateField(field);
},
setError: (field, error) => {
set(state => ({
errors: { ...state.errors, [field]: error }
}));
},
validateField: (field) => {
const value = get().formData[field];
const error = validators[field]?.(value) || null;
get().setError(field, error);
}
}));
// Component (presentation layer)
function FormField({ fieldName, label }: { fieldName: string, label: string }) {
const { formData, errors, setField } = useFormStore();
return (
<div>
<label>{label}</label>
<input
value={formData[fieldName] || ''}
onChange={e => setField(fieldName, e.target.value)}
/>
{errors[fieldName] && <span>{errors[fieldName]}</span>}
</div>
);
}
Backend Architecture (2025)
API-First Design
Why API-first: Frontend and backend evolve independently. Multiple clients (web, mobile, partners) consume same API.
Current options (2025): - REST (simple, widely understood, good default) - GraphQL (flexible queries, good for complex data) - tRPC (TypeScript RPC, great for full-stack TypeScript) - gRPC (binary protocol, excellent for microservices)
Architectural principle: Regardless of protocol, expose business logic through well-defined APIs, not database access.
Example REST API (Node.js + Express):
import express from 'express';
import { z } from 'zod';
const app = express();
// Input validation schema (Pattern 6: Domain-Aware)
const PermitApplicationSchema = z.object({
propertyAddress: z.string().min(1),
deckSize: z.number().positive(),
deckHeight: z.number().positive(),
contractorLicense: z.string().regex(/^[A-Z]{2}-\d{6}$/)
});
// Route handler
app.post('/api/permits', async (req, res) => {
try {
// Validate input
const data = PermitApplicationSchema.parse(req.body);
// Business logic (injected service)
const permitService = req.app.get('permitService');
const result = await permitService.submitApplication(data, req.user);
// Return result
res.status(201).json(result);
} catch (error) {
if (error instanceof z.ZodError) {
res.status(400).json({ errors: error.errors });
} else {
res.status(500).json({ error: 'Internal server error' });
}
}
});
// Business logic (separate from API layer)
class PermitService {
constructor(
private permitRepository: IPermitRepository,
private zoningService: IZoningService,
private workflowEngine: IWorkflowEngine
) {}
async submitApplication(data: PermitApplicationInput, user: User) {
// Check zoning compliance
const zoning = await this.zoningService.checkCompliance(data);
if (!zoning.compliant) {
throw new BusinessRuleError('Zoning violation: ' + zoning.reason);
}
// Save application
const permit = await this.permitRepository.create({
...data,
applicantId: user.id,
status: 'submitted',
submittedAt: new Date()
});
// Start approval workflow
await this.workflowEngine.start('permit_approval', permit);
return permit;
}
}
Database Layer
Why abstraction matters: Databases change. Your business logic shouldn't know if you're using PostgreSQL or whatever replaces it in 2030.
Current options (2025): - PostgreSQL (excellent relational DB, JSONB for flexibility) - MongoDB (document store, good for flexible schemas) - SQLite (embedded, great for local-first apps)
ORMs (Object-Relational Mapping): - Prisma (TypeScript-first, excellent DX) - TypeORM (mature, feature-rich) - Drizzle (lightweight, SQL-like)
Architectural principle: Repository pattern. Business logic talks to repositories, repositories talk to database.
Example with Prisma + Repository Pattern:
// Domain entity (business logic layer)
interface Permit {
id: string;
propertyAddress: string;
deckSize: number;
status: 'submitted' | 'under_review' | 'approved' | 'denied';
submittedAt: Date;
}
// Repository interface (abstraction)
interface IPermitRepository {
create(data: CreatePermitInput): Promise<Permit>;
findById(id: string): Promise<Permit | null>;
update(id: string, updates: Partial<Permit>): Promise<Permit>;
findByStatus(status: string): Promise<Permit[]>;
}
// Repository implementation (Prisma)
class PrismaPermitRepository implements IPermitRepository {
constructor(private prisma: PrismaClient) {}
async create(data: CreatePermitInput): Promise<Permit> {
return await this.prisma.permit.create({
data: {
propertyAddress: data.propertyAddress,
deckSize: data.deckSize,
status: 'submitted',
submittedAt: new Date()
}
});
}
async findById(id: string): Promise<Permit | null> {
return await this.prisma.permit.findUnique({
where: { id }
});
}
async update(id: string, updates: Partial<Permit>): Promise<Permit> {
return await this.prisma.permit.update({
where: { id },
data: updates
});
}
async findByStatus(status: string): Promise<Permit[]> {
return await this.prisma.permit.findMany({
where: { status }
});
}
}
Why this survives technology changes: If you switch from Prisma to something else, you only rewrite the repository implementation, not your business logic.
Integration Architecture (2025)
Pattern 21: External Data Integration and Pattern 22: Real-Time Lookup require external API calls.
Current best practices (2025): - HTTP clients: axios, fetch, got - Rate limiting: bottleneck, rate-limiter-flexible - Caching: Redis, in-memory LRU - Retry logic: axios-retry, retry-axios
Architectural principle: Isolate external integrations behind adapters. Never let external API structures leak into your business logic.
Example: Address Validation Integration:
// Adapter interface (your domain)
interface IAddressValidator {
validate(address: Address): Promise<AddressValidationResult>;
}
// USPS implementation (current)
class USPSAddressValidator implements IAddressValidator {
private client: AxiosInstance;
private cache: Map<string, AddressValidationResult>;
constructor() {
this.client = axios.create({
baseURL: 'https://api.usps.com',
timeout: 5000,
headers: { 'Authorization': `Bearer ${process.env.USPS_API_KEY}` }
});
this.cache = new Map();
}
async validate(address: Address): Promise<AddressValidationResult> {
const cacheKey = this.getCacheKey(address);
// Check cache first
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey)!;
}
try {
// Call USPS API
const response = await this.client.post('/validate', {
street: address.street,
city: address.city,
state: address.state,
zipCode: address.zipCode
});
// Map USPS response to our domain model
const result: AddressValidationResult = {
valid: response.data.valid,
correctedAddress: response.data.valid ? {
street: response.data.deliveryAddress,
city: response.data.city,
state: response.data.state,
zipCode: response.data.zip5 + '-' + response.data.zip4
} : null,
warnings: response.data.warnings || []
};
// Cache result
this.cache.set(cacheKey, result);
return result;
} catch (error) {
// Circuit breaker could go here
throw new ExternalServiceError('USPS validation failed', error);
}
}
private getCacheKey(address: Address): string {
return `${address.street}|${address.city}|${address.state}|${address.zipCode}`;
}
}
// Google Maps implementation (alternative)
class GoogleAddressValidator implements IAddressValidator {
async validate(address: Address): Promise<AddressValidationResult> {
// Different API, same interface
// ...
}
}
// Business logic doesn't care which implementation
class PermitService {
constructor(private addressValidator: IAddressValidator) {}
async submitApplication(data: PermitInput) {
// Validate address - works with USPS or Google or whatever
const validation = await this.addressValidator.validate(data.propertyAddress);
if (!validation.valid) {
throw new ValidationError('Invalid address: ' + validation.warnings.join(', '));
}
// Use corrected address if available
const address = validation.correctedAddress || data.propertyAddress;
// Continue processing...
}
}
Section 3: Evaluating New Technologies
Technologies change. In 10 years, React might be gone, PostgreSQL might have a successor. How do you evaluate new technologies as they arise?
Evaluation Framework
1. Does it solve a real problem? - Good: "This new framework reduces bundle size by 80%" - Bad: "This framework uses a cool new paradigm!" (no concrete benefit)
2. Is it mature enough? - Check GitHub stars, issues, last commit date - Look for production usage by known companies - Evaluate documentation quality - Rule of thumb: For production systems, wait until tech is 2+ years old
3. Does it align with architectural principles? - Does it encourage separation of concerns? - Can you migrate away if needed? - Does it lock you in?
4. What's the ecosystem? - Good TypeScript support? - Libraries for common tasks? - Active community?
5. What's the team fit? - Can your team learn it in reasonable time? - Hiring: Can you find developers who know it?
Example: Evaluating a new frontend framework (2025)
Let's say a new framework called "HyperView" claims to replace React:
✓ Problem solved: Claims 5x faster rendering
✓ Maturity: 3 years old, used by Stripe and Shopify
✓ Architecture: Component-based (good), but requires custom build tool (lock-in concern)
✓ Ecosystem: 500+ libraries, TypeScript support, active Discord
✗ Team fit: Unfamiliar syntax, 3-month learning curve
Decision: Maybe for new projects. Not worth migrating existing React apps.
Migration Strategy
When adopting new tech, migrate incrementally:
Phase 1: Proof of Concept (1-2 weeks) - Build one small feature with new tech - Evaluate developer experience - Measure performance
Phase 2: Parallel Running (1-2 months) - New features use new tech - Old features stay on old tech - Both run side-by-side
Phase 3: Gradual Migration (3-6 months) - Migrate one module at a time - Keep both systems working - Rollback plan if issues arise
Phase 4: Complete Transition (6-12 months) - Deprecate old tech - All new development on new tech - Update documentation and training
Never "big bang" rewrite everything. That's how projects fail.
Section 4: Architecture Patterns for the 25 Patterns
How do you architect systems to support all 25 patterns? Here's a reference architecture:
Frontend Architecture
┌─────────────────────────────────────────────────┐
│ User Interface Layer │
│ (React/Vue/Svelte components) │
│ │
│ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
│ │ Pattern 1 │ │ Pattern 3 │ │ Pattern 4│ │
│ │Progressive │ │ Inline │ │Contextual│ │
│ │ Disclosure │ │ Validation │ │ Help │ │
│ └────────────┘ └────────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ State Management Layer │
│ (Zustand/Redux/Context) │
│ │
│ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
│ │ Form State │ │ Validation │ │ User │ │
│ │ │ │ Rules │ │ Session │ │
│ └────────────┘ └────────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ API Client Layer │
│ (axios/fetch) │
│ │
│ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
│ │ Pattern │ │ Pattern │ │ Pattern │ │
│ │ 22 │ │ 21 │ │ 24 │ │
│ │ Real-Time │ │ External │ │Webhooks │ │
│ │ Lookup │ │ Data │ │ │ │
│ └────────────┘ └────────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘
Backend Architecture
┌─────────────────────────────────────────────────┐
│ API Layer │
│ (REST/GraphQL/tRPC endpoints) │
│ │
│ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
│ │ Input │ │ Output │ │ Error │ │
│ │ Validation │ │ Formatting │ │ Handling │ │
│ └────────────┘ └────────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Business Logic Layer │
│ (Services, Use Cases) │
│ │
│ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
│ │ Pattern 6 │ │ Pattern 14 │ │Pattern 23│ │
│ │ Domain- │ │Cross-Field │ │API-Driven│ │
│ │ Aware │ │ Validation │ │ Rules │ │
│ │ Validation │ │ │ │ │ │
│ └────────────┘ └────────────┘ └──────────┘ │
│ │
│ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
│ │ Pattern 7 │ │ Pattern 10 │ │Pattern 17│ │
│ │ Adaptive │ │ Semantic │ │State- │ │
│ │ Behavior │ │Suggestions │ │Aware │ │
│ └────────────┘ └────────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Workflow Orchestration Layer │
│ (Temporal/Camunda/Custom) │
│ │
│ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
│ │ Pattern 25 │ │ Pattern 20 │ │Pattern 24│ │
│ │Cross-System│ │ Scheduled │ │Webhooks │ │
│ │ Workflows │ │ Actions │ │ │ │
│ └────────────┘ └────────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Data Access Layer │
│ (Repositories, ORM) │
│ │
│ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
│ │ Pattern 18 │ │ Pattern 19 │ │Pattern 22│ │
│ │Audit Trail │ │ Version │ │Real-Time │ │
│ │ │ │ Control │ │ Lookup │ │
│ └────────────┘ └────────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Database Layer │
│ (PostgreSQL/MongoDB/etc.) │
└─────────────────────────────────────────────────┘
Integration Architecture
┌─────────────────────────────────────────────────┐
│ Your Application │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Integration Adapters │
│ (Isolate external dependencies) │
│ │
│ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
│ │ USPS │ │ SendGrid │ │ Stripe │ │
│ │ Address │ │ Email │ │ Payment │ │
│ │ Validator │ │ Service │ │ Gateway │ │
│ └────────────┘ └────────────┘ └──────────┘ │
│ │
│ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
│ │ Pattern │ │ Pattern │ │ Pattern │ │
│ │ 21 │ │ 22 │ │ 24 │ │
│ │ External │ │ Real-Time │ │Webhooks │ │
│ │ Data │ │ Lookup │ │ │ │
│ └────────────┘ └────────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ External Services │
│ (USPS, SendGrid, Stripe, etc.) │
└─────────────────────────────────────────────────┘
Section 5: Example Multi-Tenant Architecture
The DataPublisher platform demonstrates a successful multi-tenant architecture:
Components
┌─────────────────────────────────────────────────┐
│ Microsoft Word (Client) │
│ │
│ ┌──────────────────────────────────────────┐ │
│ │ Office.js Task Pane Add-in │ │
│ │ (Frontend: HTML/CSS/JavaScript) │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘
│ HTTP/REST
▼
┌─────────────────────────────────────────────────┐
│ Node.js Express Server │
│ (Backend: JavaScript) │
│ │
│ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
│ │ Template │ │ Data │ │ Document │ │
│ │ Engine │ │ Import │ │Generator │ │
│ │ │ │ (CSV/DB) │ │ │ │
│ └────────────┘ └────────────┘ └──────────┘ │
│ │
│ ┌────────────┐ ┌────────────┐ │
│ │ Email │ │ Reply │ │
│ │Distribution│ │ Tracking │ │
│ │(Power Auto)│ │ │ │
│ └────────────┘ └────────────┘ │
└─────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────┐
│ Microsoft SQL Server │
│ (Database: MSSQL) │
│ │
│ ┌────────────┐ ┌────────────┐ ┌──────────┐ │
│ │ Templates │ │ Data │ │ Audit │ │
│ │ │ │ │ │ Logs │ │
│ └────────────┘ └────────────┘ └──────────┘ │
└─────────────────────────────────────────────────┘
Key Architectural Decisions
1. Office.js Integration - Leverages existing Microsoft Word infrastructure - Users work in familiar tool - No separate UI to build/maintain
2. Template-Driven - Word documents are templates with merge fields - Configuration, not code, for each domain - Same platform serves homeschool, legal, healthcare
3. Data Source Flexibility - CSV import (simple, universal format) - Database connections (direct SQL queries) - API integrations (when needed)
4. Process Management - PM2 for Node.js process supervision - Automatic restart on crashes - Load balancing across multiple processes
5. Windows Server Deployment - On-premises or cloud Windows VMs - Familiar environment for IT departments - Integrates with Active Directory
Why This Architecture Works
Multi-tenancy at the data level: - One codebase serves all customers - Each customer has own database schema or tables - Templates and configurations per customer
Low barrier to entry: - Customers already have Microsoft Office - No browser requirements, no mobile app needed - Leverage existing Word skills
Flexible pricing: - License per user or per organization - Can charge based on document volume - SaaS or on-premises deployment
Section 6: Future-Proofing Your Architecture
How do you build systems that survive technology changes?
Strategy 1: Abstract External Dependencies
Never let external services' data structures leak into your codebase.
Example:
// DON'T: Stripe types throughout your code
async function processPayment(amount: number) {
const stripe = new Stripe(apiKey);
const paymentIntent = await stripe.paymentIntents.create({
amount: amount,
currency: 'usd'
});
// Now your code depends on Stripe's PaymentIntent structure
return paymentIntent;
}
// DO: Your own payment abstraction
interface Payment {
id: string;
amount: number;
status: 'pending' | 'succeeded' | 'failed';
createdAt: Date;
}
interface IPaymentProcessor {
processPayment(amount: number): Promise<Payment>;
}
class StripePaymentProcessor implements IPaymentProcessor {
async processPayment(amount: number): Promise<Payment> {
const stripe = new Stripe(apiKey);
const intent = await stripe.paymentIntents.create({
amount: amount,
currency: 'usd'
});
// Map Stripe's structure to your domain model
return {
id: intent.id,
amount: intent.amount,
status: this.mapStatus(intent.status),
createdAt: new Date(intent.created * 1000)
};
}
private mapStatus(stripeStatus: string): Payment['status'] {
switch (stripeStatus) {
case 'succeeded': return 'succeeded';
case 'processing': return 'pending';
case 'requires_payment_method': return 'pending';
default: return 'failed';
}
}
}
When Stripe is replaced in 2030: You only rewrite StripePaymentProcessor, not your entire codebase.
Strategy 2: Version Your APIs
External APIs (for clients):
// v1 API (initial)
app.get('/api/v1/permits/:id', getPermitV1);
// v2 API (new format, but v1 still works)
app.get('/api/v2/permits/:id', getPermitV2);
// Clients can migrate gradually
Internal modules:
// Use semantic versioning for internal packages
import { FormValidator } from '@myapp/validation@2.1.0';
Strategy 3: Feature Flags
Roll out new features gradually. If something breaks, turn it off without deploying.
const features = {
newCheckoutFlow: process.env.FEATURE_NEW_CHECKOUT === 'true',
aiSuggestions: process.env.FEATURE_AI === 'true'
};
function Checkout() {
if (features.newCheckoutFlow) {
return <NewCheckout />;
}
return <OldCheckout />;
}
Strategy 4: Monitoring & Observability
When things break (and they will), you need to know quickly.
Essential monitoring (2025 tools): - Errors: Sentry, Rollbar - Performance: New Relic, Datadog - Logs: Elasticsearch, CloudWatch - Uptime: Pingdom, UptimeRobot
Architectural principle: Log everything important, alert on anomalies.
Conclusion: Build for Tomorrow
Technologies change. React gives way to something new. PostgreSQL evolves or gets replaced. Node.js might fade away.
But the principles endure: - Separation of concerns - Loose coupling - Single source of truth - Fail gracefully - Design for change
Software has evolved from text-based terminals on wired networks to mobile, cloud, and real-time systems. Tomorrow it might be AR, VR, or brain interfaces.
Your architecture should accommodate this evolution.
Use today's best practices (React, Node.js, PostgreSQL) but don't marry them. Abstract dependencies. Version APIs. Build for change.
The patterns survive because they're about WHAT to do, not HOW to do it. Pattern 3 (Conversational Rhythm) will be relevant whether you're building with React in 2025 or QuantumUI in 2035.
Next chapter: Migration Strategies (how to replace legacy systems).
Further Reading
Software Architecture
Foundational Texts: - Fowler, M. (2002). Patterns of Enterprise Application Architecture. Addison-Wesley. - Layer architecture, domain logic, data source patterns - Richards, M., & Ford, N. (2020). Fundamentals of Software Architecture. O'Reilly Media. - Modern architectural patterns and trade-offs - Newman, S. (2021). Building Microservices (2nd ed.). O'Reilly Media. - Microservices architecture and decomposition strategies
Domain-Driven Design: - Evans, E. (2003). Domain-Driven Design. Addison-Wesley. - Bounded contexts, aggregates, domain events - Vernon, V. (2013). Implementing Domain-Driven Design. Addison-Wesley. - Practical DDD implementation patterns
Data Architecture
Databases: - Kleppmann, M. (2017). Designing Data-Intensive Applications. O'Reilly Media. - Essential reading—data models, replication, partitioning, transactions - Date, C. J. (2004). An Introduction to Database Systems (8th ed.). Addison-Wesley. - Relational database theory
NoSQL: - Sadalage, P. J., & Fowler, M. (2012). NoSQL Distilled. Addison-Wesley. - When and how to use NoSQL databases - Strauch, C., et al. (2011). "NoSQL Databases." http://www.christof-strauch.de/nosqldbs.pdf - Survey of NoSQL approaches
API Design
REST: - Fielding, R. T. (2000). "Architectural Styles and the Design of Network-based Software Architectures." Doctoral dissertation, UC Irvine. - Original REST thesis - https://www.ics.uci.edu/~fielding/pubs/dissertation/top.htm - Richardson, L., Amundsen, M., & Ruby, S. (2013). RESTful Web APIs. O'Reilly Media. - Practical REST API design
GraphQL: - GraphQL: https://graphql.org/ - Query language for APIs - Apollo: https://www.apollographql.com/ - GraphQL implementation platform
Event-Driven Architecture
Core Concepts: - Richardson, C. (2018). Microservices Patterns. Manning. - Event sourcing, CQRS, sagas - Stopford, B. (2018). Designing Event-Driven Systems. O'Reilly Media. - Stream processing and event-driven microservices - https://www.confluent.io/designing-event-driven-systems/
Messaging: - Hohpe, G., & Woolf, B. (2003). Enterprise Integration Patterns. Addison-Wesley. - Messaging patterns—definitive reference - https://www.enterpriseintegrationpatterns.com/
Cloud Architecture
Cloud Providers: - AWS Well-Architected Framework: https://aws.amazon.com/architecture/well-architected/ - Operational excellence, security, reliability, performance, cost - Azure Architecture Center: https://docs.microsoft.com/en-us/azure/architecture/ - Cloud design patterns and best practices - Google Cloud Architecture Framework: https://cloud.google.com/architecture/framework - System design guidance
Serverless: - Roberts, M. (2018). "Serverless Architectures." Martin Fowler blog. - https://martinfowler.com/articles/serverless.html - AWS Lambda: https://aws.amazon.com/lambda/ - Azure Functions: https://azure.microsoft.com/services/functions/
Security Architecture
Standards: - OWASP Top 10: https://owasp.org/www-project-top-ten/ - Most critical web application security risks - NIST Cybersecurity Framework: https://www.nist.gov/cyberframework - Risk management framework
Authentication/Authorization: - OAuth 2.0: https://oauth.net/2/ - Authorization framework - OpenID Connect: https://openid.net/connect/ - Identity layer on OAuth 2.0 - JWT (JSON Web Tokens): https://jwt.io/ - Stateless authentication tokens
Performance and Scalability
Optimization: - Gregg, B. (2020). Systems Performance (2nd ed.). Addison-Wesley. - Performance analysis and tuning - Allspaw, J., & Robbins, J. (2010). Web Operations. O'Reilly Media. - Running and maintaining web applications
Caching: - Redis: https://redis.io/ - In-memory data structure store - Memcached: https://memcached.org/ - Distributed memory caching
Infrastructure as Code
Tools: - Terraform: https://www.terraform.io/ - Infrastructure as code tool - Ansible: https://www.ansible.com/ - Configuration management and automation - Pulumi: https://www.pulumi.com/ - Modern infrastructure as code using real programming languages
Observability
Monitoring: - Beyer, B., et al. (2016). Site Reliability Engineering. O'Reilly Media. - Google SRE practices - https://sre.google/sre-book/table-of-contents/ - Prometheus: https://prometheus.io/ - Monitoring and alerting toolkit
Distributed Tracing: - OpenTelemetry: https://opentelemetry.io/ - Observability framework - Jaeger: https://www.jaegertracing.io/ - Distributed tracing system
Related Trilogy Content
- Volume 1, Chapter 8: Architecture of Domain-Specific Systems—architectural principles and patterns
- Volume 1, Chapter 2: Theoretical Foundations—foundational concepts for system design
- Volume 2, Chapter 4: What Makes Organizational Intelligence Possible—infrastructure for intelligent systems
- Volume 2, Chapter 2: From Static Output to Living Memory—understanding data architecture evolution
- Volume 3, Chapter 14: Migration Strategies—transitioning from legacy to modern architecture
- Volume 3, Chapter 12: Implementation Roadmap—planning your technology stack
- Volume 3, Patterns 21-25: Integration patterns for connecting systems