Kenapa Event-Driven?
Request-response architecture (REST API) sudah cukup untuk banyak use case, tapi ada saat di mana kita butuh arsitektur yang lebih loose-coupled dan scalable. Di sinilah Event-Driven Architecture (EDA) berperan. Dari beberapa proyek yang saya handle, EDA sangat efektif untuk sistem yang memiliki banyak service yang perlu berkomunikasi tapi tidak boleh tightly coupled.
Contoh nyata: di sebuah startup e-commerce di Bandung, kami migrasi dari synchronous API calls ke event-driven untuk order processing. Hasilnya? Latency turun 60% dan sistem jauh lebih resilient terhadap service failures.
Konsep Dasar Event-Driven
EDA berputar di sekitar konsep event - fakta yang sudah terjadi di sistem. Berikut implementasi dasarnya:
// Event definition
type Event struct {
ID string `json:"id"`
Type string `json:"type"`
Payload map[string]interface{} `json:"payload"`
Timestamp time.Time `json:"timestamp"`
Source string `json:"source"`
Metadata map[string]string `json:"metadata"`
}
// Event Publisher
type EventPublisher interface {
Publish(ctx context.Context, topic string, event Event) error
}
// Event Handler
type EventHandler interface {
Handle(ctx context.Context, event Event) error
EventType() string
}
// Contoh handler untuk order created
type OrderCreatedHandler struct {
inventoryService InventoryService
notificationService NotificationService
}
func (h *OrderCreatedHandler) EventType() string {
return "order.created"
}
func (h *OrderCreatedHandler) Handle(ctx context.Context, event Event) error {
var order OrderCreatedPayload
json.Unmarshal(event.Payload["order"], &order)
// Reserve inventory
if err := h.inventoryService.ReserveStock(ctx, order.ProductID, order.Quantity); err != nil {
return fmt.Errorf("failed to reserve stock: %w", err)
}
// Send confirmation email
return h.notificationService.SendOrderConfirmation(ctx, order)
}Kafka untuk High-Throughput Events
Untuk sistem yang membutuhkan high-throughput event processing, Kafka adalah pilihan utama. Berikut cara saya setup Kafka producer dan consumer di Go:
import "github.com/segmentio/kafka-go"
// Producer
type KafkaPublisher struct {
writer *kafka.Writer
}
func NewKafkaPublisher(brokers []string, topic string) *KafkaPublisher {
return &KafkaPublisher{
writer: &kafka.Writer{
Addr: kafka.TCP(brokers...),
Topic: topic,
Balancer: &kafka.LeastBytes{},
BatchTimeout: 10 * time.Millisecond,
},
}
}
func (p *KafkaPublisher) Publish(ctx context.Context, event Event) error {
data, err := json.Marshal(event)
if err != nil {
return err
}
return p.writer.WriteMessages(ctx, kafka.Message{
Key: []byte(event.Type),
Value: data,
})
}
// Consumer
func StartConsumer(brokers []string, topic, groupID string, handler EventHandler) {
reader := kafka.NewReader(kafka.ReaderConfig{
Brokers: brokers,
GroupID: groupID,
Topic: topic,
MinBytes: 1,
MaxBytes: 10e6,
})
for {
msg, err := reader.ReadMessage(context.Background())
if err != nil {
log.Error().Err(err).Msg("failed to read message")
continue
}
var event Event
json.Unmarshal(msg.Value, &event)
if err := handler.Handle(context.Background(), event); err != nil {
log.Error().Err(err).Msg("failed to handle event")
// Implement retry logic here
}
}
}Saga Pattern untuk Distributed Transactions
Di event-driven architecture, kita tidak bisa menggunakan traditional database transactions lintas service. Solusinya adalah Saga Pattern yang mengkoordinasi beberapa steps dengan compensation logic jika ada kegagalan:
type Saga struct {
steps []SagaStep
compensations []CompensationFunc
}
type SagaStep struct {
Name string
Execute func(ctx context.Context) error
Compensate func(ctx context.Context) error
}
func (s *Saga) Run(ctx context.Context) error {
for i, step := range s.steps {
if err := step.Execute(ctx); err != nil {
// Compensation: undo yang sudah dilakukan
for j := i - 1; j >= 0; j-- {
if compErr := s.steps[j].Compensate(ctx); compErr != nil {
log.Error().Err(compErr).Str("step", s.steps[j].Name).Msg("compensation failed")
}
}
return fmt.Errorf("saga failed at step %s: %w", step.Name, err)
}
}
return nil
}Kesimpulan
Event-Driven Architecture memang lebih complex dari synchronous API, tapi untuk sistem yang membutuhkan scalability, resilience, dan loose coupling, ini adalah pilihan yang tepat. Yang penting adalah mulai dari kebutuhan bisnis yang nyata, bukan karena tren semata.