Architecting Full Stack Applications
YAML--- name: architecting-full-stack-applications description: Designs and builds complete application ecosystems spanning database schema, backend APIs, frontend UI, infrastructure, and security, while mentoring engineers and translating business needs into technical plans. Use when starting a new feature or system, evaluating architectural tradeoffs, reviewing full-stack code, diagnosing performance issues, or planning deployment/security strategy. --- # Architecting Full Stack Applications
When given a feature or system request, work top-down through the stack:
- Clarify the requirement in business terms (who uses this, what problem does it solve, what's the scale?)
- Sketch the data model first — entities, relationships, access patterns
- Choose the architecture pattern (monolith/microservices/serverless) based on team size, scale, and time-to-market
- Define the API contract before writing implementation
- Build backend → frontend → wire together
- Add auth/security, tests, and CI/CD — never as an afterthought
Example prompt response pattern:
Request: "Add a commenting feature to blog posts"
1. Data model: comments table (id, post_id FK, user_id FK, body, parent_id nullable for threading, created_at)
- Index on post_id, parent_id for fast retrieval
2. Architecture: fits existing monolith, no new service needed
3. API: POST /api/posts/:id/comments, GET /api/posts/:id/comments?cursor=, DELETE /api/comments/:id
4. Backend: validation, rate-limiting, ownership check on delete
5. Frontend: optimistic UI update on submit, paginated/threaded render
6. Security: sanitize body (XSS), auth middleware, ownership-based RBAC
7. Tests: unit (validation, permissions), integration (API round-trip)
Use this checklist for any non-trivial feature or system design task:
Progress:
- [ ] Requirements clarified (business goal, users, scale, constraints)
- [ ] Data model designed (schema, relationships, indexes)
- [ ] Architecture pattern chosen and justified
- [ ] API contract defined (endpoints, payloads, error shapes)
- [ ] Backend logic implemented (business rules, validation, async handling)
- [ ] Frontend built (components, state management, accessibility)
- [ ] Security applied (auth, RBAC, input sanitization, encryption)
- [ ] Performance checked (query plans, bundle size, caching)
- [ ] Tests written (unit + integration, edge cases)
- [ ] CI/CD & deployment path confirmed
- [ ] Docs/handoff notes for team
Don't skip steps even under time pressure — flag shortcuts explicitly (e.g., "skipping RBAC for MVP, tracked as tech debt in ticket #123") rather than silently omitting them.
Example 1: Database schema design Input: "We need to store users, orders, and products. Users can have many orders, orders have many products." Output:
SQL-- Normalized relational schema CREATE TABLE users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(255) UNIQUE NOT NULL, password_hash TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT now() ); CREATE TABLE products ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, price_cents INTEGER NOT NULL CHECK (price_cents >= 0) ); CREATE TABLE orders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, status VARCHAR(20) NOT NULL DEFAULT 'pending', created_at TIMESTAMPTZ DEFAULT now() ); CREATE TABLE order_items ( -- junction table for many-to-many order_id UUID REFERENCES orders(id) ON DELETE CASCADE, product_id UUID REFERENCES products(id), quantity INTEGER NOT NULL CHECK (quantity > 0), PRIMARY KEY (order_id, product_id) ); CREATE INDEX idx_orders_user_id ON orders(user_id); CREATE INDEX idx_orders_status ON orders(status);
Rationale: junction table avoids duplication, foreign keys enforce integrity, indexes support the most common query paths (user's orders, filtering by status).
Example 2: Architecture pattern choice Input: "Small team of 3, MVP for a B2B SaaS, need to launch in 6 weeks." Output: Monolith (e.g., Laravel or Node/Express with a single deployable). Justification: microservices add operational overhead (service discovery, distributed tracing, multiple deploy pipelines) that a 3-person team can't afford pre-product-market-fit. Structure the monolith with clear module boundaries (domain folders) so it can be split later if needed.
Example 3: Performance diagnosis Input: "Dashboard page takes 4s to load." Output:
- Check network tab — is it one slow request or waterfall of many?
- If DB-bound: run
EXPLAIN ANALYZEon the query; look for seq scans, missing indexes, N+1 queries. - If N+1: replace with eager loading/JOIN or a batched dataloader.
- If frontend-bound: check bundle size, lazy-load non-critical components, check for unnecessary re-renders (React DevTools profiler).
- Add caching (Redis for hot queries, HTTP caching headers for static data) if the data doesn't change per-request.
- Design the data model before the API, and the API before the UI. Changing a schema later is expensive; changing a component is cheap.
- Default to boring, proven tech unless there's a specific reason not to (PostgreSQL over exotic DBs, REST over GraphQL unless clients need flexible queries).
- Push validation to the boundary — validate at the API layer, never trust the client, sanitize all user input server-side regardless of frontend checks.
- Make security non-optional: parameterized queries always, hash passwords with bcrypt/argon2, use HttpOnly cookies or short-lived JWTs, apply least-privilege RBAC.
- Automate everything repeatable: linting, tests, and deploys run in CI, not on a developer's machine.
- Write tests for business logic and edge cases, not for framework internals.
- In code review, explain the "why," not just the "what" — mentorship happens in the comments, not just the approval.
- When translating business requirements, restate them as explicit acceptance criteria before writing code, to catch ambiguity early.
- Don't pick microservices for a small team/early-stage product — the coordination overhead outweighs the scaling benefit until you actually have a scaling problem.
- Don't let the frontend dictate the data model. Design normalized, correct schemas first; shape data for the UI at the API/view layer.
- Don't skip indexes until "it gets slow." Index foreign keys and common filter/sort columns from day one.
- Don't roll your own auth/crypto. Use established libraries (Passport, Auth0, bcrypt) — homegrown auth is a recurring source of breaches.
- Don't optimize prematurely. Profile first (query plans, flame graphs) — don't guess at bottlenecks.
- Don't merge code without tests for the changed behavior, even under deadline pressure — this is where regressions come from.
- Don't communicate only in tickets. Complex architectural decisions need a short written rationale (ADR-style) so the team understands tradeoffs later, not just the outcome.