COMP 305: Object-Oriented Software Design

University of San Diego, Fall 2025

Client-Server Architecture

The Client-Server pattern is the foundational architecture of the web, separating concerns between the client (browser) and server (backend). In web development, this manifests as the eternal tension between server-side and client-side rendering, with profound implications for performance, security, and user experience.

The Fundamental Trade-Off

The client-server division creates an inherent tension that shapes modern web development:

Server: Control & Security

  • Trusted execution environment
  • Business logic is secret
  • Data validation is authoritative
  • Access control enforced

But: Every interaction requires a network round-trip (50-500ms latency)

Client: Performance & UX

  • Instant interactions (<5ms)
  • Rich, app-like experience
  • Offline capabilities
  • Reduced server load

But: Code is visible, modifiable, and completely untrusted in public settings

The Challenge: You need the client for performance and user experience, but you must never trust it for security or business logic.

Thin Client: Server-Heavy Approach

Traditional web development where the server does most of the work. The client is just a "thin" rendering layer.

// THIN CLIENT - Server does most of the work
// Traditional server-side web development

// Server-side (Node.js/Express)
app.get('/products', async (req, res) => {
    const { category, sort } = req.query;

    // Server handles ALL business logic
    const products = await db.products
        .where('category', category)
        .orderBy(sort)
        .limit(20);

    const relatedProducts = await db.products
        .where('category', category)
        .where('id', '!=', products[0].id)
        .limit(5);

    const reviews = await db.reviews
        .whereIn('product_id', products.map(p => p.id));

    // Server renders complete HTML
    res.render('products', {
        products,
        relatedProducts,
        reviews,
        averageRating: calculateAverage(reviews)
    });
});

// Client-side (minimal JavaScript)
<script>
    // Just basic interactivity
    document.querySelectorAll('form').forEach(form => {
        form.addEventListener('submit', (e) => {
            // Forms submit to server, page reloads
            // No complex client-side logic
        });
    });
</script>

/* BENEFITS:
   - Server has full control
   - SEO friendly (HTML rendered server-side)
   - Works without JavaScript
   - Secure (business logic on server)

   DRAWBACKS:
   - Every interaction requires server round-trip
   - Page reloads disrupt user experience
   - Higher server load
   - Slower perceived performance
*/

Characteristics of Thin Clients

When to use: Content sites, blogs, e-commerce product pages, public-facing pages, SEO-critical applications

Fat Client: Client-Heavy Approach

Modern JavaScript-focused development where the client does most of the work. The server just provides APIs.

// FAT CLIENT - Client does most of the work
// Modern JavaScript-focused web development (SPA)

// Server-side (minimal, just API)
app.get('/api/products', async (req, res) => {
    // Server just returns data
    const products = await db.products.findAll();
    res.json(products);
});

app.get('/api/reviews/:productId', async (req, res) => {
    const reviews = await db.reviews
        .where('product_id', req.params.productId);
    res.json(reviews);
});

// Client-side (heavy JavaScript)
class ProductManager {
    constructor() {
        this.products = [];
        this.filters = { category: null, sort: 'name' };
    }

    async loadProducts() {
        // Client fetches raw data
        const response = await fetch('/api/products');
        this.products = await response.json();
        this.applyFilters();
    }

    applyFilters() {
        // CLIENT handles filtering and sorting
        let filtered = this.products;

        if (this.filters.category) {
            filtered = filtered.filter(p =>
                p.category === this.filters.category
            );
        }

        // CLIENT handles sorting
        filtered.sort((a, b) => {
            if (this.filters.sort === 'name') {
                return a.name.localeCompare(b.name);
            }
            return a.price - b.price;
        });

        // CLIENT renders UI
        this.render(filtered);
    }

    render(products) {
        // CLIENT generates HTML dynamically
        const html = products.map(p => `
            <div class="product">
                <h3>${p.name}</h3>
                <p>${p.price}</p>
                <button onclick="addToCart(${p.id})">Add to Cart</button>
            </div>
        `).join('');

        document.getElementById('products').innerHTML = html;
    }

    setFilter(category) {
        // Instant filtering - no server round-trip!
        this.filters.category = category;
        this.applyFilters();
    }
}

/* BENEFITS:
   - Instant interactions (no page reload)
   - Reduced server load (offload to client)
   - Rich, app-like experience
   - Offline capabilities possible

   DRAWBACKS:
   - Client code is visible and modifiable
   - Business logic can be tampered with
   - Requires JavaScript to function
   - SEO challenges (initial HTML is empty)
   - Larger initial bundle size
*/

Characteristics of Fat Clients

When to use: Web applications, dashboards, collaborative tools, rich interactive experiences, when SEO is not critical

The Security Trade-Off

The client is an untrusted environment. Any code running in the browser can be inspected, modified, and bypassed by the user.

// SECURITY TRADE-OFFS

// ❌ NEVER trust client-side validation
// Client-side validation is for UX, not security

// Client-side (BAD - can be bypassed)
function validatePrice(price) {
    if (price < 0) {
        alert('Price cannot be negative!');
        return false;
    }
    return true;
}

function submitProduct(product) {
    if (validatePrice(product.price)) {
        // User can modify this code in DevTools!
        // They can send negative prices!
        fetch('/api/products', {
            method: 'POST',
            body: JSON.stringify(product)
        });
    }
}

// Server-side (GOOD - always validate)
app.post('/api/products', (req, res) => {
    const { name, price } = req.body;

    // ALWAYS validate on server
    if (price < 0) {
        return res.status(400).json({
            error: 'Price cannot be negative'
        });
    }

    // Additional checks server-side
    if (price > 1000000) {
        return res.status(400).json({
            error: 'Price exceeds maximum'
        });
    }

    // Check user permissions
    if (!req.user.canCreateProducts) {
        return res.status(403).json({
            error: 'Insufficient permissions'
        });
    }

    // Server has final say
    db.products.create({ name, price });
    res.json({ success: true });
});

// Rule: Client-side validation = UX
//       Server-side validation = Security

Security Principles

The Performance Trade-Off

Network round-trips are the enemy of performance. Every server call adds 50-500ms of latency.

// PERFORMANCE TRADE-OFFS

// Thin Client: Every action hits server
async function searchProducts(query) {
    // Network round-trip for every keystroke
    const response = await fetch(`/search?q=${query}`);
    const results = await response.text(); // Full HTML
    document.body.innerHTML = results; // Replace entire page
}

// Result: 300ms+ delay per keystroke
// - Network latency: 50-200ms
// - Server processing: 50-100ms
// - HTML parsing: 10-50ms
// User experience: Laggy and unresponsive

// Fat Client: Client-side search
class FastSearch {
    constructor() {
        // Load all data once
        this.loadAllProducts();
    }

    async loadAllProducts() {
        // One-time network cost
        const response = await fetch('/api/products');
        this.products = await response.json();
    }

    search(query) {
        // Instant search - no network!
        const results = this.products.filter(p =>
            p.name.toLowerCase().includes(query.toLowerCase())
        );

        this.render(results); // 1-5ms
    }
}

// Result: <5ms response time
// User experience: Instant and responsive

// BUT: Initial load includes ALL products
//      - Large data transfer upfront
//      - Doesn't scale to millions of products

// HYBRID: Best of both worlds
class HybridSearch {
    async search(query) {
        // Quick local search first (cached results)
        this.showCached(query);

        // Then fetch fresh results in background
        const fresh = await fetch(`/api/search?q=${query}`);
        this.updateWithFresh(await fresh.json());
    }
}

Performance Considerations

Strategy: Minimize server round-trips for user interactions, but always verify critical actions server-side.

Hybrid Approach: Best of Both Worlds

Modern applications balance client and server responsibilities strategically.

// HYBRID APPROACH - Balance client and server responsibilities

class SmartShoppingCart {
    constructor() {
        this.items = [];
        this.loadFromServer();
    }

    async loadFromServer() {
        // Initial load from server (authoritative)
        const response = await fetch('/api/cart');
        this.items = await response.json();
        this.render();
    }

    addItem(product) {
        // OPTIMISTIC UPDATE on client (instant feedback)
        this.items.push(product);
        this.render();

        // VERIFY on server (security and data integrity)
        fetch('/api/cart/add', {
            method: 'POST',
            body: JSON.stringify({ productId: product.id })
        })
        .then(response => response.json())
        .then(serverCart => {
            // Server response is source of truth
            this.items = serverCart.items;
            this.render();
        })
        .catch(error => {
            // Rollback on error
            this.items = this.items.filter(i => i.id !== product.id);
            this.render();
            alert('Failed to add item');
        });
    }

    calculateTotal() {
        // CLIENT: Display calculation (instant)
        const clientTotal = this.items.reduce((sum, item) =>
            sum + (item.price * item.quantity), 0
        );
        this.showTotal(clientTotal);

        // SERVER: Authoritative calculation (security)
        fetch('/api/cart/total')
            .then(r => r.json())
            .then(serverTotal => {
                if (Math.abs(clientTotal - serverTotal) > 0.01) {
                    // Discrepancy detected - trust server
                    console.warn('Client/server total mismatch');
                    this.showTotal(serverTotal);
                }
            });
    }

    async checkout() {
        // Critical operations ALWAYS on server
        const response = await fetch('/api/checkout', {
            method: 'POST',
            body: JSON.stringify({ items: this.items })
        });

        if (response.ok) {
            window.location.href = '/order-confirmation';
        }
    }
}

/* PRINCIPLE: Client for UX, Server for truth
   - Client: Fast, responsive, optimistic updates
   - Server: Secure, authoritative, validation
*/

Hybrid Principles

Architectural Decision Framework

// ARCHITECTURAL DECISIONS

// When to use THIN CLIENT (server-heavy):
const thinClientUseCase = {
    scenarios: [
        'SEO-critical content (blogs, marketing sites)',
        'Public data that changes frequently',
        'Simple CRUD applications',
        'Admin panels with complex permissions',
        'E-commerce product pages (SEO important)',
        'Forms with complex server-side validation',
        'When JavaScript can't be guaranteed'
    ],

    technologies: [
        'PHP, Ruby on Rails, Django, Laravel',
        'Server-side rendering (SSR)',
        'Template engines (EJS, Handlebars, Blade)',
        'Progressive enhancement'
    ],

    tradeoffs: {
        pros: [
            'SEO out of the box',
            'Works without JavaScript',
            'Lower client device requirements',
            'Server has full control'
        ],
        cons: [
            'Page reloads for every action',
            'Higher server costs',
            'Slower perceived performance',
            'Less interactive'
        ]
    }
};

// When to use FAT CLIENT (client-heavy):
const fatClientUseCase = {
    scenarios: [
        'Web applications (Gmail, Figma, Notion)',
        'Dashboards with real-time updates',
        'Collaborative tools',
        'Games',
        'Rich text editors',
        'Interactive data visualization',
        'Offline-capable applications'
    ],

    technologies: [
        'React, Vue, Angular, Svelte',
        'Single Page Applications (SPAs)',
        'GraphQL clients',
        'State management (Redux, Zustand)',
        'Service Workers for offline'
    ],

    tradeoffs: {
        pros: [
            'Instant interactions',
            'Rich, app-like experience',
            'Lower server costs (compute offloaded)',
            'Offline capabilities'
        ],
        cons: [
            'Requires JavaScript',
            'SEO requires extra work (SSR/SSG)',
            'Security concerns (client is untrusted)',
            'Larger initial download'
        ]
    }
};

// Modern HYBRID approach:
const hybridApproach = {
    approach: 'Server-side rendering + client-side hydration',

    technologies: [
        'Next.js (React)',
        'Nuxt (Vue)',
        'SvelteKit',
        'Astro with islands'
    ],

    benefits: [
        'Fast initial load (server-rendered HTML)',
        'SEO friendly',
        'Progressive enhancement to SPA',
        'Best of both worlds'
    ],

    example: `
        // Server renders initial HTML
        // Client "hydrates" to add interactivity
        // Subsequent navigation uses client-side routing
        // Critical content works without JS
        // Enhanced features require JS
    `
};

Real-World Example: E-commerce Evolution

See how the same product page evolves across different architectural approaches.

// REAL-WORLD EXAMPLE: E-commerce Product Page

// Traditional Thin Client Approach (WordPress, Shopify Classic)
// ----------------------------------------------------------------
// Server: PHP/Ruby renders complete HTML
// - Product details from database
// - Reviews from database
// - Recommendations calculated server-side
// - Every "Add to Cart" reloads page

// Result:
// ✓ SEO perfect (Google sees everything)
// ✓ Works without JavaScript
// ✗ Adding to cart = 2-3 second page reload
// ✗ Switching images = new page load
// ✗ High server load

// Modern Fat Client Approach (Headless commerce)
// ----------------------------------------------------------------
// Server: REST API returns JSON
// Client: React/Vue handles everything

class ProductPage {
    async load() {
        const [product, reviews, related] = await Promise.all([
            fetch('/api/products/123'),
            fetch('/api/reviews/123'),
            fetch('/api/related/123')
        ]);

        // Client renders everything
        this.render({ product, reviews, related });
    }

    addToCart(productId) {
        // Optimistic update - instant feedback
        this.showCartAnimation();
        this.updateCartCount(+1);

        // Server call in background
        fetch('/api/cart/add', {
            method: 'POST',
            body: JSON.stringify({ productId })
        });
    }
}

// Result:
// ✓ Instant interactions (no page reload)
// ✓ Smooth animations
// ✗ SEO requires SSR setup
// ✗ Initial HTML is empty
// ✗ Requires JavaScript to function

// HYBRID Approach (Next.js, Astro)
// ----------------------------------------------------------------
// Server: Renders initial HTML
// Client: Enhances with JavaScript

export async function getServerSideProps() {
    // Server fetches data at request time
    const product = await api.getProduct(123);
    return { props: { product } };
}

export default function ProductPage({ product }) {
    // HTML rendered on server (SEO + fast load)
    // React hydrates and adds interactivity
    // Subsequent actions use client-side

    return (
        <div>
            <h1>{product.name}</h1>
            <AddToCartButton product={product} />
        </div>
    );
}

// Result:
// ✓ SEO perfect (server-rendered HTML)
// ✓ Fast initial load
// ✓ Instant subsequent interactions
// ✓ Progressive enhancement
// ~ More complex architecture

Modern Solutions

Approach Technologies Best For
Traditional SSR PHP, Rails, Django Blogs, marketing sites
SPA React, Vue, Angular Web apps, dashboards
SSR + Hydration Next.js, Nuxt, SvelteKit E-commerce, content + app
Islands Architecture Astro, Fresh Content-heavy with interactive widgets

Key Takeaways

Related Patterns