読み込み中...
Building web applications that scale requires deliberate architectural decisions made early in the development process. This guide covers the major architectural patterns, their tradeoffs, and practical guidance for choosing the right approach.
Web architecture exists on a spectrum from simple to complex. The key is choosing the right level of complexity for your current needs while leaving room to evolve.
Simple <------------------------------------------> Complex
Static Site -> SSR Monolith -> Modular Monolith -> Microservices
The golden rule: Start with the simplest architecture that meets your requirements. You can always decompose later; premature distribution creates unnecessary complexity.
A single deployable unit containing all application logic: routing, business logic, data access, and background jobs.
src/
app/ # Routes and pages
api/ # API endpoints
[locale]/ # i18n pages
server/
usecases/ # Business logic
repositories/ # Data access
actions/ # Server Actions
client/
components/ # UI components
hooks/ # React hooks
database/ # Schema definitions
This structure keeps everything in one deployable unit while maintaining clean separation of concerns internally.
A monolith organized into well-defined modules with explicit boundaries. Each module owns its data and exposes a public API. Modules communicate through defined interfaces rather than reaching directly into each other's internals.
src/
modules/
auth/
public-api.ts # Exported interface
internal/ # Private implementation
auth.module.ts # Module registration
content/
public-api.ts
internal/
content.module.ts
billing/
public-api.ts
internal/
billing.module.ts
shared/ # Cross-cutting concerns
This is often the ideal architecture for medium-sized teams. You get the operational simplicity of a monolith with the organizational benefits of service boundaries.
An application decomposed into independently deployable services, each owning its own data store and communicating over the network.
Synchronous (Request/Response)
Client -> API Gateway -> Service A -> Service B
<- Response
<- Aggregated Response
Asynchronous (Event-Driven)
Service A -> Message Queue -> Service B
-> Service C
Microservices introduce significant operational complexity:
Do not adopt microservices unless you have the team and infrastructure to support them.
Application logic deployed as individual functions that execute in response to events. The cloud provider manages all infrastructure.
| Platform | Provider | Language Support |
|---|---|---|
| Lambda | AWS | Node.js, Python, Go, Java, Rust |
| Cloud Functions | GCP | Node.js, Python, Go, Java |
| Azure Functions | Azure | Node.js, Python, C#, Java |
| Cloudflare Workers | Cloudflare | JavaScript, Rust (via Wasm) |
Pre-rendered static content served from a CDN, enhanced with JavaScript and APIs. Modern frameworks like Next.js, Nuxt, and Astro blur the line between static and dynamic.
| Strategy | When to Use |
|---|---|
| Static Generation (SSG) | Content that rarely changes (blog, docs) |
| Incremental Static Regen (ISR) | Content that changes periodically (product pages) |
| Server-Side Rendering (SSR) | Personalized or real-time content |
| Client-Side Rendering (CSR) | Highly interactive dashboards behind auth |
Run server logic at CDN edge nodes close to users:
User (Tokyo) -> Edge Node (Tokyo) -> Origin (US) if needed
Response in ~20ms instead of ~200ms
Platforms: Vercel Edge Functions, Cloudflare Workers, Deno Deploy
Caveat: Edge runtimes have a limited API surface. No filesystem access, limited Node.js APIs, and database connections require connection pooling or HTTP-based databases.
Abstract data access behind an interface:
interface IContentRepository {
findById(id: string): Promise<Content | null>;
create(data: CreateInput): Promise<Content>;
}
class DrizzleContentRepository implements IContentRepository {
async findById(id: string) {
return db.query.contents.findFirst({
where: eq(contents.id, id),
});
}
}Benefits: testability, swappable implementations, clear data access boundaries.
Separate read and write models when read and write patterns diverge significantly:
Write Path: Command -> Validate -> Write to DB -> Emit Event
Read Path: Query -> Read from Optimized View/Cache -> Return
Use when reads and writes have different performance characteristics or when you need denormalized read models.
Store every state change as an immutable event rather than the current state. Current state is derived by replaying events. Powerful for audit trails and complex business logic, but adds significant complexity.
Browser Cache -> CDN Cache -> Application Cache -> Database
(seconds) (minutes) (minutes-hours) (source of truth)
| Strategy | Description | Use Case |
|---|---|---|
| Cache-Aside | App checks cache, falls back to DB | General purpose |
| Read-Through | Cache handles DB reads transparently | Simple read-heavy workloads |
| Write-Through | Writes go to cache and DB simultaneously | Consistency-critical data |
| Write-Behind | Writes go to cache, async flush to DB | High write throughput |
WAF / DDoS Protection
API Gateway / Rate Limiting
Auth / Authorization
Input Validation
Application Logic
Every layer provides protection. Never rely on a single layer.
| Factor | Monolith | Modular Monolith | Microservices | Serverless |
|---|---|---|---|---|
| Team size | 1-5 | 3-15 | 10+ | 1-10 |
| Deployment | Simple | Simple | Complex | Simple |
| Scaling | Vertical | Vertical | Independent | Automatic |
| Complexity | Low | Medium | High | Medium |
| Operational cost | Low | Low | High | Variable |
| Time to market | Fast | Fast | Slow | Fast |
Most successful applications follow this progression:
1. Monolith (validate the idea)
2. Modular Monolith (organize as you grow)
3. Extract critical services (scale what needs scaling)
4. Microservices (only if team and traffic demand it)
Do not skip steps. Each stage builds the understanding needed for the next.
Good architecture is not about following trends. It is about making deliberate decisions that match your team's capabilities, your application's requirements, and your business constraints. Start simple, measure real bottlenecks, and evolve intentionally.
コメント