server_authoritative_design.md (45276B)
1 # Server-Authoritative Design 2 3 # Server-Authoritative Design: The Backend-as-Truth Principle 4 5 Server-authoritative design is an architectural philosophy where **the backend serves as the single source of truth**, treating client applications as inherently unreliable from a connection and execution standpoint. This principle asserts that while frontends may disconnect, crash, or behave unpredictably, the backend must maintain consistency, enforce business rules, and guarantee data integrity. 6 7 ``` 8 ┌─────────────────────┐ ┌─────────────────────┐ 9 │ Client │ │ Backend │ 10 │ │ │ │ 11 │ - Ephemeral │ ◄── Request ──► │ - Authoritative │ 12 │ - Unreliable │ │ - Persistent │ 13 │ - Untrusted │ ◄── Response ──► │ - Trusted │ 14 │ - Stateful │ │ - Stateless/ │ 15 │ (when connected) │ Connection │ Transactional │ 16 └─────────────────────┘ may fail └─────────────────────┘ 17 ▲ 18 │ 19 ┌─────────────────────┐ 20 │ Single Source │ 21 │ of Truth │ 22 │ │ 23 │ - Validation │ 24 │ - Business Rules │ 25 │ - Data Integrity │ 26 │ - Transactions │ 27 └─────────────────────┘ 28 ``` 29 30 ## Core Philosophy 31 32 ### Why Treat Frontends as Unreliable? 33 34 From a **connection perspective**, clients are fundamentally unreliable: 35 36 | Client Failure Mode | Implications for Design | 37 |---------------------|-------------------------| 38 | **Network Disconnection** | WiFi drops, mobile signal loss, VPN issues | 39 | **Browser/Tab Closure** | User can close application at any moment | 40 | **Device Power Loss** | Battery dies, system crash, forced restart | 41 | **Background Throttling** | Mobile OS limits background process execution | 42 | **Intentional Disruption** | User kills app via task manager | 43 44 From a **security and trust perspective**, clients are inherently untrusted: 45 - Code can be inspected, modified, or bypassed 46 - Input validation can be circumvented 47 - Authentication tokens can be stolen 48 - Users may attempt to exploit business logic 49 50 ### The Server's Role as Authoritative Source 51 52 The backend becomes the **arbiter of truth**: 53 54 1. **Validation Gatekeeper**: Every mutation request must pass server-side validation 55 2. **Business Logic Enforcer**: Rules applied consistently across all clients 56 3. **Transactional Guarantor**: ACID properties maintained at database level 57 4. **Consensus Point**: Single source for resolving conflicts or race conditions 58 59 ## Architectural Patterns 60 61 ### Thin Client vs. Fat Client Spectrum 62 63 ``` 64 Thin Client Fat Client 65 ┌─────────────────────┐ ┌─────────────────────┐ 66 │ Presentation Only │ │ Business Logic │ 67 │ │ │ │ 68 │ + Minimal State │ │ + Rich State │ 69 │ + Server-Rendered │ │ + Optimistic UI │ 70 │ + Simple Updates │ │ + Advanced Caching │ 71 │ - High Latency │ │ - Complex Sync │ 72 │ - Poor Offline │ │ - Security Risks │ 73 └─────────┬───────────┘ └─────────┬───────────┘ 74 │ │ 75 └────────────────────────────────────┘ 76 Hybrid Approach 77 (Server-Authoritative with 78 Intelligent Client Features) 79 ``` 80 81 ### Hybrid Server-Authoritative Pattern 82 83 Modern applications often follow a hybrid approach: 84 85 ``` 86 ┌─────────────────────────────────────────────────────────────────┐ 87 │ Client Application │ 88 │ │ 89 │ ┌──────────────────┐ ┌──────────────────┐ │ 90 │ │ Local Cache │ │ Optimistic UI │ │ 91 │ │ (Ephemeral) │ │ (Immediate │ │ 92 │ │ │ │ Feedback) │ │ 93 │ └──────────────────┘ └──────────────────┘ │ 94 │ │ │ │ 95 │ └──────────────────────────┼────────────────────────────┘ 96 │ ▼ │ 97 │ ┌──────────────────┐ │ 98 │ │ Sync Engine │ │ 99 │ │ (Queues/Retry) │ │ 100 │ └─────────┬────────┘ │ 101 └─────────────────────────────────────┼────────────────────────────┘ 102 │ 103 Network Boundary 104 │ 105 ┌─────────────────────────────────────┼────────────────────────────┐ 106 │ Backend Server │ 107 │ │ 108 │ ┌──────────────────┐ ┌──────────────────┐ ┌─────────────┐ │ 109 │ │ Validation │ │ Business Logic │ │ Data Store │ │ 110 │ │ (Authoritative) │────▶ (Authoritative) │────▶ (Source of │ │ 111 │ │ │ │ │ │ Truth) │ │ 112 │ └──────────────────┘ └──────────────────┘ └─────────────┘ │ 113 │ │ 114 │ ┌─────────────────────────────────────────────────────────────┐ │ 115 │ │ Conflict Resolution │ │ 116 │ │ - Last-Write-Wins (with versioning) │ │ 117 │ │ - Operational Transformation (for collaborative editing) │ │ 118 │ │ - Client ID precedence rules │ │ 119 │ └─────────────────────────────────────────────────────────────┘ │ 120 └───────────────────────────────────────────────────────────────────┘ 121 ``` 122 123 ## Implementation Examples 124 125 ### Rust Backend: Transactional Business Logic 126 127 ```rust 128 // Domain model - The source of truth 129 #[derive(Debug, Clone, Serialize, Deserialize)] 130 pub struct BankAccount { 131 pub id: Uuid, 132 pub user_id: Uuid, 133 pub balance: Decimal, 134 pub version: i32, // For optimistic concurrency control 135 pub created_at: DateTime<Utc>, 136 pub updated_at: DateTime<Utc>, 137 } 138 139 // Authoritative business logic 140 pub struct AccountService { 141 db_pool: PgPool, 142 } 143 144 impl AccountService { 145 /// Transfer funds between accounts - AUTHORITATIVE VERSION 146 /// This is the single source of truth for fund transfers 147 #[tracing::instrument(skip(self))] 148 pub async fn transfer_funds( 149 &self, 150 from_account_id: Uuid, 151 to_account_id: Uuid, 152 amount: Decimal, 153 request_id: Uuid, // Idempotency key 154 ) -> Result<Transaction, TransferError> { 155 // Check: amount must be positive 156 if amount <= Decimal::ZERO { 157 return Err(TransferError::InvalidAmount); 158 } 159 160 // Wrap in database transaction to maintain consistency 161 let mut tx = self.db_pool.begin().await?; 162 163 // Use SELECT FOR UPDATE to lock rows 164 let (from_account, to_account) = tokio::try_join!( 165 sqlx::query_as!( 166 BankAccount, 167 "SELECT * FROM bank_accounts WHERE id = $1 FOR UPDATE", 168 from_account_id 169 ) 170 .fetch_optional(&mut *tx) 171 .map_err(TransferError::from), 172 173 sqlx::query_as!( 174 BankAccount, 175 "SELECT * FROM bank_accounts WHERE id = $1 FOR UPDATE", 176 to_account_id 177 ) 178 .fetch_optional(&mut *tx) 179 .map_err(TransferError::from), 180 )?; 181 182 // Validate: accounts exist 183 let from_account = from_account.ok_or(TransferError::AccountNotFound(from_account_id))?; 184 let to_account = to_account.ok_or(TransferError::AccountNotFound(to_account_id))?; 185 186 // Validate: sufficient funds (business rule) 187 if from_account.balance < amount { 188 return Err(TransferError::InsufficientFunds { 189 available: from_account.balance, 190 requested: amount, 191 }); 192 } 193 194 // Validate: not transferring to self 195 if from_account_id == to_account_id { 196 return Err(TransferError::SelfTransfer); 197 } 198 199 // Validate: amount limits (business rule) 200 const MAX_TRANSFER_AMOUNT: Decimal = Decimal::from_str("10000").unwrap(); 201 if amount > MAX_TRANSFER_AMOUNT { 202 return Err(TransferError::ExceedsLimit(MAX_TRANSFER_AMOUNT)); 203 } 204 205 // Perform the transfer atomically 206 let new_from_balance = from_account.balance - amount; 207 let new_to_balance = to_account.balance + amount; 208 209 // Update with optimistic concurrency control 210 let updated_from = sqlx::query!( 211 r#" 212 UPDATE bank_accounts 213 SET balance = $1, version = version + 1, updated_at = NOW() 214 WHERE id = $2 AND version = $3 215 RETURNING id, user_id, balance, version, created_at, updated_at 216 "#, 217 new_from_balance, 218 from_account_id, 219 from_account.version 220 ) 221 .fetch_optional(&mut *tx) 222 .await?; 223 224 if updated_from.is_none() { 225 return Err(TransferError::ConcurrentModification); 226 } 227 228 let updated_to = sqlx::query!( 229 r#" 230 UPDATE bank_accounts 231 SET balance = $1, version = version + 1, updated_at = NOW() 232 WHERE id = $2 AND version = $3 233 RETURNING id, user_id, balance, version, created_at, updated_at 234 "#, 235 new_to_balance, 236 to_account_id, 237 to_account.version 238 ) 239 .fetch_optional(&mut *tx) 240 .await?; 241 242 if updated_to.is_none() { 243 return Err(TransferError::ConcurrentModification); 244 } 245 246 // Record the transaction for audit trail 247 let transaction = sqlx::query_as!( 248 Transaction, 249 r#" 250 INSERT INTO transactions 251 (id, from_account_id, to_account_id, amount, status, request_id, created_at) 252 VALUES ($1, $2, $3, $4, $5, $6, NOW()) 253 RETURNING * 254 "#, 255 Uuid::new_v4(), 256 from_account_id, 257 to_account_id, 258 amount, 259 "COMPLETED", 260 request_id 261 ) 262 .fetch_one(&mut *tx) 263 .await?; 264 265 // Commit the entire transaction 266 tx.commit().await?; 267 268 Ok(transaction) 269 } 270 } 271 272 // Compare with naive client-side implementation (WHAT NOT TO DO) 273 pub struct NaiveClientSideTransfer { 274 // This would be BAD: trusting client to validate and calculate 275 pub async fn transfer_funds_naive( 276 &self, 277 from_balance: Decimal, // Client-provided - UNTRUSTED! 278 to_balance: Decimal, // Client-provided - UNTRUSTED! 279 amount: Decimal, 280 ) -> Result<(), TransferError> { 281 // Client-side validation - CAN BE BYPASSED 282 if amount <= Decimal::ZERO { 283 return Err(TransferError::InvalidAmount); 284 } 285 286 // Client-side business logic - CAN BE MANIPULATED 287 if from_balance < amount { 288 return Err(TransferError::InsufficientFunds { 289 available: from_balance, 290 requested: amount, 291 }); 292 } 293 294 // Client-side calculation - WRONG if balances changed 295 let new_from = from_balance - amount; 296 let new_to = to_balance + amount; 297 298 // Send to server - RACE CONDITION if other transfers happening 299 self.update_balance(from_account_id, new_from).await?; 300 self.update_balance(to_account_id, new_to).await?; 301 302 Ok(()) 303 } 304 } 305 ``` 306 307 ### TypeScript Frontend: Optimistic UI with Server Reconciliation 308 309 ```typescript 310 // Client-side representation (NOT authoritative) 311 interface ClientBankAccount { 312 id: string; 313 userId: string; 314 balance: number; 315 pendingTransactions: PendingTransaction[]; 316 version: number; // For optimistic updates 317 } 318 319 // Sync engine that respects server authority 320 class AccountSyncEngine { 321 private localState: Map<string, ClientBankAccount> = new Map(); 322 private pendingQueue: PendingOperation[] = []; 323 private isOnline: boolean = true; 324 325 // Optimistic update - immediate UI feedback 326 async transferFundsOptimistic( 327 fromAccountId: string, 328 toAccountId: string, 329 amount: number 330 ): Promise<void> { 331 // 1. Client-side validation (for UX, not security) 332 if (amount <= 0) { 333 throw new Error('Amount must be positive'); 334 } 335 336 const fromAccount = this.localState.get(fromAccountId); 337 const toAccount = this.localState.get(toAccountId); 338 339 if (!fromAccount || !toAccount) { 340 throw new Error('Account not found'); 341 } 342 343 // 2. Optimistic update - show changes immediately 344 const operationId = crypto.randomUUID(); 345 const pendingTx: PendingTransaction = { 346 id: operationId, 347 fromAccountId, 348 toAccountId, 349 amount, 350 status: 'pending', 351 timestamp: Date.now(), 352 }; 353 354 // Update local state optimistically 355 this.localState.set(fromAccountId, { 356 ...fromAccount, 357 balance: fromAccount.balance - amount, 358 pendingTransactions: [...fromAccount.pendingTransactions, pendingTx], 359 version: fromAccount.version + 1, 360 }); 361 362 this.localState.set(toAccountId, { 363 ...toAccount, 364 balance: toAccount.balance + amount, 365 version: toAccount.version + 1, 366 }); 367 368 // Notify UI of change 369 this.notifyStateChange(); 370 371 // 3. Queue for server sync (authoritative) 372 const operation: PendingOperation = { 373 id: operationId, 374 type: 'transfer', 375 fromAccountId, 376 toAccountId, 377 amount, 378 retryCount: 0, 379 timestamp: Date.now(), 380 }; 381 382 this.pendingQueue.push(operation); 383 384 // 4. Try to sync immediately 385 await this.flushQueue(); 386 } 387 388 // Sync with server - respects server authority 389 private async flushQueue(): Promise<void> { 390 if (!this.isOnline || this.pendingQueue.length === 0) { 391 return; 392 } 393 394 // Process operations in order 395 for (const operation of this.pendingQueue.slice()) { 396 try { 397 // Send to authoritative backend 398 const response = await fetch('/api/transfers', { 399 method: 'POST', 400 headers: { 401 'Content-Type': 'application/json', 402 'Idempotency-Key': operation.id, 403 }, 404 body: JSON.stringify({ 405 fromAccountId: operation.fromAccountId, 406 toAccountId: operation.toAccountId, 407 amount: operation.amount, 408 requestId: operation.id, 409 }), 410 }); 411 412 if (response.ok) { 413 // Server accepted - update local state to match server 414 const serverTransaction = await response.json(); 415 await this.reconcileWithServer(serverTransaction); 416 417 // Remove from queue 418 const index = this.pendingQueue.indexOf(operation); 419 if (index > -1) { 420 this.pendingQueue.splice(index, 1); 421 } 422 } else if (response.status === 409) { 423 // Conflict - server rejected due to business rule 424 await this.handleConflict(operation, response); 425 } else { 426 // Other error - retry later 427 operation.retryCount++; 428 if (operation.retryCount > 3) { 429 // Give up and revert optimistic update 430 await this.revertOptimisticUpdate(operation); 431 } 432 } 433 } catch (error) { 434 // Network error - will retry when back online 435 console.warn('Network error, will retry:', error); 436 } 437 } 438 } 439 440 // Reconciliation: align client state with server truth 441 private async reconcileWithServer(serverTransaction: ServerTransaction): Promise<void> { 442 // Get current optimistic state 443 const fromAccount = this.localState.get(serverTransaction.fromAccountId); 444 const toAccount = this.localState.get(serverTransaction.toAccountId); 445 446 if (!fromAccount || !toAccount) { 447 // Something wrong - fetch fresh state from server 448 await this.fetchAccountState(serverTransaction.fromAccountId); 449 await this.fetchAccountState(serverTransaction.toAccountId); 450 return; 451 } 452 453 // Remove pending transaction 454 const updatedFromPending = fromAccount.pendingTransactions.filter( 455 tx => tx.id !== serverTransaction.requestId 456 ); 457 458 const updatedToPending = toAccount.pendingTransactions.filter( 459 tx => tx.id !== serverTransaction.requestId 460 ); 461 462 // Update to match server authoritative state 463 // NOTE: We use server-provided balances, not our calculated ones 464 this.localState.set(serverTransaction.fromAccountId, { 465 ...fromAccount, 466 balance: serverTransaction.fromAccountNewBalance, 467 pendingTransactions: updatedFromPending, 468 version: serverTransaction.fromAccountVersion, 469 }); 470 471 this.localState.set(serverTransaction.toAccountId, { 472 ...toAccount, 473 balance: serverTransaction.toAccountNewBalance, 474 pendingTransactions: updatedToPending, 475 version: serverTransaction.toAccountVersion, 476 }); 477 478 this.notifyStateChange(); 479 } 480 481 // Handle server rejection (business rule violation) 482 private async handleConflict(operation: PendingOperation, response: Response): Promise<void> { 483 const error = await response.json(); 484 485 // Revert optimistic update 486 await this.revertOptimisticUpdate(operation); 487 488 // Fetch fresh state from server 489 await this.fetchAccountState(operation.fromAccountId); 490 await this.fetchAccountState(operation.toAccountId); 491 492 // Notify user of the conflict 493 this.notifyError({ 494 type: 'conflict', 495 message: error.message, 496 operationId: operation.id, 497 }); 498 499 // Remove from queue 500 const index = this.pendingQueue.indexOf(operation); 501 if (index > -1) { 502 this.pendingQueue.splice(index, 1); 503 } 504 } 505 } 506 507 // HTTP API client that respects server authority 508 class AuthoritativeApiClient { 509 // Idempotent request pattern 510 async transferFunds( 511 fromAccountId: string, 512 toAccountId: string, 513 amount: number 514 ): Promise<ServerTransaction> { 515 const requestId = crypto.randomUUID(); 516 517 const response = await fetch('/api/transfers', { 518 method: 'POST', 519 headers: { 520 'Content-Type': 'application/json', 521 'Idempotency-Key': requestId, 522 }, 523 body: JSON.stringify({ 524 fromAccountId, 525 toAccountId, 526 amount, 527 requestId, 528 }), 529 }); 530 531 if (!response.ok) { 532 const error = await response.json(); 533 throw new Error(error.message || `Transfer failed: ${response.status}`); 534 } 535 536 return response.json(); 537 } 538 539 // Poll for updates - accept server authority 540 async pollForUpdates(accountId: string, lastVersion: number): Promise<AccountUpdate> { 541 const response = await fetch(`/api/accounts/${accountId}/updates?sinceVersion=${lastVersion}`); 542 543 if (response.status === 304) { 544 // No changes - server is authoritative about this 545 return { hasUpdates: false }; 546 } 547 548 if (!response.ok) { 549 throw new Error(`Failed to poll updates: ${response.status}`); 550 } 551 552 const update = await response.json(); 553 return { 554 hasUpdates: true, 555 account: update.account, 556 transactions: update.transactions, 557 }; 558 } 559 } 560 ``` 561 562 ## Use Cases and Trade-offs 563 564 ### When Server-Authoritative Design Is Essential 565 566 | Use Case | Why Server-Authoritative | Example Implementation | 567 |----------|--------------------------|------------------------| 568 | **Financial Systems** | Legal requirement for audit trails, fraud prevention, regulatory compliance | Banking transactions with double-entry bookkeeping and immutable ledger | 569 | **E-commerce** | Inventory management (prevent overselling), pricing consistency, tax calculation | Stock reservation system, cart abandonment recovery | 570 | **Collaborative Editing** | Conflict resolution, version history, real-time synchronization | Operational transformation in Google Docs, CRDTs with authoritative merge | 571 | **Multiplayer Games** | Cheat prevention, game state consistency, fair play enforcement | Deterministic lockstep simulation, server-side game logic | 572 | **Healthcare Systems** | Patient safety, regulatory compliance (HIPAA), audit requirements | Electronic health records with change tracking | 573 574 ### When to Relax Server Authority 575 576 | Scenario | Appropriate Approach | Rationale | 577 |----------|---------------------|-----------| 578 | **Read-heavy applications** | Client-side caching with TTL/ETag | Reduce server load, improve responsiveness | 579 | **Offline-first apps** | Local-first with eventual consistency | Must function without network connectivity | 580 | **Real-time collaboration** | Hybrid with conflict-free data types | Low latency required, conflicts resolvable | 581 | **Static content delivery** | CDN edge caching with invalidation | Performance outweighs consistency needs | 582 | **Analytics dashboards** | Client-side aggregation of pre-approved data | Reduce server computation costs | 583 584 ## Case Studies 585 586 ### Case Study 1: Banking Application 587 588 **Problem**: A mobile banking app where users could theoretically manipulate client-side code to bypass balance checks. 589 590 **Server-Authoritative Solution**: 591 592 ``` 593 Client (Untrusted) Server (Authoritative) 594 ┌─────────────────┐ ┌─────────────────────┐ 595 │ Transfer Request│──────────────▶│ 1. Validate Token │ 596 │ - Amount: $1000 │ │ 2. Check Balance │ 597 │ - From: Acct A │ │ (SELECT ... FOR │ 598 │ - To: Acct B │ │ UPDATE) │ 599 └─────────────────┘ │ 3. Apply Business │ 600 │ │ Rules │ 601 │ │ 4. Execute in │ 602 │ ┌──────────────┤ Transaction │ 603 │ │ │ 5. Record Audit │ 604 │ │ Rejection │ Trail │ 605 ▼ ▼ └─────────────────────┘ 606 ┌─────────────────┐ ┌─────────────────────┐ 607 │ UI Shows │ │ Transaction │ 608 │ Success │ │ Failed: │ 609 │ Immediately │ │ Insufficient │ 610 │ (Optimistic) │ │ Funds │ 611 └─────────────────┘ └─────────────────────┘ 612 │ │ 613 │ Server Truth │ 614 └──────────────────────┘ 615 ▼ 616 ┌─────────────────┐ 617 │ UI Reconciles │ 618 │ with Server │ 619 │ (Actual State)│ 620 └─────────────────┘ 621 ``` 622 623 **Key Architecture Decisions**: 624 1. **Idempotent requests**: Each transfer includes unique `request_id` to prevent duplicate processing 625 2. **Pessimistic locking**: `SELECT ... FOR UPDATE` prevents race conditions 626 3. **Audit trail**: Every transaction recorded immutably 627 4. **Client reconciliation**: Optimistic UI updates, but final state from server 628 629 ### Case Study 2: E-commerce Inventory 630 631 **Problem**: Flash sale with 100 items, 10,000 simultaneous users. Prevent overselling. 632 633 **Naive Approach (Client-side counting)**: 634 ```typescript 635 // BAD: Client decides if item is available 636 async function purchaseItem(itemId: string) { 637 const inventory = await fetchInventory(itemId); 638 if (inventory.available > 0) { 639 // RACE CONDITION: Other users might be buying simultaneously 640 await purchase(itemId); 641 } 642 } 643 ``` 644 645 **Server-Authoritative Solution**: 646 ```rust 647 // GOOD: Server authoritatively manages inventory 648 #[derive(Debug, Clone, Copy)] 649 pub enum InventoryReservation { 650 Reserved { reservation_id: Uuid, expires_at: DateTime<Utc> }, 651 SoldOut, 652 Available, 653 } 654 655 pub struct InventoryService { 656 redis: RedisConnection, 657 db_pool: PgPool, 658 } 659 660 impl InventoryService { 661 /// Reserve item atomically - only server can do this 662 pub async fn reserve_item( 663 &self, 664 item_id: Uuid, 665 user_id: Uuid, 666 quantity: i32, 667 ) -> Result<InventoryReservation, InventoryError> { 668 // Use Redis for distributed lock AND inventory count 669 let lock_key = format!("inventory:lock:{}", item_id); 670 let inventory_key = format!("inventory:{}", item_id); 671 672 // Atomic check-and-decrement 673 let script = r#" 674 local current = redis.call('GET', KEYS[2]) 675 if not current or tonumber(current) < tonumber(ARGV[1]) then 676 return {false, 'insufficient'} 677 end 678 679 local new_val = tonumber(current) - tonumber(ARGV[1]) 680 redis.call('SET', KEYS[2], new_val) 681 682 local reservation_id = ARGV[2] 683 local expires_at = ARGV[3] 684 redis.call('HSET', 'reservations', reservation_id, ARGV[4]) 685 redis.call('EXPIREAT', reservation_id, expires_at) 686 687 return {true, reservation_id} 688 "#; 689 690 let reservation_id = Uuid::new_v4(); 691 let expires_at = Utc::now() + chrono::Duration::minutes(10); 692 693 let result: (bool, String) = redis::cmd("EVAL") 694 .arg(script) 695 .arg(2) // number of keys 696 .arg(&lock_key) 697 .arg(&inventory_key) 698 .arg(quantity) 699 .arg(reservation_id.to_string()) 700 .arg(expires_at.timestamp()) 701 .arg(user_id.to_string()) 702 .query_async(&mut self.redis.clone()) 703 .await?; 704 705 match result { 706 (true, rid) => Ok(InventoryReservation::Reserved { 707 reservation_id: Uuid::parse_str(&rid).unwrap(), 708 expires_at, 709 }), 710 (false, _) => Ok(InventoryReservation::SoldOut), 711 } 712 } 713 } 714 ``` 715 716 ## Workflow Implementation Steps 717 718 ### Step 1: Identify Authoritative vs. Non-Authoritative Operations 719 720 ``` 721 ┌─────────────────────────────────────────────────────────────┐ 722 │ Operation Analysis Matrix │ 723 ├─────────────────┬─────────────────┬─────────────────────────┤ 724 │ Operation │ Must Be │ Can Be │ 725 │ │ Authoritative │ Client-Side │ 726 ├─────────────────┼─────────────────┼─────────────────────────┤ 727 │ Funds Transfer │ ✅ Yes │ ❌ No │ 728 │ │ (Financial rule)│ │ 729 ├─────────────────┼─────────────────┼─────────────────────────┤ 730 │ Form Validation │ ⚠️ Partial │ ✅ Yes (for UX) │ 731 │ │ (Final check │ │ 732 │ │ on server) │ │ 733 ├─────────────────┼─────────────────┼─────────────────────────┤ 734 │ Search Filtering│ ❌ No │ ✅ Yes │ 735 │ │ (Presentation │ │ 736 │ │ only) │ │ 737 ├─────────────────┼─────────────────┼─────────────────────────┤ 738 │ Read Operations │ ⚠️ Depends │ ✅ Often │ 739 │ │ (Cached vs. │ │ 740 │ │ fresh data) │ │ 741 └─────────────────┴─────────────────┴─────────────────────────┘ 742 ``` 743 744 ### Step 2: Design Idempotent APIs 745 746 ```rust 747 // Rust backend implementing idempotency 748 pub struct IdempotencyService { 749 db_pool: PgPool, 750 } 751 752 impl IdempotencyService { 753 pub async fn execute_with_idempotency<F, T, E>( 754 &self, 755 request_id: Uuid, 756 user_id: Uuid, 757 operation: &str, 758 f: F, 759 ) -> Result<T, E> 760 where 761 F: FnOnce() -> futures::future::BoxFuture<'static, Result<T, E>>, 762 E: From<IdempotencyError>, 763 { 764 // Check if we've already processed this request 765 let existing = sqlx::query!( 766 r#" 767 SELECT result, status_code 768 FROM idempotency_keys 769 WHERE key = $1 AND user_id = $2 AND operation = $3 770 "#, 771 request_id, 772 user_id, 773 operation 774 ) 775 .fetch_optional(&self.db_pool) 776 .await?; 777 778 if let Some(record) = existing { 779 // Request already processed - return cached result 780 match record.status_code.as_str() { 781 "COMPLETED" => { 782 let result: T = serde_json::from_str(&record.result.unwrap())?; 783 return Ok(result); 784 } 785 "FAILED" => { 786 return Err(serde_json::from_str(&record.result.unwrap())?); 787 } 788 _ => { 789 // In progress - wait or retry 790 return Err(IdempotencyError::RequestInProgress.into()); 791 } 792 } 793 } 794 795 // First time - record that we're starting 796 sqlx::query!( 797 r#" 798 INSERT INTO idempotency_keys 799 (key, user_id, operation, status_code, created_at, updated_at) 800 VALUES ($1, $2, $3, 'PROCESSING', NOW(), NOW()) 801 "#, 802 request_id, 803 user_id, 804 operation 805 ) 806 .execute(&self.db_pool) 807 .await?; 808 809 // Execute the actual operation 810 let result = f().await; 811 812 // Record the outcome 813 match &result { 814 Ok(success_result) => { 815 let result_json = serde_json::to_string(success_result)?; 816 sqlx::query!( 817 r#" 818 UPDATE idempotency_keys 819 SET status_code = 'COMPLETED', result = $1, updated_at = NOW() 820 WHERE key = $2 AND user_id = $3 AND operation = $4 821 "#, 822 result_json, 823 request_id, 824 user_id, 825 operation 826 ) 827 .execute(&self.db_pool) 828 .await?; 829 } 830 Err(error) => { 831 let error_json = serde_json::to_string(error)?; 832 sqlx::query!( 833 r#" 834 UPDATE idempotency_keys 835 SET status_code = 'FAILED', result = $1, updated_at = NOW() 836 WHERE key = $2 AND user_id = $3 AND operation = $4 837 "#, 838 error_json, 839 request_id, 840 user_id, 841 operation 842 ) 843 .execute(&self.db_pool) 844 .await?; 845 } 846 } 847 848 result 849 } 850 } 851 ``` 852 853 ### Step 3: Implement Optimistic UI with Reconciliation 854 855 ```typescript 856 // TypeScript reconciliation engine 857 class ReconciliationEngine<T> { 858 private localState: T; 859 private pendingMutations: Array<{ 860 id: string; 861 mutation: (state: T) => T; 862 timestamp: number; 863 }> = []; 864 865 constructor(initialState: T) { 866 this.localState = initialState; 867 } 868 869 // Apply mutation optimistically 870 mutate(mutation: (state: T) => T, mutationId: string): T { 871 const pending = { 872 id: mutationId, 873 mutation, 874 timestamp: Date.now(), 875 }; 876 877 this.pendingMutations.push(pending); 878 this.localState = mutation(this.localState); 879 880 return this.localState; 881 } 882 883 // Reconcile with server authoritative state 884 reconcile(serverState: T, appliedMutationIds: string[]): T { 885 // Remove mutations that server has confirmed 886 this.pendingMutations = this.pendingMutations.filter( 887 mutation => !appliedMutationIds.includes(mutation.id) 888 ); 889 890 // Start from server state (authoritative) 891 let reconciledState = serverState; 892 893 // Re-apply pending mutations that server hasn't seen yet 894 for (const pending of this.pendingMutations) { 895 reconciledState = pending.mutation(reconciledState); 896 } 897 898 this.localState = reconciledState; 899 return reconciledState; 900 } 901 902 // Handle server rejection 903 rejectMutation(mutationId: string, serverState: T): T { 904 // Remove the rejected mutation 905 this.pendingMutations = this.pendingMutations.filter( 906 m => m.id !== mutationId 907 ); 908 909 // Reset to server state 910 this.localState = serverState; 911 912 // Re-apply remaining pending mutations 913 for (const pending of this.pendingMutations) { 914 this.localState = pending.mutation(this.localState); 915 } 916 917 return this.localState; 918 } 919 } 920 ``` 921 922 ## Performance Considerations 923 924 ### Trade-offs: Latency vs. Consistency 925 926 ``` 927 Response Time Impact 928 ┌─────────────────────────────────────────────────┐ 929 │ Client-Side Heavy │ 930 │ │ 931 │ Fast ╭───────────────────────────────────╮ │ 932 │ │■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■│ │ 933 │ │■■■ Optimistic Updates ■■■■│ │ 934 │ │■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■│ │ 935 │ │■■■■ Immediate Feedback ■■■■■■■■■■■│ │ 936 │ │■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■│ │ 937 │ ╰───────────────────────────────────╯ │ 938 │ │ 939 │ Server-Authoritative │ 940 │ │ 941 │ ╭───────────────────────────────────╮ │ 942 │ │■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■│ │ 943 │ │■■■ Network Round-Trips ■■■■│ │ 944 │ │■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■│ │ 945 │ │■■■■ Validation & Locking ■■■■■■■│ │ 946 │ Slow │■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■■│ │ 947 │ ╰───────────────────────────────────╯ │ 948 │ │ 949 └─────────────────────────────────────────────────┘ 950 Consistency 951 ``` 952 953 ### Optimization Strategies 954 955 1. **Batched Authoritative Operations** 956 ```rust 957 // Instead of multiple round-trips 958 for item in cart.items { 959 reserve_item(item.id).await?; // N+1 problem 960 } 961 962 // Batch authoritative operations 963 batch_reserve_items(cart.items).await?; // Single round-trip 964 ``` 965 966 2. **Edge Caching with Validation** 967 ```typescript 968 // Cache validation rules at edge, final check at origin 969 async function validateInputCached(input: unknown): Promise<ValidationResult> { 970 // Check local cache first (rules don't change often) 971 const cachedRules = await cache.get('validation-rules'); 972 const quickResult = quickValidate(input, cachedRules); 973 974 if (!quickResult.valid) { 975 return quickResult; 976 } 977 978 // Final authoritative check 979 return await authoritativeValidate(input); 980 } 981 ``` 982 983 3. **Predictive Pre-authorization** 984 ```rust 985 // Pre-approve likely actions based on user behavior 986 pub async fn pre_authorize_transfer( 987 &self, 988 user_id: Uuid, 989 max_amount: Decimal, 990 window_minutes: i32, 991 ) -> Result<PreAuthorization, AuthError> { 992 // Analytics suggest user will transfer < $500 in next 5 minutes 993 // Pre-reserve capacity, reduce latency for actual transfer 994 } 995 ``` 996 997 ## Security Implications 998 999 ### Threat Model for Server-Authoritative Systems 1000 1001 | Threat | Without Server Authority | With Server Authority | 1002 |--------|--------------------------|------------------------| 1003 | **Balance Manipulation** | Client can modify JavaScript to bypass checks | Server validates all transactions | 1004 | **Race Conditions** | Two clients might oversell inventory | Distributed locks prevent overselling | 1005 | **Replay Attacks** | Same transaction submitted multiple times | Idempotency keys prevent duplicates | 1006 | **Business Logic Bypass** | Client can skip validation steps | All rules enforced server-side | 1007 | **Data Tampering** | Client-side state can be manipulated | Only server state is authoritative | 1008 1009 ### Defense in Depth Strategy 1010 1011 ``` 1012 ┌─────────────────────────────────────────────────────────┐ 1013 │ Defense in Depth Layers │ 1014 ├─────────────────┬───────────────────────────────────────┤ 1015 │ Layer │ Implementation │ 1016 ├─────────────────┼───────────────────────────────────────┤ 1017 │ Client-Side │ Basic validation (UX only) │ 1018 │ │ Input sanitization │ 1019 ├─────────────────┼───────────────────────────────────────┤ 1020 │ Network │ HTTPS/TLS 1.3 │ 1021 │ │ Request signing │ 1022 ├─────────────────┼───────────────────────────────────────┤ 1023 │ API Gateway │ Rate limiting │ 1024 │ │ Schema validation │ 1025 ├─────────────────┼───────────────────────────────────────┤ 1026 │ Application │ Business logic validation │ 1027 │ │ Authentication/Authorization │ 1028 ├─────────────────┼───────────────────────────────────────┤ 1029 │ Database │ ACID transactions │ 1030 │ │ Row-level security │ 1031 │ │ Audit logging │ 1032 └─────────────────┴───────────────────────────────────────┘ 1033 ``` 1034 1035 ## Future Evolution 1036 1037 ### Beyond Traditional Server-Authoritative 1038 1039 1. **Edge Computing with Authoritative Rules** 1040 - Push validation logic to CDN edge 1041 - Final authorization at origin 1042 - Reduced latency while maintaining security 1043 1044 2. **Blockchain as Authoritative Layer** 1045 - Smart contracts as business logic 1046 - Immutable transaction ledger 1047 - Decentralized but still authoritative 1048 1049 3. **Federated Authority** 1050 - Multiple authoritative services 1051 - Consensus protocols for coordination 1052 - Used in distributed systems like Kubernetes 1053 1054 4. **Zero-Trust with Continuous Authorization** 1055 - Every operation re-validated 1056 - Context-aware authorization 1057 - Dynamic policy evaluation 1058 1059 ## Conclusion 1060 1061 The server-authoritative design principle remains essential for systems where correctness, security, and consistency matter more than pure latency. While modern applications often adopt hybrid approaches—optimistic UI updates with eventual server reconciliation—the fundamental truth remains: **the backend must be the ultimate arbiter of business rules and data integrity**. 1062 1063 The key is not eliminating client-side logic, but rather clearly demarcating which operations require server authority and which can be safely delegated. This boundary should be explicit in both architecture documentation and code implementation. 1064 1065 As connectivity improves and edge computing matures, the line between "client" and "server" may blur, but the need for authoritative validation of critical operations will persist. The most robust systems will continue to embrace server authority while optimizing for user experience through intelligent client-side enhancements. 1066 1067 --- 1068 1069 *Article written with examples in Rust and TypeScript, demonstrating practical implementation of server-authoritative patterns while maintaining responsive user interfaces.*