Design Principles
Core design principles that guide the system architecture including separation of concerns, type safety, security, performance, and scalability.
Design Principles
The system architecture is built on fundamental design principles that ensure maintainability, security, and scalability.
1. Separation of Concerns
Each layer and component has distinct, well-defined responsibilities to minimize coupling and maximize cohesion.
Layer Responsibilities
Presentation Layer (Client)
- UI Rendering: Display data and handle visual presentation
- User Interaction: Process user events and input
- Client State: Manage local component state
- Navigation: Handle routing and page transitions
// Good: Component focused on presentation
export function ProductCard({ product, onSelect }: ProductCardProps) {
return (
<div className="product-card" onClick={() => onSelect(product)}>
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p>{formatPrice(product.price)}</p>
</div>
)
}
// Avoid: Mixing data fetching with presentation
export function ProductCard({ productId }: { productId: string }) {
const [product, setProduct] = useState(null)
useEffect(() => {
// Don't fetch data in presentation components
fetchProduct(productId).then(setProduct)
}, [productId])
// ... rest of component
}
Business Logic Layer (Application)
- Data Processing: Transform and validate data
- Business Rules: Implement domain logic
- Integration: Coordinate between services
- Workflow Management: Orchestrate complex operations
// Good: Service handles business logic
export class ProductService {
async createProduct(productData: CreateProductRequest): Promise<Product> {
// Validate business rules
await this.validateProductData(productData)
// Apply business logic
const product = this.applyBusinessRules(productData)
// Persist data
return await this.productRepository.create(product)
}
private async validateProductData(data: CreateProductRequest) {
if (data.price <= 0) {
throw new BusinessError('Product price must be greater than zero')
}
const existingProduct = await this.productRepository.findBySku(data.sku)
if (existingProduct) {
throw new BusinessError('Product SKU already exists')
}
}
}
Data Layer
- Data Persistence: Store and retrieve data
- Data Integrity: Ensure data consistency
- Access Control: Implement security policies
- Performance: Optimize data access patterns
Component Separation
Container vs Presentational Components
// Container Component: Handles data and logic
export function ProductListContainer() {
const { products, loading, error } = useProducts()
const { user } = useAuth()
const handleProductSelect = (product: Product) => {
if (user.canViewProduct(product)) {
router.push(`/products/${product.id}`)
}
}
return (
<ProductList
products={products}
loading={loading}
error={error}
onProductSelect={handleProductSelect}
/>
)
}
// Presentational Component: Handles display only
export function ProductList({ products, loading, error, onProductSelect }: ProductListProps) {
if (loading) return <LoadingSpinner />
if (error) return <ErrorMessage message={error} />
return (
<div className="product-grid">
{products.map(product => (
<ProductCard
key={product.id}
product={product}
onSelect={onProductSelect}
/>
))}
</div>
)
}
2. Type Safety
End-to-end type safety ensures reliability and improves developer experience.
Database Schema Types
// Auto-generated database types
export interface Database {
public: {
Tables: {
products: {
Row: {
id: string
name: string
price: number
category_id: string
created_at: string
}
Insert: {
id?: string
name: string
price: number
category_id: string
created_at?: string
}
Update: {
id?: string
name?: string
price?: number
category_id?: string
created_at?: string
}
}
}
}
}
API Contract Types
// Request/Response types
export interface CreateProductRequest {
name: string
price: number
categoryId: string
description?: string
}
export interface CreateProductResponse {
success: boolean
data?: Product
error?: string
}
// Type-safe API client
export async function createProduct(data: CreateProductRequest): Promise<CreateProductResponse> {
const response = await fetch('/api/products', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
})
return response.json() as CreateProductResponse
}
Runtime Validation
import { z } from 'zod'
// Schema definition
const ProductSchema = z.object({
name: z.string().min(1, 'Name is required'),
price: z.number().positive('Price must be positive'),
categoryId: z.string().uuid('Invalid category ID'),
description: z.string().optional()
})
// Type inference
type Product = z.infer<typeof ProductSchema>
// Runtime validation
export function validateProduct(data: unknown): Product {
return ProductSchema.parse(data)
}
3. Security First
Security considerations are integrated at every architectural level.
Defense in Depth
// Multiple security layers
export async function updateProduct(productId: string, data: UpdateProductData) {
// 1. Authentication check
const user = await authenticate(request)
if (!user) throw new UnauthorizedError()
// 2. Authorization check
if (!user.canUpdateProduct(productId)) {
throw new ForbiddenError()
}
// 3. Input validation
const validatedData = ProductUpdateSchema.parse(data)
// 4. Business rule validation
await validateBusinessRules(productId, validatedData)
// 5. Database operation with RLS
return await supabase
.from('products')
.update(validatedData)
.eq('id', productId)
.select()
.single()
}
Row Level Security (RLS)
-- Database-level security policies
CREATE POLICY "Users can only view products from their organization"
ON products FOR SELECT
USING (organization_id = auth.jwt() ->> 'organization_id');
CREATE POLICY "Only admins can create products"
ON products FOR INSERT
WITH CHECK (
auth.jwt() ->> 'role' = 'admin' AND
organization_id = auth.jwt() ->> 'organization_id'
);
Input Sanitization
// Sanitize and validate all inputs
export function sanitizeProductData(data: any): CreateProductRequest {
return {
name: sanitizeString(data.name),
price: sanitizeNumber(data.price),
categoryId: sanitizeUuid(data.categoryId),
description: data.description ? sanitizeString(data.description) : undefined
}
}
function sanitizeString(input: any): string {
if (typeof input !== 'string') {
throw new ValidationError('Expected string input')
}
return input.trim().replace(/[<>]/g, '') // Basic XSS prevention
}
4. Performance Optimization
Multiple strategies ensure optimal application performance.
Server-Side Rendering (SSR)
// Server component for initial page load
export default async function ProductsPage() {
// Data fetched on server
const products = await getProducts()
return (
<div>
<h1>Products</h1>
<ProductList products={products} />
</div>
)
}
Code Splitting and Lazy Loading
// Lazy load components
const ProductModal = lazy(() => import('./ProductModal'))
const InventoryChart = lazy(() => import('./InventoryChart'))
export function ProductDashboard() {
const [showModal, setShowModal] = useState(false)
return (
<div>
{/* Always loaded */}
<ProductList />
{/* Lazy loaded when needed */}
<Suspense fallback={<Loading />}>
{showModal && <ProductModal />}
<InventoryChart />
</Suspense>
</div>
)
}
Caching Strategies
// Multiple caching levels
export async function getProduct(id: string): Promise<Product> {
// 1. Memory cache
const cached = memoryCache.get(`product:${id}`)
if (cached) return cached
// 2. Database query with caching
const { data, error } = await supabase
.from('products')
.select('*')
.eq('id', id)
.single()
if (error) throw new Error(error.message)
// 3. Cache the result
memoryCache.set(`product:${id}`, data, { ttl: 300 }) // 5 minutes
return data
}
Database Optimization
// Optimized queries with proper indexing
export async function searchProducts(query: string, category?: string) {
let dbQuery = supabase
.from('products')
.select(`
id,
name,
price,
category:categories!inner(name)
`)
.ilike('name', `%${query}%`) // Uses index on name column
if (category) {
dbQuery = dbQuery.eq('categories.name', category) // Uses category index
}
return dbQuery.limit(50) // Limit results for performance
}
5. Scalability
Architecture designed for both horizontal and vertical scaling.
Stateless Design
// Stateless server components
export async function ProductList({ category }: { category?: string }) {
// No server-side state - can be replicated across instances
const products = await getProductsByCategory(category)
return (
<div>
{products.map(product => (
<ProductCard key={product.id} product={product} />
))}
</div>
)
}
Microservice-Ready Architecture
// Service abstraction for future microservices
export interface ProductService {
getProducts(filters: ProductFilters): Promise<Product[]>
createProduct(data: CreateProductData): Promise<Product>
updateProduct(id: string, data: UpdateProductData): Promise<Product>
deleteProduct(id: string): Promise<void>
}
// Current implementation
export class SupabaseProductService implements ProductService {
// Implementation using Supabase
}
// Future microservice implementation
export class HttpProductService implements ProductService {
// Implementation using HTTP API
}
Event-Driven Architecture
// Event system for loose coupling
export class EventBus {
private listeners: Map<string, Function[]> = new Map()
emit(event: string, data: any) {
const handlers = this.listeners.get(event) || []
handlers.forEach(handler => handler(data))
}
on(event: string, handler: Function) {
const handlers = this.listeners.get(event) || []
this.listeners.set(event, [...handlers, handler])
}
}
// Usage
eventBus.on('product.created', (product: Product) => {
// Update inventory
// Send notifications
// Log activity
})
eventBus.emit('product.created', newProduct)
Resource Optimization
// Connection pooling and resource management
export class DatabasePool {
private pool: Pool
constructor() {
this.pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20, // Maximum connections
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
})
}
async query(text: string, params?: any[]) {
const client = await this.pool.connect()
try {
return await client.query(text, params)
} finally {
client.release()
}
}
}
Implementation Guidelines
Code Organization
- Group related functionality together
- Use consistent naming conventions
- Implement proper error boundaries
- Document architectural decisions
Testing Strategy
- Unit tests for business logic
- Integration tests for API endpoints
- End-to-end tests for critical workflows
- Performance tests for bottlenecks
Monitoring and Observability
- Structured logging at each layer
- Performance metrics collection
- Error tracking and alerting
- Health check endpoints
These design principles work together to create a robust, maintainable, and scalable system architecture that can evolve with changing requirements while maintaining quality and performance standards.