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.