Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Event-driven application made easy with Watermill
Search
Sponsored
·
Ship Features Fearlessly
Turn features on and off without deploys. Used by thousands of Ruby developers.
→
Robert Laszczak
April 13, 2019
Technology
250
1
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
Event-driven application made easy with Watermill
Robert Laszczak
April 13, 2019
More Decks by Robert Laszczak
See All by Robert Laszczak
Let's build event-driven application in 15 minutes - introduction to Watermill
roblaszczak
0
840
Wprowadzenie do Watermill - Gophers Silesia
roblaszczak
1
74
Wprowadzenie do Watermill GoCracow
roblaszczak
0
37
Tactical DDD patterns in Go
roblaszczak
0
110
Other Decks in Technology
See All in Technology
薬剤師(ドメインエキスパート)と一緒に育てる薬局向けAIアシスタント
kakehashi
PRO
2
110
JAWS-UG初心者支部#88わいわい初心塾(夏休みの宿題やったかGit編)
otsuki
0
150
PfEingのアプローチで働こう
rindrics
0
180
いま、生成AIにKaggleをどこまで 任せられるか — ROGIIコンペでの進め方とTips
k951286
3
1.4k
Kiro Crewしか勝たん!?
miu_crescent
PRO
0
180
こんなアーキテクチャ図は嫌だ BEYOND THE TIME: 半年後の自分へ贈る15のメッセージ / 15 of Anti-pattern in AWS Architecture Diagrams
naospon
3
260
Oracle Cloud Infrastructure IaaS 新機能アップデート 2026/6 - 2026/8
oracle4engineer
PRO
0
110
Jetpack Compose で挑む新聞紙面UI ─ 複合ジェスチャー・ポリゴン記事領域・適応的ページ構成という3つの壁/droidkaigi2026
nikkei_engineer_recruiting
0
230
GuardDuty 検知対応を DevOps Agent で効率化しようとしている話 / GuardDuty Investigations with DevOps Agent
masahirokawahara
1
320
【視聴者参加型!】AWSセキュリティアンチパターンクイズ
syoshie
0
450
Microsoft 365 Copilot chat -tekoälypalvelun tietosuojaongelmat
hponka
0
460
Bet AI Day 2026丨AIを「使う」から、AIが「働く」へ ― LayerXが進める「組織AI」の社会実装
layerx
PRO
3
2.7k
Featured
See All Featured
Statistics for Hackers
jakevdp
799
230k
[RailsConf 2023] Rails as a piece of cake
palkan
59
7k
Introduction to Domain-Driven Design and Collaborative software design
baasie
1
970
Efficient Content Optimization with Google Search Console & Apps Script
katarinadahlin
PRO
1
840
[SF Ruby Conf 2025] Rails X
palkan
2
1.3k
Leo the Paperboy
mayatellez
8
2.2k
Bootstrapping a Software Product
garrettdimon
PRO
306
120k
Why Our Code Smells
bkeepers
PRO
340
58k
Dealing with People You Can't Stand - Big Design 2015
cassininazir
367
27k
Product Roadmaps are Hard
iamctodd
55
13k
A Modern Web Designer's Workflow
chriscoyier
698
190k
Helping Users Find Their Own Way: Creating Modern Search Experiences
danielanewman
31
3.4k
Transcript
roblaszczak MESSAGE-DRIVEN APPLICATION MADE EASY WITH WATERMILL
roblaszczak MESSAGE-DRIVEN ARCHITECTURE
roblaszczak SCALEABLE RESILIENT LOWER COUPLING
roblaszczak HARDER TO IMPLEMENT HARDER TO DEBUG
roblaszczak BUT… CAN BE BUILDING MESSAGE-DRIVEN APPLICATIONS EASIER?
roblaszczak PROVEN PATTERNS
roblaszczak WATERMILL
roblaszczak COMPANY X ROOM BOOKING PORTAL
roblaszczak
roblaszczak
roblaszczak r.Post("/book-room", func(w http.ResponseWriter, r *http.Request) { // ... if
err := bookingRepo.AddBooking(booking); err != nil { panic(err) } err := paymentsInitializer.InitializePayment(p) if err != nil { panic(err) } })
roblaszczak
roblaszczak
roblaszczak HOW TO FIX IT?
roblaszczak MAYBE MESSAGE-DRIVEN?
roblaszczak WE DIDN’T DONE THAT BEFORE
roblaszczak LET’S EXPERIMENT! LET’S USE WATERMILL
roblaszczak
roblaszczak
roblaszczak err := bookingRepo.AddBooking(booking) //... event, err := json.Marshal(RoomBooked{ BookingUUID:
booking.ID(), RoomID: req.RoomID, Price: booking.Price(), PaymentChannel: req.PaymentChannel, }) //... err := publisher.Publish( "room_bookings", message.NewMessage(watermill.NewUUID(), event), ) //...
roblaszczak import ( "github.com/ThreeDotsLabs/watermill/message/infrastructure/kafka" ) publisher, err := kafka.NewPublisher( []string{"localhost:9092"},
kafka.DefaultMarshaler{}, nil, // Sarama config watermillLogger, )
roblaszczak subscriber, err := kafka.NewSubscriber( kafka.SubscriberConfig{ Brokers: []string{"localhost:9092"}, ConsumerGroup: "some_consumer_group",
}, nil, kafka.DefaultMarshaler{}, watermillLogger, )
roblaszczak watermillRouter, err := message.NewRouter( message.RouterConfig{}, watermillLogger, ) if err
!= nil { panic(err) } watermillRouter.AddMiddleware(watermillMiddleware.Recoverer)
roblaszczak type HandlerFunc func(msg *Message) ([]*Message, error) type HandlerMiddleware func(h
HandlerFunc) HandlerFunc
roblaszczak watermillRouter.AddNoPublisherHandler( "book_room_handler", // handler name "room_bookings", //subscribe topic subscriber,
func(msg *message.Message) ([]*message.Message, error) { event := RoomBooked{} err := json.Unmarshal(msg.Payload, &event) // ... p, err := payment.NewPayment(event.Price, event.Channel) // ... err := paymentsInitializer.InitializePayment(p) // ... return nil, nil }, )
roblaszczak
roblaszczak watermillRouter.AddMiddleware( watermillMiddleware.Recoverer, watermillMiddleware.Retry{ MaxRetries: 3, WaitTime: time.Second*10, }.Middleware, )
roblaszczak
roblaszczak
roblaszczak ANALOGY 400 Bad Request
roblaszczak POISON QUEUE
roblaszczak
roblaszczak pq, err := watermillMiddleware.NewPoisonQueue( publisher, "poison_queue" ) // ...
watermillRouter.AddMiddleware( pq.Middleware, watermillMiddleware.Retry{ MaxRetries: 1, Logger: watermillLogger, }.Middleware, )
roblaszczak pq, err := watermillMiddleware.NewPoisonQueueWithFilter( publisher, "poison_queue", func(err error) bool
{ switch errors.Cause(err).(type) { case *json.InvalidUnmarshalError: return true Default: return false } }, )
roblaszczak FEATURE REQUEST: BOOKINGS COUNTER
roblaszczak JUST A SQL QUERY?
roblaszczak
roblaszczak
roblaszczak
roblaszczak CQRS COMPONENT
roblaszczak func (BookingsCounterGenerator) NewEvent() interface{} { return &event.RoomBooked{} } func
(b BookingsCounterGenerator) Handle(e interface{}) error { event := e.(*event.RoomBooked) return b.i.IncrementBookingCounter(event.BookingUUID) }
roblaszczak FEATURE REQUEST: FULL TEXT SEARCH
roblaszczak
roblaszczak POLYGLOT PERSISTENCE
roblaszczak CONTRACT IS IMPORTANT
roblaszczak
roblaszczak DATA IS NOT UP-TO DATE
roblaszczak EVENTUAL CONSISTENCY
roblaszczak
roblaszczak IN MOST CASES EVENTUAL CONSISTENCY IS ACCEPTABLE
roblaszczak DEBUGGING
roblaszczak CORRELATION ID watermillRouter.AddMiddleware( watermillMiddleware.CorrelationIDWithAutogenerate( func() string { return watermill.NewShortUUID()
}), )
roblaszczak
roblaszczak METRICS prometheusRegistry, closeMetricsServer := metrics.CreateRegistryAndServeHTTP(":8081") defer closeMetricsServer() metricsBuilder :=
metrics.NewPrometheusMetricsBuilder(prometheusRegistry, "", "") metricsBuilder.AddPrometheusRouterMetrics(watermillRouter)
roblaszczak
roblaszczak TRACING IN PROGRESS :)
roblaszczak
roblaszczak
roblaszczak DUPLICATED BOOKINGS
roblaszczak 23:09:35 [INFO] Booking 01D6P9VAFXEGXHAF6MW4ZRZD8V done 23:09:37 [INFO] Booking 01D6P9STY5CCN8RJT4YNTHFZRH
done 23:09:37 [ERROR] Lost Kafka connection 23:09:38 [INFO] Connecting to Kafka 23:09:41 [INFO] Booking 01D6P9STY5CCN8RJT4YNTHFZRH done 23:09:35 [INFO] Booking 01D6P9VTE0QNCESP805MVH9Z1K done
roblaszczak
roblaszczak
roblaszczak
roblaszczak AT-LEAST-ONCE DELIVERY
roblaszczak 23:09:35 [INFO] Booking 01D6P9VAFXEGXHAF6MW4ZRZD8V done 23:09:37 [INFO] Booking 01D6P9STY5CCN8RJT4YNTHFZRH
done 23:09:37 [ERROR] Lost Kafka connection 23:09:38 [INFO] Connecting to Kafka 23:09:41 [INFO] Booking 01D6P9STY5CCN8RJT4YNTHFZRH done 23:09:35 [INFO] Booking 01D6P9VTE0QNCESP805MVH9Z1K done
roblaszczak SOLUTION: DEDUPLICATION
roblaszczak func (m *MemoryBookings) IncrementBookingCounter(bookingUUID string) error { m.lock.Lock() defer
m.lock.Unlock() if m.bookings == nil { m.bookings = map[string]struct{}{} } if _, ok := m.bookings[bookingUUID]; ok { // deduplicated return nil } m.bookings[bookingUUID] = struct{}{} m.count += 1 return nil }
roblaszczak WHAT’S NEXT?
roblaszczak DON’T BE AFRAID TO EXPERIMENT
roblaszczak roblaszczak https://watermill.io/ github.com/ThreeDotsLabs/watermill github.com/roblaszczak/reservation-system
roblaszczak roblaszczak https://watermill.io/ github.com/ThreeDotsLabs/watermill github.com/roblaszczak/reservation-system THANKS!