Banyak proses bisnis modern tidak selesai dalam satu request HTTP. Approval berjenjang, onboarding pelanggan, pembuatan invoice, sinkronisasi antar layanan, atau provisioning akun sering melibatkan banyak langkah, dependensi eksternal, dan jeda waktu yang panjang. Jika semua ini ditangani hanya dengan controller API, cron, dan tabel status buatan sendiri, kompleksitas akan cepat meningkat: retry menjadi rapuh, timeout sulit dikelola, state tersebar, dan debugging eksekusi lintas layanan menjadi mahal.
Di sinilah Temporal relevan. Temporal adalah platform workflow orchestration yang dirancang untuk proses tahan gagal, tahan restart, dan cocok untuk eksekusi jangka panjang. Sementara itu, Nuxt 3 dapat menjadi lapisan aplikasi web yang memicu workflow, menampilkan status ke pengguna, dan menyediakan UX yang tetap responsif walau proses di backend berjalan dalam hitungan menit, jam, bahkan hari.
Artikel ini membahas integrasi Nuxt 3 dengan Temporal secara praktis dan teknis. Kita akan fokus pada pola arsitektur, implementasi dasar workflow dan activity, strategi retry dan timeout, kompensasi, observabilitas, serta desain UI yang tidak memblokir interaksi pengguna.
Mengapa Temporal untuk proses bisnis jangka panjang
Temporal memisahkan workflow definition dari eksekusi kerja aktual. Secara sederhana:
- Workflow mendefinisikan urutan proses, state, percabangan, penjadwalan timer, penanganan sinyal, dan logika orkestrasi.
- Activity menjalankan pekerjaan yang berinteraksi dengan dunia luar: database, API eksternal, email, payment gateway, ERP, atau layanan internal.
- Worker adalah proses Node.js yang menjalankan workflow dan activity sesuai task queue.
Keuntungan utamanya bukan sekadar antrean job. Temporal menyimpan histori workflow secara durable sehingga eksekusi bisa dilanjutkan walau worker mati, container direstart, atau deploy baru dilakukan. Anda tidak perlu membangun sendiri mekanisme state machine, retry policy, scheduler, dan recovery.
Untuk proses seperti approval invoice, manfaat ini sangat nyata:
- Request dari UI tidak perlu menunggu semua langkah selesai.
- Jika layanan notifikasi gagal, activity bisa di-retry otomatis.
- Jika approval menunggu manusia selama beberapa hari, workflow tetap hidup tanpa loop polling manual.
- Jika langkah terakhir gagal dan perlu rollback, kompensasi dapat dijalankan secara terstruktur.
Arsitektur Nuxt 3 + Temporal yang masuk akal
Pemisahan tanggung jawab
Pola yang umum dan sehat adalah memisahkan tiga komponen:
- Nuxt 3 App: frontend dan server API ringan untuk menerima aksi pengguna.
- Temporal Worker Service: proses backend yang menjalankan workflow dan activity.
- Service pendukung: database aplikasi, email provider, payment gateway, ERP, CRM, atau microservice lain.
Nuxt tidak perlu menjalankan worker di proses yang sama. Secara operasional, lebih aman jika worker dipisah karena karakter bebannya berbeda. Nuxt fokus pada request-response web, sedangkan worker fokus pada background execution jangka panjang.
Alur integrasi dasar
Contoh alur untuk approval invoice:
- Pengguna klik Ajukan Invoice di aplikasi Nuxt.
- Nuxt memvalidasi input dan menyimpan data awal ke database aplikasi.
- Nuxt memanggil Temporal Client untuk memulai workflow dengan workflowId yang stabil.
- Temporal menjalankan langkah-langkah: validasi, kirim notifikasi approver, tunggu approval, generate invoice final, sinkronisasi ke sistem akuntansi.
- Nuxt menampilkan status proses berdasarkan data workflow atau status yang disinkronkan ke database.
- Jika approver menyetujui atau menolak, Nuxt mengirim signal ke workflow yang sedang berjalan.
Dengan model ini, request HTTP dari user tetap cepat. Durasi proses bisnis tidak lagi mengikat durasi request.
Komponen inti: workflow, activity, signal, retry, dan timeout
Workflow: tempat logika orkestrasi
Workflow sebaiknya berisi logika bisnis tingkat tinggi, bukan akses database atau HTTP call langsung. Di Temporal, workflow harus bersifat deterministik. Artinya, hindari memanggil API acak, membaca waktu sistem langsung, atau melakukan operasi I/O di dalam workflow. Semua interaksi ke luar harus dibungkus sebagai activity.
Contoh workflow approval invoice:
import { proxyActivities, defineSignal, setHandler, condition } from '@temporalio/workflow'
const { validateInvoice, notifyApprover, finalizeInvoice, rejectInvoice } = proxyActivities({
startToCloseTimeout: '1 minute',
retry: {
initialInterval: '2s',
backoffCoefficient: 2,
maximumAttempts: 5,
},
})
export const approvalSignal = defineSignal<[{
approved: boolean
approverId: string
note?: string
}]>('approvalSignal')
export async function invoiceApprovalWorkflow(input: {
invoiceId: string
requesterId: string
amount: number
}) {
await validateInvoice(input)
await notifyApprover(input)
let approval: { approved: boolean; approverId: string; note?: string } | null = null
setHandler(approvalSignal, (payload) => {
approval = payload
})
const received = await condition(() => approval !== null, '7 days')
if (!received) {
throw new Error('Approval timeout')
}
if (approval!.approved) {
return await finalizeInvoice({ ...input, approval: approval! })
}
await rejectInvoice({ ...input, approval: approval! })
return { status: 'rejected' }
}Di sini ada beberapa konsep penting:
- Signal dipakai untuk menerima keputusan approval dari luar workflow.
- condition memungkinkan workflow menunggu event tanpa polling manual.
- Timeout untuk approval dapat dinyatakan eksplisit, misalnya 7 hari.
- Retry policy pada activity membuat kegagalan sementara tidak langsung mematikan proses.
Activity: tempat interaksi dengan dunia luar
Activity menjalankan pekerjaan nyata. Di sinilah Anda melakukan query database, memanggil API pihak ketiga, mengirim email, atau memperbarui sistem lain.
export async function validateInvoice(input: { invoiceId: string; amount: number }) {
if (input.amount <= 0) {
throw new Error('Invalid invoice amount')
}
return { valid: true }
}
export async function notifyApprover(input: { invoiceId: string; requesterId: string }) {
// contoh: kirim email / slack / buat notifikasi internal
return { notified: true }
}
export async function finalizeInvoice(input: any) {
// contoh: simpan status final, generate PDF, sinkronisasi ke ERP
return { status: 'approved', invoiceNumber: 'INV-2026-001' }
}
export async function rejectInvoice(input: any) {
// contoh: update status invoice menjadi ditolak
return { status: 'rejected' }
}Activity boleh gagal, timeout, atau di-retry. Namun Anda perlu memperhatikan idempotensi. Jika activity seperti sinkronisasi ke ERP diulang beberapa kali, hasilnya harus tetap aman. Gunakan idempotency key atau cek status sebelum menulis ulang.
Retry otomatis dan kapan tidak boleh dipakai membabi buta
Retry adalah salah satu alasan utama memakai Temporal, tetapi bukan berarti semua error harus diulang. Bedakan:
- Transient error: jaringan putus, timeout API, rate limit sementara. Ini cocok di-retry.
- Business error: nominal invoice tidak valid, approver tidak berwenang, data wajib kosong. Ini sebaiknya gagal cepat tanpa retry berkali-kali.
Praktiknya, Anda bisa mengklasifikasikan error di activity dan mengatur retry policy seperlunya. Retry yang terlalu agresif bisa membebani layanan eksternal atau memperparah antrian.
Timeout yang perlu dipahami
Timeout bukan cuma satu jenis. Dalam desain workflow, pikirkan beberapa level:
- Activity timeout: batas maksimal satu pekerjaan teknis, misalnya panggilan API 30 detik.
- Workflow timeout: batas total proses jika memang perlu dibatasi.
- Business deadline: misalnya approval harus masuk dalam 7 hari. Ini lebih cocok dimodelkan di workflow menggunakan timer atau condition timeout.
Kesalahan umum adalah menyamakan timeout teknis dengan SLA bisnis. Approval 7 hari tidak berarti request HTTP harus hidup 7 hari; justru itu alasan workflow perlu dipisah dari request-response.
Bagaimana Nuxt 3 memicu workflow dan menampilkan status
Endpoint server Nuxt untuk memulai workflow
Di Nuxt 3, Anda dapat membuat endpoint server yang menerima aksi dari UI dan memulai workflow melalui Temporal Client.
// server/api/invoices/start.post.ts
import { Client, Connection } from '@temporalio/client'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const connection = await Connection.connect({
address: process.env.TEMPORAL_ADDRESS,
})
const client = new Client({ connection })
const handle = await client.workflow.start('invoiceApprovalWorkflow', {
taskQueue: 'business-workflows',
workflowId: `invoice-${body.invoiceId}`,
args: [{
invoiceId: body.invoiceId,
requesterId: body.requesterId,
amount: body.amount,
}],
})
return {
workflowId: handle.workflowId,
runId: handle.firstExecutionRunId,
status: 'started',
}
})workflowId yang stabil penting untuk mencegah duplikasi proses. Misalnya, satu invoice sebaiknya hanya punya satu workflow aktif. Jika endpoint terpanggil dua kali karena user menekan tombol dua kali, Anda bisa mendeteksi konflik berdasarkan workflowId.
Endpoint untuk mengirim signal approval
// server/api/invoices/approve.post.ts
import { Client, Connection } from '@temporalio/client'
export default defineEventHandler(async (event) => {
const body = await readBody(event)
const connection = await Connection.connect({
address: process.env.TEMPORAL_ADDRESS,
})
const client = new Client({ connection })
const handle = client.workflow.getHandle(`invoice-${body.invoiceId}`)
await handle.signal('approvalSignal', {
approved: body.approved,
approverId: body.approverId,
note: body.note,
})
return { ok: true }
})Signal memungkinkan UI atau sistem lain mendorong event ke workflow tanpa perlu menyimpan polling state machine secara manual.
Menampilkan status ke user tanpa membuat UI terasa lambat
UI sebaiknya tidak menunggu workflow selesai. Pola yang lebih baik:
- Setelah workflow dimulai, kembalikan respons cepat ke browser.
- Tampilkan status awal seperti Menunggu Approval atau Sedang Diproses.
- Ambil status terbaru lewat polling ringan, SSE, WebSocket, atau status yang disinkronkan ke database aplikasi.
Untuk banyak aplikasi bisnis, pendekatan paling sederhana adalah Temporal sebagai source of orchestration dan database aplikasi sebagai read model untuk UI. Activity dapat memperbarui tabel status yang mudah dibaca UI, misalnya invoice_process_status. Ini membuat halaman Nuxt tidak harus query histori workflow langsung setiap saat.
Pola ini juga membantu jika Anda ingin membuat dashboard, filter, atau pencarian status dengan SQL biasa.
Kompensasi dan saga untuk langkah yang tidak atomik
Dalam proses bisnis nyata, sering ada urutan langkah yang tidak bisa dibungkus dalam satu transaksi database. Contoh onboarding pelanggan:
- Buat data pelanggan internal.
- Buat akun di IAM eksternal.
- Aktifkan paket billing.
- Kirim email selamat datang.
Jika langkah 3 gagal setelah langkah 1 dan 2 berhasil, Anda perlu kompensasi, bukan rollback database biasa. Temporal cocok untuk pola saga semacam ini.
export async function customerOnboardingWorkflow(input: any) {
const completed: string[] = []
try {
await createCustomerRecord(input)
completed.push('customer')
await provisionIdentity(input)
completed.push('identity')
await activateBilling(input)
completed.push('billing')
await sendWelcomeEmail(input)
return { status: 'completed' }
} catch (err) {
if (completed.includes('billing')) {
await deactivateBilling(input)
}
if (completed.includes('identity')) {
await revokeIdentity(input)
}
if (completed.includes('customer')) {
await markCustomerFailed(input)
}
throw err
}
}Trade-off-nya: kompensasi tidak selalu benar-benar mengembalikan sistem ke keadaan semula. Misalnya email yang sudah terlanjur terkirim tidak bisa ditarik kembali. Karena itu, desain langkah harus mempertimbangkan urutan yang meminimalkan dampak irreversible terlalu dini.
Observabilitas, debugging, dan operasi
Apa yang perlu dipantau
Workflow yang berjalan lama menuntut observabilitas yang baik. Minimal, pantau:
- Status workflow: running, completed, failed, timed out, canceled.
- Durasi per langkah dan jumlah retry activity.
- Error yang sering muncul pada integrasi eksternal.
- Antrian task queue dan kesehatan worker.
- Korelasi antara workflowId, entity bisnis, dan user action.
Temporal menyediakan histori eksekusi yang sangat berguna untuk debugging. Anda bisa melihat langkah mana yang gagal, activity mana yang di-retry, signal kapan diterima, dan timer kapan dipicu.
Tips praktis observabilitas
- Simpan workflowId di tabel bisnis Anda agar mudah ditelusuri dari UI ke backend.
- Log setiap activity dengan correlation ID yang sama.
- Expose status yang ramah pengguna di read model, tetapi simpan detail teknis di log dan histori workflow.
- Bedakan error operasional dan error bisnis agar dashboard tidak menyesatkan.
Kesalahan umum saat debugging
- Menaruh I/O di workflow: ini melanggar determinisme dan memicu perilaku aneh saat replay.
- Activity tidak idempotent: retry menyebabkan data ganda atau side effect berulang.
- Terlalu bergantung pada polling agresif dari UI: halaman menjadi boros resource.
- Tidak mendesain workflowId dengan benar: proses duplikat sulit dicegah.
Pola desain yang disarankan untuk aplikasi nyata
1. Gunakan workflow untuk orkestrasi, bukan sebagai pengganti semua service logic
Temporal kuat untuk koordinasi proses lintas langkah dan lintas waktu. Namun validasi domain inti, aturan akses, dan model data aplikasi tetap sebaiknya hidup di service/domain layer Anda. Workflow memanggil capability tersebut melalui activity.
2. Buat read model status untuk UI
Alih-alih menjadikan histori workflow sebagai satu-satunya sumber tampilan, sinkronkan milestone penting ke tabel status yang mudah dibaca Nuxt. Ini memudahkan pencarian, list page, dashboard, dan notifikasi.
3. Gunakan signal untuk interaksi manusia
Approval, revisi data, atau konfirmasi user sangat cocok dimodelkan sebagai signal. Ini lebih bersih daripada menyimpan loop polling atau cron yang terus mengecek database untuk perubahan status.
4. Definisikan timeout bisnis secara eksplisit
Setiap proses lama sebaiknya punya deadline yang jelas: approval 7 hari, sinkronisasi 2 jam, onboarding 1 hari kerja. Deadline ini harus tercermin di workflow dan juga di UI agar ekspektasi pengguna konsisten.
5. Rancang kompensasi sejak awal
Jika proses menyentuh banyak sistem, tanyakan dari awal: jika langkah keempat gagal, apa yang harus dibatalkan? Semakin awal pertanyaan ini dijawab, semakin sedikit kondisi setengah jadi yang akan menyulitkan operasi.
Kapan pendekatan ini layak dipakai
Nuxt 3 + Temporal sangat cocok jika aplikasi Anda memiliki salah satu karakteristik berikut:
- Proses lintas layanan dengan banyak langkah dan kegagalan sementara.
- Proses yang menunggu interaksi manusia atau event eksternal.
- Kebutuhan audit trail yang jelas atas eksekusi proses.
- Retry, timeout, dan recovery yang tidak ingin dibangun manual.
Namun, untuk tugas sederhana seperti kirim email satu kali setelah form submit, antrean job biasa mungkin sudah cukup. Temporal memberi nilai terbesar ketika kompleksitas orkestrasi mulai nyata, bukan untuk semua background task kecil.
Penutup
Menggabungkan Nuxt 3 dengan Temporal memberi pemisahan yang sehat antara pengalaman pengguna yang cepat dan proses backend yang panjang serta tahan gagal. Nuxt menangani interaksi web, validasi awal, dan presentasi status. Temporal menangani orkestrasi workflow, retry otomatis, timeout, waiting state, signal, dan recovery saat terjadi kegagalan.
Untuk kasus approval, onboarding, invoice, atau sinkronisasi antar layanan, pendekatan ini jauh lebih terstruktur dibanding mengandalkan controller, cron, dan tabel status ad hoc. Kuncinya adalah memahami batas tanggung jawab: workflow untuk orkestrasi deterministik, activity untuk I/O nyata, read model untuk UI, dan kompensasi untuk langkah yang tidak atomik.
Jika diterapkan dengan disiplin—terutama soal idempotensi, timeout, workflowId, dan observabilitas—Anda akan mendapatkan proses bisnis yang lebih dapat diandalkan, lebih mudah diaudit, dan lebih mudah di-debug saat sistem tumbuh semakin kompleks.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!