Arsitektur Software: MVC, CQRS, Event-Driven

Foto oleh Unsplash
Tambahkan CQRS ketika read model dan write model Anda berbeda secara signifikan atau ketika performa read menjadi bottleneck. Dalam MVC biasa, model yang sama melayani pembacaan sekaligus penulisan, yang memaksa trade-off ketika keduanya memiliki kebutuhan scaling yang sangat berbeda. CQRS memungkinkan Anda mengoptimalkan masing-masing sisi secara independen — misalnya, proyeksi read yang di-cache di Redis di samping write model yang ketat — namun menambah kompleksitas, jadi mulailah dengan MVC dan perkenalkan CQRS hanya saat perbedaannya sudah nyata.
Event adalah fakta yang sudah terjadi (misalnya, 'pesanan telah ditempatkan'), Command adalah permintaan agar sesuatu terjadi ('tempatkan pesanan ini'), dan Query meminta state saat ini. Event disiarkan — sejumlah consumer dapat bereaksi terhadapnya — sementara Command diarahkan ke satu handler. Mencampuradukkan konsep-konsep ini menimbulkan kesalahan desain umum seperti menamai event sebagai command atau memperlakukan query sebagai event.
EDA memperkenalkan eventual consistency: ketika order service menerbitkan event dan inventory service mengonsumsinya, ada jendela waktu di mana inventaris belum diperbarui. Ini dapat diterima untuk banyak use case, tetapi berbahaya untuk yang lain, seperti menagih pelanggan sebelum mengonfirmasi ketersediaan inventaris. Untuk alur yang memerlukan konsistensi kuat, implementasikan pola Saga — serangkaian transaksi lokal dengan compensating transaction yang membatalkan langkah sebelumnya jika ada langkah yang gagal.
Di Next.js App Router, React Server Component bertindak sebagai View, Route Handler bertindak sebagai Controller, dan lapisan data Anda — model Prisma, kelas service, atau fungsi repository — bertindak sebagai Model. Rekomendasi dalam postingan ini adalah memilih React Server Component untuk pengambilan data daripada API Route Handler karena RSC menghilangkan satu network round-trip dan mengurangi JavaScript di sisi client. Route Handler sebaiknya hanya digunakan untuk request yang diinisiasi client, seperti pengiriman form atau mutasi dari komponen 'use client'.
Tidak harus. CQRS dan Event Sourcing sering dipasangkan, tetapi Event Sourcing membawa kompleksitas yang jauh lebih tinggi — ia menyimpan urutan event (OrderPlaced, ItemShipped, OrderCancelled) alih-alih state saat ini, dan read model dibangun ulang dengan memutar ulang event tersebut. Anda sebaiknya mengadopsi Event Sourcing hanya ketika benar-benar membutuhkan jejak audit lengkap atau kemampuan memutar ulang history untuk membangun read model baru. Kerangka keputusan dalam postingan ini menempatkannya paling akhir: mulai dengan MVC, tambahkan CQRS jika diperlukan, tambahkan EDA jika diperlukan, dan tambahkan Event Sourcing hanya bila ada kebutuhan yang meyakinkan.

Foto oleh Unsplash
Pola arsitektur software adalah solusi yang dapat digunakan kembali untuk masalah struktural yang berulang dalam desain software. Pola yang tepat dapat membuat aplikasi Anda lebih mudah diskalakan, ditest, dan dikembangkan. Pola yang salah dapat menciptakan kompleksitas yang tidak perlu yang memperlambat tim Anda selama bertahun-tahun. Postingan ini membandingkan tiga pola terpenting — MVC, CQRS, dan Event-Driven Architecture — dengan contoh TypeScript konkret untuk membantu Anda memilih dan menggabungkannya dengan percaya diri.
Model-View-Controller adalah pola arsitektur yang paling banyak digunakan dalam pengembangan web. Ini memisahkan aplikasi menjadi tiga komponen yang saling terhubung: Model (data dan logika bisnis), View (lapisan presentasi), dan Controller (penanganan request dan koordinasi). Keawetan MVC berasal dari kesederhanaan dan keakrabannya yang luas — hampir setiap web framework mendukungnya, dan sebagian besar developer memahaminya segera.
Next.js modern memetakan secara alami ke konsep MVC bahkan tanpa terminologi MVC yang eksplisit. React Server Component bertindak sebagai View, Route Handler bertindak sebagai Controller, dan lapisan data Anda (model Prisma, kelas service, fungsi repository) bertindak sebagai Model. Co-location UI dan pengambilan data di komponen halaman yang sama di App Router sedikit mengaburkan batas, tetapi pemisahan kepentingan tetap ada.
// MVC in a Next.js App Router context
// Model — src/models/post.ts
export interface Post { id: string; title: string; body: string; authorId: string; }
export async function getPostById(id: string): Promise<Post | null> {
return db.post.findUnique({ where: { id } });
}
// Controller (Route Handler) — app/api/posts/[id]/route.ts
import { getPostById } from "@/models/post";
export async function GET(_: Request, { params }: { params: { id: string } }) {
const post = await getPostById(params.id);
if (!post) return Response.json({ error: "Not found" }, { status: 404 });
return Response.json(post);
}
// View — app/blog/[id]/page.tsx (React Server Component)
import { getPostById } from "@/models/post";
export default async function PostPage({ params }: { params: { id: string } }) {
const post = await getPostById(params.id);
return <article><h1>{post?.title}</h1><p>{post?.body}</p></article>;
}Di Next.js App Router, pilih React Server Component untuk pengambilan data daripada API route handler. RSC menghilangkan satu network round-trip dan mengurangi JavaScript di sisi client. Gunakan Route Handler hanya untuk request yang diinisiasi client (pengiriman form, mutasi dari komponen 'use client').
Command Query Responsibility Segregation memisahkan bagian sistem Anda yang mengubah state (Command) dari bagian yang membaca state (Query). Pemisahan ini memungkinkan Anda mengoptimalkan masing-masing secara independen — read model yang dioptimalkan untuk tampilan UI tertentu dapat memiliki bentuk yang sama sekali berbeda dari write model yang menerapkan aturan bisnis. CQRS powerful tetapi menambah kompleksitas; paling dibenarkan dalam sistem di mana read dan write memiliki kebutuhan scaling yang sangat berbeda.
Command mewakili maksud untuk mengubah state: PlaceOrderCommand, CancelSubscriptionCommand. Ia harus divalidasi dan ditangani oleh handler khusus. Query meminta proyeksi state yang dioptimalkan untuk use case tertentu: GetOrderSummaryQuery mengembalikan DTO datar yang dirancang untuk UI daftar pesanan. Read model dapat berupa SQL view, Redis cache, atau database yang dioptimalkan untuk read — CQRS tidak mengatur implementasinya.
// CQRS: separate read models from write models
// Write side — Command
interface PlaceOrderCommand {
userId: string;
items: { productId: string; qty: number }[];
}
async function handlePlaceOrder(cmd: PlaceOrderCommand): Promise<string> {
const orderId = generateId();
await db.orders.insert({ id: orderId, ...cmd, status: "pending" });
await eventBus.publish("order.placed", { orderId, ...cmd });
return orderId;
}
// Read side — Query (optimized read model, possibly from a separate DB/view)
interface OrderSummaryDTO {
orderId: string;
customerName: string;
itemCount: number;
total: number;
status: string;
}
async function getOrderSummary(orderId: string): Promise<OrderSummaryDTO> {
return readDb.orderSummaries.findOne({ where: { orderId } });
}CQRS sering dipasangkan dengan Event Sourcing, di mana write model menyimpan urutan event (OrderPlaced, ItemShipped, OrderCancelled) daripada state saat ini. Read model dibangun dengan memutar ulang event ini. Ini memberi Anda jejak audit lengkap, kemampuan untuk memutar ulang history guna membangun read model baru, dan integrasi alami dengan Event-Driven Architecture. Trade-off-nya adalah kompleksitas yang jauh lebih tinggi — jangan mengadopsi Event Sourcing tanpa kebutuhan yang meyakinkan.
Event-Driven Architecture (EDA) adalah pola di mana komponen berkomunikasi dengan mengeluarkan dan mengonsumsi event secara asinkron melalui message broker (Kafka, RabbitMQ, AWS EventBridge). EDA memisahkan producer dari consumer — order service menerbitkan event 'order.placed' tanpa mengetahui atau peduli layanan mana yang mengonsumsinya. Ini memungkinkan coupling yang sangat longgar, tetapi memerlukan pemikiran matang tentang evolusi schema event, jaminan urutan, dan penanganan error.
Event adalah fakta yang terjadi di masa lalu ('pesanan telah ditempatkan'). Command adalah permintaan agar sesuatu terjadi ('tempatkan pesanan ini'). Query meminta state saat ini. Event disiarkan — sejumlah consumer dapat bereaksi. Command diarahkan — satu handler bertanggung jawab. Memahami perbedaan ini mencegah kesalahan desain umum seperti menamai event sebagai command.
EDA memperkenalkan eventual consistency — ketika order service menerbitkan event dan inventory service mengonsumsinya, ada jendela di mana inventaris belum diperbarui. Ini dapat diterima untuk banyak use case tetapi berbahaya untuk yang lain (misalnya, menagih pelanggan sebelum mengonfirmasi inventaris). Selalu analisis kebutuhan konsistensi Anda sebelum mengadopsi EDA, dan implementasikan compensating transaction (saga) untuk alur yang memerlukan konsistensi kuat.
Ketika operasi mencakup beberapa layanan — tempatkan pesanan, reservasi inventaris, tagih pembayaran — Anda tidak dapat menggunakan database transaction lintas batas layanan. Pola Saga mengorkestrasikan serangkaian transaksi lokal, setiap langkah menerbitkan event yang memicu langkah berikutnya. Jika ada langkah yang gagal, compensating transaction akan membatalkan langkah sebelumnya. Choreography saga terdesentralisasi; orchestration saga menggunakan koordinator terpusat.
Aplikasi nyata jarang menggunakan satu pola secara terisolasi. Kombinasi umum: MVC untuk lapisan request/response, CQRS untuk memisahkan jalur read dan write untuk domain yang kompleks, dan EDA untuk mengintegrasikan lintas batas layanan dan menangani async workflow. Kuncinya adalah memahami masalah apa yang dipecahkan setiap pola dan menerapkannya secara bedah pada bagian sistem Anda di mana trade-off masuk akal.
Mulai dengan MVC. Tambahkan CQRS ketika read dan write model Anda berbeda secara signifikan atau ketika performa read menjadi bottleneck. Tambahkan EDA ketika Anda perlu memisahkan layanan, menangani async workflow (notifikasi email, background processing), atau mengintegrasikan dengan sistem eksternal. Tambahkan Event Sourcing hanya ketika Anda benar-benar membutuhkan jejak audit atau kemampuan memutar ulang history.
Dokumentasikan keputusan arsitektur signifikan menggunakan Architecture Decision Records (ADR). ADR menangkap konteks (mengapa kita membuat keputusan ini?), opsi yang dipertimbangkan, keputusan yang dibuat, dan konsekuensinya (positif dan negatif). ADR tinggal di repository Anda bersama kode dan memberikan catatan historis yang sangat berharga tentang mengapa sistem Anda seperti itu — konteks penting untuk onboarding engineer baru.
Gunakan Structurizr DSL atau diagram C4 model untuk mendokumentasikan arsitektur Anda di beberapa level abstraksi (sistem, container, komponen). Diagram otomatis sebagai kode mencegah documentation drift dan memberi anggota tim baru peta sistem yang dapat dinavigasi.
Setiap pola arsitektur ada untuk menyelesaikan masalah tertentu. Cocokkan pola dengan masalah, bukan sebaliknya. MVC menyelesaikan pemisahan presentasi, data, dan logika kontrol. CQRS menyelesaikan ketidakcocokan impedansi antara read dan write yang dioptimalkan. EDA menyelesaikan coupling antara komponen yang perlu bereaksi terhadap perubahan state satu sama lain. Jika ragu, mulai lebih sederhana dan berkembang — kompleksitas arsitektur jauh lebih mudah ditambahkan daripada dihapus.
Konsep arsitektur kunci dalam postingan ini meliputi MVC, CQRS, Event Sourcing, EDA, Saga, and eventual consistency.