Skip to content

Tier 3 · Chapter 1: Cross-Stack Synthesis

Synthesis & Capstone · ~35 min read + exercise


Why this chapter exists

You've now learned the foundations and applied them in a concrete stack. This chapter does something the earlier ones deliberately set up but never quite delivered: it puts the two stacks side by side on the same problem, so you can see clearly what is architecture and what is merely framework. That distinction is the whole point of the course, and it's hard to feel until you watch one use case solved twice — because the parts that stay the same across the Nuxt fullstack track and Laravel are the architecture, and the parts that differ are the implementation detail. Learning to see through the framework to the architecture underneath is what lets you carry your skills to any stack, and it's what separates an engineer who knows a framework from one who understands architecture.

The throughline: the architecture is what survives the change of stack; the framework is what doesn't. When you implement the same feature in Nuxt (Vue + Nitro) and in PHP/Laravel and the decisions turn out identical — where the boundaries go, what's transactional, what's async, what's strongly consistent, how it fails — while only the syntax and tools differ, you've located the architecture. This chapter walks one realistic use case across both stacks along the dimensions the course has built, to make that line vivid.

The same feature in both stacks: identical boxes and arrows — edge, services, queue, database — with only the labels differing

One honest complication, and it's the most instructive part of the chapter: the two tracks aren't shaped the same. The Nuxt track is fullstack — one codebase that spans the browser and the server. The Laravel track is a backend that exposes an API; in our world, any frontend it serves is split out as its own separately-deployed single-page app. So most dimensions compare cleanly (the decisions are identical), but one — rendering and the client/server boundary — is where the stack's execution model genuinely shapes the answer. That dimension is the exception that proves the rule, and learning to tell a genuine runtime-shaped decision apart from a mere framework difference is the advanced version of the skill this whole course teaches. It's also the warm-up for the capstone, where you'll build a feature twice yourself.

What this chapter covers

  • One shared use case — a realistic feature we'll examine in both stacks.
  • Six dimensions of comparison — boundaries, rendering & the client/server boundary, transactions, async, testing, and operations.
  • What's architecture vs. what's framework — reading the comparison to see the line, including the one dimension where the runtime genuinely moves it.

The shared use case

To compare meaningfully we need one concrete feature, complex enough to exercise the course's lessons. We'll use "resolve a support ticket" from the running ticketing domain — chosen because it touches everything: a state change with rules, a multi-step transactional update, asynchronous side effects, authorization, data that varies in how consistent it must be, and a screen a human actually drives.

The use case, in plain terms: an authenticated expert resolves a ticket they're assigned, through a UI that has to render the ticket, fetch its data, and reflect the change. Resolving it must (1) verify the expert is allowed to resolve this ticket, (2) enforce the rule that only an assigned ticket can be resolved, (3) atomically update the ticket's status, write a resolution record, and adjust the expert's open-ticket count, (4) asynchronously notify the customer and schedule a satisfaction survey, and (5) record an analytics event. It's a small feature with a realistic amount of architectural substance, and we'll examine how each stack handles it along six dimensions.


Dimension 1: Architecture boundaries

The decision (architecture): Where does the business logic live, and what does it depend on? The course's answer is the same in both stacks: the resolve logic belongs in a use-case unit — the same unit under each stack's idiom (a use-case/service in Nuxt, an action in Laravel: one concept, two names) — not in the HTTP handler; it operates on a rich domain entity that owns the "only assigned tickets can be resolved" rule; and it reaches data through a repository so it doesn't depend on the ORM. Edges are thin — they validate input and delegate. The domain depends on nothing framework-specific.

Nuxt (Vue + Nitro): A ResolveTicket use-case/service depends on a TicketRepository interface; a thin Nitro server route (server/api/tickets/[id]/resolve.post.ts) using defineEventHandler validates the body by parsing it against a shared schema (Valibot/Zod) via h3's readValidatedBody, narrowing unknown to a typed value, never a cast — the same schema the Vue form drives through vee-validate — then calls the use case and shapes the response. The client/server contract is carried by that shared schema for inputs, plus Nitro's automatic inference of route return types into useFetch for responses. The Ticket type uses a discriminated union — a type whose variants carry a tag the compiler reads, so it rejects illegal combinations (the "make illegal states unrepresentable" discipline from the Nuxt track's boundaries chapter) — so illegal states (resolving an unassigned ticket) are hard to represent.

Laravel: A ResolveTicket action depends on a TicketRepository interface bound in the container; a thin controller with a form request validates and delegates. The Ticket entity carries a resolve() method enforcing the lifecycle rule.

What's architecture vs. framework: The boundary decisions are identical — thin edge, use-case unit, rich entity, repository, inward dependencies. What differs is purely mechanism: a shared schema parsed at the Nitro edge vs. form requests, the container's interface binding vs. constructor injection, TypeScript's discriminated unions vs. PHP's typed entity methods. Same architecture; different framework expression. The one Nuxt-flavored extra — a single schema spanning both server and client (it validates the edge and drives the form, with the response type inferred end to end) — is an ergonomic superpower, not a different decision: the boundary is in the same place either way. If you described the design without naming the stack, nobody could tell which one you built.


Dimension 2: Rendering and the client/server boundary

This is the dimension the earlier framing skipped, and the most interesting one — because it's where the two stacks genuinely diverge.

The decision (architecture): When a human loads the resolve screen, where does the UI render, where is the client/server data boundary drawn, and is there a backend-for-frontend (BFF) layer between the UI and the domain? Concretely: what data does first paint (and SEO, if it matters) actually need, fetched server-side — versus what can load after the page is interactive? Where is the trust boundary the browser is not allowed to cross (credentials, privileged logic)? Does the browser talk to a layer that shapes and guards data for it, or straight to the domain API? Those questions are architectural and they're the same in any stack.

Nuxt (Vue + Nitro): The app is universal — one codebase runs on the server (SSR) and re-runs in the browser (hydration). The ticket data first paint needs is fetched server-side with useFetch/useAsyncData, which transfers the result to the client so it isn't re-fetched on hydration; secondary or personalized data loads client-side after hydration. The resolve action itself is an interaction, so it's a $fetch in the click handler — deliberately off the render path. The Nitro server routes are the BFF: the browser only ever talks to your own server, which holds credentials and shapes data; the client/server boundary lives inside one project, policed by serialization honesty (the discipline is send plain data, not behavior — Nuxt's payload uses devalue, which round-trips Date/Map/Set/refs, but a class instance loses its methods crossing the wire, so the boundary carries state, never behavior) and the universal/server-only code split (a fitness function bans client code from importing server/ modules).

Laravel: The frontend is split out — a separate single-page app, its own deployable, consuming the Laravel API over HTTP. Rendering happens entirely client-side in that SPA; Laravel is a pure JSON API. So the client/server boundary is a network-and-deployment seam between two codebases, not a code-split inside one. The BFF role, if anyone plays it, belongs to the SPA's own server or an API gateway — not to the domain API. The "what does first paint need" decision still exists, but it's made and executed over there, in the SPA, against the API's responses.

What's architecture vs. framework — the exception that proves the rule: Here the decision itself shifts, not just its expression. The architectural questions survive the stack untouched — first-paint data vs. deferred, where the trust boundary sits, whether a BFF mediates. But the feasible answers are constrained by the execution model: Nuxt's universal rendering lets you answer them inside one codebase with SSR plus hydration and a Nitro BFF; the Laravel-API-plus-separate-SPA model forces the client/server boundary to be a hard network and deployment seam between two deployables. This is a real case where the runtime genuinely shapes the architecture — and that's exactly why it's worth dwelling on. The advanced skill isn't just "decisions survive the stack"; it's telling apart a decision shaped by a genuine runtime constraint (this one) from a decision that's merely framework expression (dimensions 1, 3, 4, and 6, where the call is identical and only the keyword changes). Most of architecture is the latter. This dimension is a true instance of the former, and recognizing the difference is the move that separates a senior judgment from a junior one.


Dimension 3: Transaction strategy

The decision (architecture): Which changes must be atomic? The course's answer is identical: updating the ticket status, writing the resolution record, and adjusting the expert's count are one consistency unit — they must all commit or all roll back, because a partial failure leaves the expert's count permanently wrong. So they go in one transaction. The external notification is not in the transaction (it's a slow external call, and holding a transaction open across it would hold locks and risk the cascading contention the reliability chapter warned about) — it happens after commit, asynchronously.

Nuxt (Vue + Nitro): The use case (called from the Nitro route, never inline in the handler) wraps the three writes in the ORM's transaction (Prisma's $transaction / a Drizzle or Knex transaction), commits, then enqueues the notification job.

Laravel: The action wraps the three writes in DB::transaction(...), commits, then dispatches the notification job.

What's architecture vs. framework: The transaction boundary — these three writes together, the external call outside — is the architecture, and it's the same in both. This is a direct application of the consistency lessons (Foundations Chapter 3, and both stack tracks' data chapters): the aggregate of ticket+resolution+count is the strong-consistency unit. The framework difference is trivial — $transaction(...) vs. DB::transaction(...) — a syntax change around an identical decision. The hard part (knowing what must be atomic and what must stay outside the transaction) is stack-independent reasoning.


Dimension 4: Async strategy

The decision (architecture): What happens off the request path, and how is it made reliable? The course's answer, identical in both stacks: the customer notification, survey scheduling, and analytics are not things the resolving expert needs to wait for, so they happen asynchronously via a queue — the request returns as soon as the ticket is resolved. Because queues deliver at-least-once, the job handlers must be idempotent (the notification must not send twice on redelivery). Failed jobs go to a dead-letter store and are monitored. The reactions can be modeled as a domain event (TicketResolved) with independent listeners — choreography — so new reactions can be added without touching the resolve logic.

Nuxt (Vue + Nitro): The use case emits a TicketResolved event / enqueues jobs; how they run follows the deployment target — on a long-running Node server a BullMQ worker pulls from a Redis queue, while on serverless the job goes to a managed queue (SQS / Cloud Tasks) that triggers a function per job. Either way the handlers (notification, survey, analytics) are idempotent, and failures land in a dead-letter queue with monitoring. (The deployment target is chosen early, because it bounds the async design — building for a persistent worker and then deploying serverless gives you something that can't run.)

Laravel: The action dispatches TicketResolved; queued listeners handle notification, survey, analytics, each idempotent; failures land in failed_jobs with monitoring (Horizon).

What's architecture vs. framework: Every decision is the same — what's async, the event-with-listeners choreography, the at-least-once-so-idempotent requirement, the dead-letter-with-monitoring safety net. These come straight from Foundations Chapters 4 and 5 and are pure architecture. The framework supplies different names for identical concepts: BullMQ or a managed queue vs. Laravel queues, an event emitter vs. Laravel's event system, a DLQ vs. the failed_jobs table, a queue dashboard vs. Horizon. The idempotency requirement in particular is framework-independent — it's forced by the at-least-once delivery model that every queue shares (true on a Node worker, a serverless trigger, and a Laravel listener alike), which is exactly why both stack chapters taught it identically.


Dimension 5: Testing approach

The decision (architecture): How do you verify it, and what makes that possible? The course's answer is the same: the boundaries make the resolve rule unit-testable without the framework (a fake repository, no database, no HTTP); the integration seams — the transactional write against a real database, the authorization, the full request — get integration tests against real infrastructure; and an architectural fitness function enforces that the domain stays framework-independent. The test portfolio is balanced the same way and weighted toward the integration seams where an I/O-heavy feature actually breaks.

Nuxt (Vue + Nitro): Unit test of ResolveTicket with an in-memory repository (Vitest); integration test of the Nitro route's transactional write and the endpoint against a containerized Postgres; and, because this is fullstack, the client half too — a component test of the resolve UI (Vue Test Utils / Testing Library, data in via props, action out via emitted event) and a sparing end-to-end test (Playwright) that drives a real browser through the real resolve flow, catching the bugs that live in the crossing: SSR/hydration mismatches and serialization slips. A dependency-cruiser fitness function keeps the domain off the framework and bans client code from importing server/ modules.

Laravel: Unit test of the ResolveTicket action with a fake repository (Pest); feature tests of the transactional write and the endpoint (including the 403 authorization case) against a test database; a Pest arch test keeps the domain off Illuminate. (The split-out SPA carries its own component and e2e suite in its own repo.)

What's architecture vs. framework: The testing strategy — what's unit-testable and why, which seams get integration coverage, the architectural fitness function, the portfolio balance weighted toward the seams — is identical and is the architecture. The frameworks differ in tooling: Vitest/Vue Test Utils/Playwright vs. Pest, dependency-cruiser vs. the Pest arch plugin, Testcontainers vs. Laravel's test-database tooling. The one genuine scope difference traces straight back to Dimension 2: the Nuxt suite tests a client/server crossing that lives inside one codebase, while the Laravel API's frontend is tested separately because it is separate. And note the deep point both testing chapters made: the reason the rule is unit-testable in either stack is the boundaries from Dimension 1 — testability is a structural property of the architecture, not a feature of the test framework.


Dimension 6: Deployment and operations

The decision (architecture): How does it run, scale, and stay observable in production? The course's answer is the same: separate worker processes for the async jobs (scaled independently of the web tier), horizontal scaling of stateless processes behind a load balancer, shared state (sessions, caches that must be consistent) in an external store rather than process memory, observability across logs/metrics/traces with the async job failures made visible, and health checks plus graceful shutdown so deploys don't drop in-flight resolves or abandon half-done jobs.

Nuxt (Vue + Nitro): On a Node-server deployment, multiple stateless processes plus separate BullMQ worker processes (on serverless, the platform scales per request and a managed queue triggers job functions); Redis for shared state; structured logs (Pino); metrics including the Node-specific ones — event-loop lag and SSR render time, both of which sit on the critical path; OpenTelemetry tracing; liveness/readiness checks; graceful shutdown draining in-flight SSR renders and jobs. Plus the half a backend never has: client-side observability — error tracking for JS exceptions and hydration mismatches (Sentry), and Real User Monitoring / Core Web Vitals, because the server can be fast while the browser is slow, and only RUM tells you which.

Laravel: Multiple stateless PHP-FPM processes plus separate queue:work worker processes, Redis for shared state and queues, structured logs and metrics, tracing, health checks, graceful worker shutdown so jobs finish or safely requeue. (The split-out SPA is its own deployable with its own client-side observability and release cadence.)

What's architecture vs. framework: The operational architecture — separate workers, stateless horizontal scaling, external shared state, the three observability pillars, health checks and graceful shutdown — is the same, and it's the concrete form of the "operational maturity" that Foundations Chapter 2 named as the precondition for distribution. Two differences are real rather than cosmetic, and both trace to earlier dimensions: Nuxt's universal rendering means SSR render time and event-loop lag are first-class metrics and that you must observe both sides of the app (Dimension 2), whereas the Laravel API observes its server side and leaves the SPA's client observability to the SPA's own deployment. The framework-level specifics (Node's per-process event-loop model vs. PHP-FPM's process model) are tuning details inside an otherwise identical operational shape.


Reading the comparison

Step back from the six dimensions and the pattern is almost unmistakable — with one deliberate exception. In five of the six — boundaries, transactions, async, testing, operations — the decisions were identical across both stacks; only the tools changed. Where the boundaries go, what's transactional, what's async and why it must be idempotent, what's unit- vs. integration-tested, how it scales and stays observable — every one of these was the same call in Nuxt and in Laravel. The differences were all at the level of which library, which keyword, which class name — a shared schema or form requests, $transaction or DB::transaction, BullMQ or Laravel queues, Vitest or Pest. That sameness is not a coincidence; it's the definition of architecture. Architecture is the set of decisions that don't depend on your stack, and you've just watched them survive a complete change of stack untouched.

The sixth dimension — rendering and the client/server boundary — is where it gets interesting, and where a lazy version of this lesson would mislead you. There, the decision genuinely shifted: Nuxt's universal rendering collapses client and server into one codebase with SSR and hydration and a Nitro BFF, while the Laravel-API-plus-separate-SPA model forces the client/server boundary into a hard network and deployment seam. But look carefully at what shifted. The architectural questions — what does first paint need, where's the trust boundary, is there a BFF — were the same in both; it was the feasible answers that the runtime constrained. That's the real lesson, sharper than "everything's the same": most decisions are framework-independent and survive any stack, but a few are genuinely shaped by the runtime's execution model, and the senior skill is telling the two apart — not mistaking a framework keyword for an architectural choice, and not mistaking a genuine runtime constraint for something you were free to decide.

There's a second lens worth laying over the comparison, because "same decision, different mechanism" can still hide a real quality difference. When two stacks make the identical architectural call, the way each expresses it differs in complexity — and Ousterhout's two sources of complexity, dependencies and obscurity (Foundations Chapter 1), are a sharper measure than a vague "which is cleaner." Framework magic — convention-over-configuration, the service container's auto-wiring, Nitro's automatic type inference — is a trade: it removes code you'd otherwise write (fewer visible dependencies) at the risk of obscurity, behavior that happens somewhere you can't see. Nuxt's inferred end-to-end types remove a whole class of dependency you'd otherwise hand-maintain — a genuine win. A container binding three layers together with no visible wiring removes boilerplate but can obscure what actually depends on what. So when the decision is the same, the useful question isn't "which framework is cleaner," it's "which one hides the things you don't need to see, and which hides things you do" — and that's a judgment you make per feature, not per framework.

This has a liberating consequence for your career: the architectural skill transfers almost completely. An engineer who has internalized these decisions can walk into a Go service, a Python backend, a Rust system, and make the same sound calls on five dimensions out of six, because those calls were never about the language — they were about trade-offs, boundaries, consistency, failure, and communication, which are universal. The sixth they'll reason about with the same lens, asking the same questions, and reading off the answers the new runtime makes feasible. The framework is something you can learn in weeks; the judgment — including the judgment about which decisions the runtime gets a vote in — is the thing worth years, and it's yours to carry anywhere.

That's the synthesis: one feature, two stacks, six dimensions, and a single conclusion — the architecture is what stayed the same, and the one place it moved, it moved for a reason you can name. Now you'll prove it to yourself by building one, twice, in the capstone.


Practice

This chapter's practice is lighter, because the capstone is the real exercise. Budget about an hour.

Exercise 1: Run the comparison yourself (30 min)

Take a feature you know well from a real system (not "resolve a ticket" — something from your own work). Walk it across the same six dimensions — boundaries, rendering & the client/server boundary, transactions, async, testing, operations — and for each, write down what the decision is, separately from how your framework happens to express it. The skill is separating the two columns: decision vs. mechanism. Pay special attention to the rendering/boundary dimension: is your client/server seam a code-split inside one app, or a network seam between two deployables — and was that a decision you made or one the stack made for you?

Exercise 2: Predict the other stack (30 min)

For that same feature, imagine implementing it in a stack you don't normally use (if you're a Nuxt dev, imagine a Laravel API with a separate SPA, or vice versa; if you know both, pick a third like Go or Python). For each of the six dimensions, predict what would stay the same (the architecture) and what would change (the framework). When you hit a decision that would genuinely change — not just its expression but the decision itself — examine it closely: is it a real runtime constraint (like the rendering/boundary dimension), or a framework constraint you'd mistaken for architecture? That distinction is the whole exercise.

Deliverable

A two-column table for one real feature: for each of the six dimensions, the architectural decision in one column and the framework mechanism in the other. The point is a clean separation — anyone reading the decision column should understand the architecture without needing to know your stack. Mark the one or two rows (if any) where the decision itself — not just the mechanism — would differ across stacks, and write one sentence on why the runtime, not fashion, forced it.

Check yourself

  • State, in one sentence, the difference between architecture and framework as this chapter draws it.
  • For each of the six dimensions, give an example of a decision that stays the same across stacks and a mechanism that changes.
  • Explain why the rendering/client-server-boundary dimension is the exception — what stays the same there (the questions) and what genuinely shifts (the feasible answers), and why the runtime is responsible.
  • Explain why testability being a structural property (not a test-framework feature) supports the architecture-vs-framework thesis.
  • Explain why the architectural skill transfers to a language you've never used — and where you'd expect to have to think harder.

What a strong answer demonstrates: the thesis sentence should be sharp — architecture is the set of decisions that survive a change of stack; the framework is what doesn't — and you should be able to give, for at least a few dimensions, a paired example of an identical decision (e.g. the transaction boundary) versus a changed mechanism ($transaction vs. DB::transaction). The answer that separates senior from junior judgment handles Dimension 2 with care: it shows that the questions (first-paint data, the trust boundary, whether a BFF mediates) stay the same while the runtime constrains the feasible answers — and so distinguishes a genuine runtime constraint from a framework keyword mistaken for an architectural choice. Citing testability-as-structural as evidence (the same boundaries make the feature testable in both stacks) is the mark of a complete answer.

Going deeper (optional)

  • Software Architecture: The Hard Parts (Ford, Richards, Sadalage & Dehghani, O'Reilly, 2021) — the entire book is trade-off analysis at the level that transcends any stack; its cross-cutting treatment of decomposition, data, and workflow is the deep version of this chapter's "what's architecture" line.
  • Primary source: Martin Fowler, Patterns of Enterprise Application Architecture (Addison-Wesley, 2002) — the canonical demonstration of the chapter's thesis: a catalog of patterns (layering, Service Layer, Repository, Unit of Work) that recur across languages and frameworks, i.e. the "architecture that survives the change of stack" made into a reference.
  • Primary source: D. L. Parnas, On the Criteria To Be Used in Decomposing Systems into Modules (Communications of the ACM, 1972) — the seminal paper on drawing boundaries to hide what's likely to change; it is the foundational argument behind Dimension 1 (where the boundaries go) being architecture rather than framework.
  • Revisit any foundations chapter (Tier 1) alongside its two stack realizations (the matching Tier 2a and 2b chapters) — reading the three together is itself the synthesis exercise, and makes the architecture-vs-framework line concrete chapter by chapter. The Nuxt track's rendering, data-fetching-boundary, and operational chapters are the deep background for this chapter's Dimension 2.

Key takeaways

  • Architecture is what survives a change of stack; framework is what doesn't. Implementing one feature in two stacks reveals the line.
  • Across five of the six dimensions — boundaries, transactions, async, testing, operations — the decisions were identical in Nuxt and Laravel; only the tools differed.
  • The sixth — rendering and the client/server boundary — is the instructive exception: the questions stay the same, but the feasible answers are genuinely shaped by the runtime (Nuxt's universal rendering and Nitro BFF vs. a Laravel API with a separate SPA). The senior skill is telling a real runtime constraint apart from a mere framework difference.
  • Testability being a structural property of the boundaries (not a test-framework feature) is one of the clearest demonstrations: the same boundaries make the feature testable in both stacks.
  • The architectural skill transfers almost completely — to Go, Python, Rust, anything — because the decisions were rarely about the language. The framework takes weeks to learn; the judgment, including which decisions the runtime gets a vote in, is worth years.

Next: the Capstone — you'll build one feature two ways along a decision axis you choose from a catalog (build-vs-buy, decomposition, consistency, sync-vs-async control flow, deterministic-vs-AI, or data-store strategy), document the decisions as ADRs, and write a trade-off review comparing them. It's where everything in the course comes together in something you build.