How Architecture Patterns Shape Development
Every web application needs a way to organize code that handles data, logic, and presentation. The architecture pattern you choose determines how these concerns interact and where responsibilities lie within your application, whether on the server or client side.
Different patterns emerged to solve specific problems in web development. Understanding these patterns helps you recognize why certain architectural decisions were made in frameworks and when to apply each approach across the full stack.
The Core Problem: Separation of Concerns
All web application architectures address the same fundamental challenge: how to separate data management, presentation logic, and user interface rendering in a way that makes applications maintainable and scalable, whether that code runs on the server, client, or both.
Consider a typical user profile screen in Linear or Notion. You have user data (name, avatar, settings), logic that determines what to show based on permissions, and the actual interface elements that render this information. The question is: where does each piece live, how do they communicate, and which parts execute where?
MVC: The Controller as Traffic Manager
Model-View-Controller separates concerns into three distinct layers with clear responsibilities.
interface Model {}
interface View {}
interface Controller {}
class UserModel implements Model {}
class ProfileView implements View {
models: [UserModel];
}
class ProfileController implements Controller {
view: ProfileView;
}
The Model owns all data and business logic. In a GitHub profile page, this would include user repositories, contribution data, and follower counts. Models don't know anything about how this data gets displayed.
The View handles pure presentation. It receives data and renders the interface elements: repository cards, commit graphs, and profile information. Views are passive and don't make decisions about what data to show. Notice how the view holds references to the models it needs to display.
The Controller sits between them as a coordinator. When you click "Follow User" on GitHub, the controller receives this input, calls the appropriate model methods to update the follow status, then tells the view to re-render with the updated data. The controller orchestrates the interaction between the view and its models.
This creates a clear data flow: User Input → Controller → Model → Controller → View. The controller orchestrates everything but doesn't contain business logic or presentation code.
MVC works well when you have complex user interactions that require coordination between multiple models and views. Traditional web frameworks like Ruby on Rails use this pattern because HTTP requests map naturally to controller actions that coordinate between database models and response rendering.
MVT: Templates as Pure Presentation
Model-View-Template reorganizes MVC responsibilities by moving presentation logic into the View and making Templates purely declarative.
interface Model {}
interface View {}
type Template = string & { __brand: "template" };
class UserModel implements Model {}
class ProfileView implements View {
models: [UserModel];
template: ProfileTemplate;
}
const ProfileTemplate: Template = "<div>{{user.name}}</div>";
The Model remains the same: data and business logic. In a Notion page, this includes document structure, user permissions, and content data.
The View becomes the logic layer that processes model data for presentation. When displaying a Notion document, the view determines which blocks to show based on user permissions, formats dates and timestamps, and prepares data structures for rendering. Notice how the view now holds both the models and the template it will use for rendering.
The Template becomes a purely static presentation layer with placeholders. Think of it as an HTML template with {{user.name}}
variables that gets filled with processed data from the view. The template is just a string with interpolation markers, completely separated from logic.
The flow becomes: User Input → View → Model → View → Template. The view handles both input processing and presentation logic, while templates remain completely static.
This pattern excels when you need to reuse the same presentation logic across multiple templates, or when designers work directly with template files without touching logic code.
Django popularized this approach because it allows clear separation between Python logic and HTML templates, making it easier for developers and designers to collaborate on server-rendered applications.
MVVM: Two-Way Data Binding
Model-View-ViewModel introduces a ViewModel as a binding layer between data and presentation, enabling reactive updates in both directions.
interface Model {}
interface View {}
interface ViewModel {}
class UserModel implements Model {}
class ProfileView implements View {
viewModel: ProfileViewModel;
}
class ProfileViewModel implements ViewModel {
models: [UserModel];
}
The Model still owns data and business logic. In a Figma-like design tool, this includes canvas objects, layer properties, and project metadata.
The View remains the presentation layer, but now it's bound directly to the ViewModel through reactive connections. When a design element is selected, the properties panel automatically updates. Notice how the view only knows about its ViewModel, not the underlying models directly.
The ViewModel acts as a presentation-focused wrapper around model data. It exposes properties like isSelected
, displayName
, and formattedPosition
that the view can bind to directly. When these properties change, the view updates automatically. The ViewModel holds references to the models and transforms their data for presentation.
The key difference is two-way binding: changes in the view immediately update the ViewModel, which can then update the Model. Move an object in Figma, and its position properties update in real-time without explicit controller coordination.
MVVM shines in applications with rich, interactive interfaces where immediate feedback is essential. WPF and Angular leverage this pattern because both desktop and web applications often need this kind of responsive behavior across client-side interactions.
Component Architecture: Self-Contained Units
Component Architecture collapses the traditional separation by making each component responsible for its own data, logic, and presentation within a defined scope.
interface Model {}
interface Component {}
class ProfileComponent implements Component {
models: [UserModel];
}
A Component combines all three concerns but keeps them organized internally. A UserProfileCard
component in Linear manages its own user data fetching, permission logic for showing edit buttons, and rendering of the profile information. Notice how the component directly holds references to the models it needs, eliminating the need for separate controllers or view models.
Components communicate through well-defined interfaces: props for input data, events for output actions, and composition for building larger interfaces from smaller pieces.
This creates a tree of components where data flows down through props and events bubble up through callbacks. A ProjectBoard
component passes project data to TaskCard
components, which emit onTaskUpdate
events back up the tree.
The architecture scales by composition rather than layering. Instead of one large Model-View-Controller system, you have many small components that each solve a focused piece of the interface puzzle.
React, Vue, and Svelte embrace this pattern because it matches how designers and users think about interfaces: as collections of interactive elements rather than monolithic applications. This works both for client-side applications and server-rendered components.
When Architectures Matter
Each pattern solves different problems across the web development spectrum:
MVC works best for applications with complex business logic that needs coordination across multiple interface sections. Traditional admin panels, content management systems, and server-side web applications benefit from this clear separation between controllers handling HTTP requests, models managing data, and views rendering responses.
MVT excels when you need to generate similar interfaces with different data or when non-developers work directly with presentation files. E-commerce sites, content-heavy applications, and server-side rendered applications often use this approach to separate logic from markup.
MVVM shines in highly interactive applications where user actions need immediate visual feedback. Design tools, dashboards, real-time collaboration interfaces, and rich client-side applications benefit from reactive binding between data and presentation layers.
Component Architecture scales well for applications built from reusable interface elements. Design systems, user-facing applications, modern web frameworks, and any interface that benefits from modular thinking work well with this approach, whether rendered on the server or client.
The Evolution of Web Development Thinking
These patterns reflect the evolution of web development priorities across both server and client environments. MVC emerged when separating business logic from presentation was the primary concern in server-side applications. MVT addressed the need for designer-developer collaboration in template-driven systems. MVVM tackled rich desktop-style interactions moving to the web. Component architecture responds to the need for reusable, composable interfaces at scale across the full stack.
Modern web applications often combine these patterns. A React application might use component architecture for the interface while following MVC principles for API design. A full-stack framework might use MVT for server-side rendering and component architecture for client-side interactivity. Understanding each pattern helps you recognize these combinations and choose the right approach for each layer of your application.
The architecture you choose shapes how developers think about the application, where they look for bugs, and how they add new features. More than just code organization, these patterns are frameworks for thinking about web development problems across the entire application stack.