├── .env.example ├── .gitignore ├── .vscode └── launch.json ├── Makefile ├── README.md ├── cmd └── api │ ├── factory │ ├── app.go │ ├── consumers.go │ └── router.go │ └── main.go ├── docker-compose.yaml ├── docs └── events_diagram.png ├── go.mod ├── go.sum ├── internal ├── order │ ├── application │ │ ├── controller │ │ │ └── order_controller.go │ │ ├── dto │ │ │ └── create_order_dto.go │ │ └── usecase │ │ │ ├── create_order_usercase.go │ │ │ ├── process_order_payment_usecase.go │ │ │ ├── send_orderemail_usecase.go │ │ │ └── stock_movement_usecase.go │ └── domain │ │ ├── entity │ │ ├── order_entity.go │ │ ├── order_item_entity.go │ │ └── product_entity.go │ │ ├── event │ │ ├── order_created_event.go │ │ └── order_paid_event.go │ │ └── queue │ │ └── publisher.go └── user │ ├── application │ ├── controller │ │ └── user_controller.go │ ├── dto │ │ └── create_user_dto.go │ └── usecase │ │ ├── create_user_usecase.go │ │ └── send_welcomeemail_usecase.go │ └── domain │ ├── entity │ └── user_entity.go │ ├── event │ ├── user_registered_event.go │ └── welcome_email_sent_event.go │ └── queue │ └── publisher.go ├── pkg └── queue │ ├── listener.go │ ├── memory_queue_adapter.go │ ├── queue.go │ ├── rabbitmq_adapter.go │ └── response_writer.go └── request.http /.env.example: -------------------------------------------------------------------------------- 1 | QUEUE_URI=amqp://guest:guest@localhost:5672/ -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # If you prefer the allow list template instead of the deny list, see community template: 2 | # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore 3 | # 4 | # Binaries for programs and plugins 5 | *.exe 6 | *.exe~ 7 | *.dll 8 | *.so 9 | *.dylib 10 | 11 | # Test binary, built with `go test -c` 12 | *.test 13 | 14 | # Output of the go coverage tool, specifically when used with LiteIDE 15 | *.out 16 | 17 | # Dependency directories (remove the comment below to include it) 18 | # vendor/ 19 | 20 | # Go workspace file 21 | go.work 22 | bin/ 23 | .env -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Server", 6 | "type": "go", 7 | "request": "launch", 8 | "mode": "auto", 9 | "program": "cmd/api/main.go", 10 | "cwd": "${workspaceFolder}", 11 | "envFile": "${workspaceFolder}/.env", 12 | }, 13 | ] 14 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | #!make 2 | include .env 3 | export $(shell sed 's/=.*//' .env) 4 | 5 | build: 6 | @go build -o bin/server cmd/api/main.go 7 | 8 | server: build up 9 | @./bin/server 10 | 11 | up: 12 | @docker-compose up -d 13 | 14 | down: 15 | @docker-compose down 16 | 17 | test: 18 | @go test -v ./... 19 | 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # event-driven-golang 2 | Event Driven Architecture example in golang 3 | ![events diagram](https://github.com/NiltonMorais/event-driven-golang/blob/main/docs/events_diagram.png?raw=true) 4 | 5 | ## Running local 6 | - run server 7 | ```sh 8 | make server 9 | ``` 10 | 11 | - up infraescructure local with docker (rabbitmq, etc) 12 | ```sh 13 | make up 14 | ``` 15 | 16 | ## Debugging 17 | - Access "Run and Debug" on VsCode and click in play "Server" -------------------------------------------------------------------------------- /cmd/api/factory/app.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | "context" 5 | "log" 6 | "net/http" 7 | "os" 8 | "reflect" 9 | 10 | orderController "github.com/NiltonMorais/event-driven-golang/internal/order/application/controller" 11 | orderUsecase "github.com/NiltonMorais/event-driven-golang/internal/order/application/usecase" 12 | orderEvent "github.com/NiltonMorais/event-driven-golang/internal/order/domain/event" 13 | userController "github.com/NiltonMorais/event-driven-golang/internal/user/application/controller" 14 | userUsecase "github.com/NiltonMorais/event-driven-golang/internal/user/application/usecase" 15 | userEvent "github.com/NiltonMorais/event-driven-golang/internal/user/domain/event" 16 | "github.com/NiltonMorais/event-driven-golang/pkg/queue" 17 | ) 18 | 19 | type Application struct { 20 | queue queue.Queue 21 | userController *userController.UserController 22 | orderController *orderController.OrderController 23 | } 24 | 25 | func NewApplication() (*Application, error) { 26 | // queue := queue.NewMemoryQueueAdapter() 27 | queueUri := os.Getenv("QUEUE_URI") 28 | queue := queue.NewRabbitMQAdapter(queueUri) 29 | 30 | createUserUseCase := userUsecase.NewCreateUserUseCase(queue) 31 | sendWelcomeEmailUseCase := userUsecase.NewSendWelcomeEmailUseCase(queue) 32 | userController := userController.NewUserController(createUserUseCase, sendWelcomeEmailUseCase) 33 | 34 | createOrderUseCase := orderUsecase.NewCreateOrderUseCase(queue) 35 | processOrderPaymentUseCase := orderUsecase.NewProcessOrderPaymentUseCase(queue) 36 | stockMovementUseCase := orderUsecase.NewStockMovementUseCase() 37 | sendOrderEmailUseCase := orderUsecase.NewSendOrderEmailUseCase() 38 | orderController := orderController.NewOrderController(createOrderUseCase, processOrderPaymentUseCase, stockMovementUseCase, sendOrderEmailUseCase) 39 | 40 | return &Application{ 41 | queue: queue, 42 | userController: userController, 43 | orderController: orderController, 44 | }, nil 45 | } 46 | 47 | func (app *Application) RunServer(ctx context.Context) error { 48 | err := app.queue.Connect(ctx) 49 | if err != nil { 50 | return err 51 | } 52 | defer app.queue.Disconnect(ctx) 53 | log.Println("Server is running on port 8080") 54 | err = http.ListenAndServe(":8080", nil) 55 | if err != nil { 56 | return err 57 | } 58 | return nil 59 | } 60 | 61 | func (app *Application) StartConsumingQueues(ctx context.Context) error { 62 | err := app.queue.Connect(ctx) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | OrderCreatedEvent := reflect.TypeOf(orderEvent.OrderCreatedEvent{}).Name() 68 | UserRegisteredEvent := reflect.TypeOf(userEvent.UserRegisteredEvent{}).Name() 69 | 70 | go func(ctx context.Context, queueName string) { 71 | err = app.queue.StartConsuming(ctx, queueName) 72 | if err != nil { 73 | log.Fatalf("Error running consumer %s: %s", queueName, err) 74 | } 75 | }(ctx, OrderCreatedEvent) 76 | 77 | go func(ctx context.Context, queueName string) { 78 | err = app.queue.StartConsuming(ctx, queueName) 79 | if err != nil { 80 | log.Fatalf("Error running consumer %s: %s", queueName, err) 81 | } 82 | }(ctx, UserRegisteredEvent) 83 | 84 | return nil 85 | } 86 | 87 | func (app *Application) DisconnectQueue(ctx context.Context) error { 88 | err := app.queue.Disconnect(ctx) 89 | if err != nil { 90 | return err 91 | } 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /cmd/api/factory/consumers.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | 7 | orderEvent "github.com/NiltonMorais/event-driven-golang/internal/order/domain/event" 8 | userEvent "github.com/NiltonMorais/event-driven-golang/internal/user/domain/event" 9 | ) 10 | 11 | func RegisterConsumers(app *Application) { 12 | var list map[reflect.Type][]func(w http.ResponseWriter, r *http.Request) = map[reflect.Type][]func(w http.ResponseWriter, r *http.Request){ 13 | reflect.TypeOf(userEvent.UserRegisteredEvent{}): { 14 | app.userController.SendWelcomeEmail, 15 | }, 16 | reflect.TypeOf(orderEvent.OrderCreatedEvent{}): { 17 | app.orderController.ProcessOrderPayment, 18 | app.orderController.StockMovement, 19 | app.orderController.SendOrderEmail, 20 | }, 21 | } 22 | 23 | for eventType, handlers := range list { 24 | for _, handler := range handlers { 25 | app.queue.ListenerRegister(eventType, handler) 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /cmd/api/factory/router.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import "net/http" 4 | 5 | func ResgisterRoutes(app *Application) { 6 | http.HandleFunc("/", app.userController.HelloWorld) 7 | http.HandleFunc("POST /create-user", app.userController.CreateUser) 8 | http.HandleFunc("POST /create-order", app.orderController.CreateOrder) 9 | } 10 | -------------------------------------------------------------------------------- /cmd/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "log" 6 | 7 | "github.com/NiltonMorais/event-driven-golang/cmd/api/factory" 8 | ) 9 | 10 | func main() { 11 | app, err := factory.NewApplication() 12 | if err != nil { 13 | log.Fatalf("Error creating application: %s", err) 14 | } 15 | factory.ResgisterRoutes(app) 16 | factory.RegisterConsumers(app) 17 | 18 | ctx := context.Background() 19 | 20 | err = app.StartConsumingQueues(ctx) 21 | if err != nil { 22 | log.Fatalf("Error consumer queues: %s", err) 23 | } 24 | defer app.DisconnectQueue(ctx) 25 | 26 | err = app.RunServer(ctx) 27 | if err != nil { 28 | log.Fatalf("Error running server: %s", err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | rabbitmq: 3 | image: "rabbitmq:3.8-management-alpine" 4 | hostname: rabbitmq 5 | ports: 6 | - "15672:15672" 7 | - "5672:5672" 8 | volumes: 9 | - "rabbitmq_data:/var/lib/rabbitmq/mnesia" 10 | environment: 11 | - RABBITMQ_DEFAULT_USER=guest 12 | - RABBITMQ_DEFAULT_PASS=guest 13 | 14 | volumes: 15 | rabbitmq_data: 16 | -------------------------------------------------------------------------------- /docs/events_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NiltonMorais/event-driven-golang/5c62d1eb5a18234a0dd46c34408f2f3182f02791/docs/events_diagram.png -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/NiltonMorais/event-driven-golang 2 | 3 | go 1.22.1 4 | 5 | require ( 6 | github.com/google/uuid v1.6.0 7 | github.com/rabbitmq/amqp091-go v1.9.0 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 4 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 6 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 7 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/rabbitmq/amqp091-go v1.9.0 h1:qrQtyzB4H8BQgEuJwhmVQqVHB9O4+MNDJCCAcpc3Aoo= 10 | github.com/rabbitmq/amqp091-go v1.9.0/go.mod h1:+jPrT9iY2eLjRaMSRHUhc3z14E/l85kv/f+6luSD3pc= 11 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 12 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 13 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 14 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 15 | go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= 16 | go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4= 17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 18 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 19 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 20 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 21 | -------------------------------------------------------------------------------- /internal/order/application/controller/order_controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/NiltonMorais/event-driven-golang/internal/order/application/dto" 8 | "github.com/NiltonMorais/event-driven-golang/internal/order/application/usecase" 9 | "github.com/NiltonMorais/event-driven-golang/internal/order/domain/event" 10 | ) 11 | 12 | type OrderController struct { 13 | createOrderUserCase *usecase.CreateOrderUseCase 14 | processOrderPaymentUseCase *usecase.ProcessOrderPaymentUseCase 15 | stockMovementUseCase *usecase.StockMovementUseCase 16 | sendOrderEmailUseCase *usecase.SendOrderEmailUseCase 17 | } 18 | 19 | func NewOrderController(createOrderUserCase *usecase.CreateOrderUseCase, 20 | processOrderPaymentUseCase *usecase.ProcessOrderPaymentUseCase, 21 | stockMovementUseCase *usecase.StockMovementUseCase, 22 | sendOrderEmailUseCase *usecase.SendOrderEmailUseCase) *OrderController { 23 | return &OrderController{ 24 | createOrderUserCase: createOrderUserCase, 25 | processOrderPaymentUseCase: processOrderPaymentUseCase, 26 | stockMovementUseCase: stockMovementUseCase, 27 | sendOrderEmailUseCase: sendOrderEmailUseCase, 28 | } 29 | } 30 | 31 | func (u *OrderController) CreateOrder(w http.ResponseWriter, r *http.Request) { 32 | var requestData dto.CreateOrderDTO 33 | json.NewDecoder(r.Body).Decode(&requestData) 34 | err := u.createOrderUserCase.Execute(r.Context(), requestData) 35 | if err != nil { 36 | w.WriteHeader(http.StatusInternalServerError) 37 | w.Write([]byte(err.Error())) 38 | return 39 | } 40 | w.WriteHeader(http.StatusCreated) 41 | } 42 | 43 | func (u *OrderController) ProcessOrderPayment(w http.ResponseWriter, r *http.Request) { 44 | var body event.OrderCreatedEvent 45 | json.NewDecoder(r.Body).Decode(&body) 46 | err := u.processOrderPaymentUseCase.Execute(r.Context(), &body) 47 | if err != nil { 48 | w.WriteHeader(http.StatusInternalServerError) 49 | w.Write([]byte(err.Error())) 50 | return 51 | } 52 | w.WriteHeader(http.StatusCreated) 53 | } 54 | 55 | func (u *OrderController) StockMovement(w http.ResponseWriter, r *http.Request) { 56 | var body event.OrderCreatedEvent 57 | json.NewDecoder(r.Body).Decode(&body) 58 | err := u.stockMovementUseCase.Execute(r.Context(), &body) 59 | if err != nil { 60 | w.WriteHeader(http.StatusInternalServerError) 61 | w.Write([]byte(err.Error())) 62 | return 63 | } 64 | w.WriteHeader(http.StatusCreated) 65 | } 66 | 67 | func (u *OrderController) SendOrderEmail(w http.ResponseWriter, r *http.Request) { 68 | var body event.OrderCreatedEvent 69 | json.NewDecoder(r.Body).Decode(&body) 70 | err := u.sendOrderEmailUseCase.Execute(r.Context(), &body) 71 | if err != nil { 72 | w.WriteHeader(http.StatusInternalServerError) 73 | w.Write([]byte(err.Error())) 74 | return 75 | } 76 | w.WriteHeader(http.StatusCreated) 77 | } 78 | -------------------------------------------------------------------------------- /internal/order/application/dto/create_order_dto.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type CreateOrderDTO struct { 4 | Products []Product `json:"products"` 5 | } 6 | 7 | type Product struct { 8 | Id string `json:"id"` 9 | Qtd string `json:"qtd"` 10 | } 11 | -------------------------------------------------------------------------------- /internal/order/application/usecase/create_order_usercase.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/NiltonMorais/event-driven-golang/internal/order/application/dto" 8 | "github.com/NiltonMorais/event-driven-golang/internal/order/domain/entity" 9 | "github.com/NiltonMorais/event-driven-golang/internal/order/domain/event" 10 | "github.com/NiltonMorais/event-driven-golang/internal/order/domain/queue" 11 | ) 12 | 13 | type CreateOrderUseCase struct { 14 | publisher queue.Publisher 15 | } 16 | 17 | func NewCreateOrderUseCase(publisher queue.Publisher) *CreateOrderUseCase { 18 | return &CreateOrderUseCase{ 19 | publisher: publisher, 20 | } 21 | } 22 | 23 | func (u *CreateOrderUseCase) Execute(ctx context.Context, input dto.CreateOrderDTO) error { 24 | fmt.Println("--- CreateOrderUseCase ---") 25 | order, err := entity.NewOrderEntity() 26 | 27 | product1, _ := entity.NewProductEntity("Product A", 10.50) 28 | item1 := entity.NewOrderItemEntity(product1, 1) 29 | 30 | product2, _ := entity.NewProductEntity("Product B", 43.19) 31 | item2 := entity.NewOrderItemEntity(product2, 2) 32 | 33 | order.AddItem(item1) 34 | order.AddItem(item2) 35 | 36 | if err != nil { 37 | return err 38 | } 39 | 40 | err = u.publisher.Publish(ctx, event.NewOrderCreatedEvent(order)) 41 | if err != nil { 42 | return err 43 | } 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /internal/order/application/usecase/process_order_payment_usecase.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "time" 7 | 8 | "github.com/NiltonMorais/event-driven-golang/internal/order/domain/entity" 9 | "github.com/NiltonMorais/event-driven-golang/internal/order/domain/event" 10 | "github.com/NiltonMorais/event-driven-golang/internal/order/domain/queue" 11 | ) 12 | 13 | type ProcessOrderPaymentUseCase struct { 14 | publisher queue.Publisher 15 | } 16 | 17 | func NewProcessOrderPaymentUseCase(publisher queue.Publisher) *ProcessOrderPaymentUseCase { 18 | return &ProcessOrderPaymentUseCase{ 19 | publisher: publisher, 20 | } 21 | } 22 | 23 | func (h *ProcessOrderPaymentUseCase) Execute(ctx context.Context, payload *event.OrderCreatedEvent) error { 24 | fmt.Println("--- ProcessOrderPaymentUseCase ---") 25 | order, err := entity.RestoreOrderEntity(payload.Id, payload.Status) 26 | if err != nil { 27 | return err 28 | } 29 | for _, item := range payload.Items { 30 | product, _ := entity.NewProductEntity(item.ProductName, item.TotalPrice/float64(item.Quantity)) 31 | order.AddItem(entity.NewOrderItemEntity(product, item.Quantity)) 32 | } 33 | paymentValue := payload.TotalPrice 34 | err = order.Pay(paymentValue) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | fmt.Printf("Processado o pagamento de R$ %f \n", payload.TotalPrice) 40 | err = h.publisher.Publish(ctx, event.OrderPaidEvent{OrderId: payload.Id, PaidValue: paymentValue, PaymentDate: time.Now()}) 41 | if err != nil { 42 | return err 43 | } 44 | return nil 45 | } 46 | -------------------------------------------------------------------------------- /internal/order/application/usecase/send_orderemail_usecase.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/NiltonMorais/event-driven-golang/internal/order/domain/event" 8 | ) 9 | 10 | type SendOrderEmailUseCase struct { 11 | } 12 | 13 | func NewSendOrderEmailUseCase() *SendOrderEmailUseCase { 14 | return &SendOrderEmailUseCase{} 15 | } 16 | 17 | func (h *SendOrderEmailUseCase) Execute(ctx context.Context, payload *event.OrderCreatedEvent) error { 18 | fmt.Println("--- SendOrderEmailHandler ---") 19 | fmt.Printf("--- MAIL Order Created: R$ %f \n", payload.TotalPrice) 20 | return nil 21 | } 22 | -------------------------------------------------------------------------------- /internal/order/application/usecase/stock_movement_usecase.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/NiltonMorais/event-driven-golang/internal/order/domain/event" 8 | ) 9 | 10 | type StockMovementUseCase struct { 11 | } 12 | 13 | func NewStockMovementUseCase() *StockMovementUseCase { 14 | return &StockMovementUseCase{} 15 | } 16 | 17 | func (h *StockMovementUseCase) Execute(ctx context.Context, payload *event.OrderCreatedEvent) error { 18 | fmt.Println("--- StockMovimentHandler ---") 19 | for _, item := range payload.Items { 20 | fmt.Printf("Retirando do stock %d itens do produto: %s\n", item.Quantity, item.ProductName) 21 | } 22 | return nil 23 | } 24 | -------------------------------------------------------------------------------- /internal/order/domain/entity/order_entity.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/google/uuid" 7 | ) 8 | 9 | const ( 10 | OrderStatusPending = "pending" 11 | OrderStatusPaid = "paid" 12 | ) 13 | 14 | type OrderEntity struct { 15 | id string 16 | status string 17 | items []*OrderItemEntity 18 | totalPrice float64 19 | paidValue float64 20 | } 21 | 22 | func NewOrderEntity() (*OrderEntity, error) { 23 | return &OrderEntity{ 24 | id: uuid.New().String(), 25 | status: OrderStatusPending, 26 | }, nil 27 | } 28 | 29 | func RestoreOrderEntity(id, status string) (*OrderEntity, error) { 30 | return &OrderEntity{ 31 | id: id, 32 | status: status, 33 | }, nil 34 | } 35 | 36 | func (o *OrderEntity) AddItem(item *OrderItemEntity) { 37 | o.items = append(o.items, item) 38 | o.totalPrice += item.GetTotalPrice() 39 | } 40 | 41 | func (o *OrderEntity) Pay(value float64) error { 42 | if value < o.totalPrice { 43 | return errors.New("value is less than the total price") 44 | } 45 | o.paidValue = value 46 | o.status = OrderStatusPaid 47 | return nil 48 | } 49 | 50 | func (o *OrderEntity) GetItems() []*OrderItemEntity { 51 | return o.items 52 | } 53 | 54 | func (o *OrderEntity) GetTotalPrice() float64 { 55 | return o.totalPrice 56 | } 57 | 58 | func (o *OrderEntity) GetID() string { 59 | return o.id 60 | } 61 | 62 | func (o *OrderEntity) GetStatus() string { 63 | return o.status 64 | } 65 | -------------------------------------------------------------------------------- /internal/order/domain/entity/order_item_entity.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | type OrderItemEntity struct { 4 | product *ProductEntity 5 | quantity int 6 | totalPrice float64 7 | } 8 | 9 | func NewOrderItemEntity(product *ProductEntity, quantity int) *OrderItemEntity { 10 | return &OrderItemEntity{ 11 | product: product, 12 | quantity: quantity, 13 | } 14 | } 15 | 16 | func (o *OrderItemEntity) GetProduct() *ProductEntity { 17 | return o.product 18 | } 19 | 20 | func (o *OrderItemEntity) GetQuantity() int { 21 | return o.quantity 22 | } 23 | 24 | func (o *OrderItemEntity) GetTotalPrice() float64 { 25 | return o.product.GetPrice() * float64(o.quantity) 26 | } 27 | -------------------------------------------------------------------------------- /internal/order/domain/entity/product_entity.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import "github.com/google/uuid" 4 | 5 | type ProductEntity struct { 6 | id string 7 | name string 8 | price float64 9 | } 10 | 11 | func NewProductEntity(name string, price float64) (*ProductEntity, error) { 12 | return &ProductEntity{ 13 | id: uuid.New().String(), 14 | name: name, 15 | price: price, 16 | }, nil 17 | } 18 | 19 | func (p *ProductEntity) GetID() string { 20 | return p.id 21 | } 22 | 23 | func (p *ProductEntity) GetName() string { 24 | return p.name 25 | } 26 | 27 | func (p *ProductEntity) GetPrice() float64 { 28 | return p.price 29 | } 30 | -------------------------------------------------------------------------------- /internal/order/domain/event/order_created_event.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import "github.com/NiltonMorais/event-driven-golang/internal/order/domain/entity" 4 | 5 | type OrderCreatedEvent struct { 6 | Id string 7 | Items []OrderItem 8 | TotalPrice float64 9 | Status string 10 | } 11 | 12 | type OrderItem struct { 13 | ProductName string 14 | Quantity int 15 | TotalPrice float64 16 | } 17 | 18 | func NewOrderCreatedEvent(order *entity.OrderEntity) OrderCreatedEvent { 19 | var items []OrderItem 20 | for _, item := range order.GetItems() { 21 | items = append(items, OrderItem{ 22 | ProductName: item.GetProduct().GetName(), 23 | Quantity: item.GetQuantity(), 24 | TotalPrice: item.GetTotalPrice(), 25 | }) 26 | } 27 | return OrderCreatedEvent{ 28 | Id: order.GetID(), 29 | TotalPrice: order.GetTotalPrice(), 30 | Status: order.GetStatus(), 31 | Items: items, 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/order/domain/event/order_paid_event.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type OrderPaidEvent struct { 8 | OrderId string 9 | PaidValue float64 10 | PaymentDate time.Time 11 | } 12 | -------------------------------------------------------------------------------- /internal/order/domain/queue/publisher.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import "context" 4 | 5 | type Publisher interface { 6 | Publish(ctx context.Context, body interface{}) error 7 | } 8 | -------------------------------------------------------------------------------- /internal/user/application/controller/user_controller.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | 7 | "github.com/NiltonMorais/event-driven-golang/internal/user/application/dto" 8 | "github.com/NiltonMorais/event-driven-golang/internal/user/application/usecase" 9 | "github.com/NiltonMorais/event-driven-golang/internal/user/domain/event" 10 | ) 11 | 12 | type UserController struct { 13 | createUserUseCase *usecase.CreateUserUseCase 14 | sendWelcomeEmailUseCase *usecase.SendWelcomeEmailUseCase 15 | } 16 | 17 | func NewUserController(createUserUseCase *usecase.CreateUserUseCase, sendWelcomeEmailUseCase *usecase.SendWelcomeEmailUseCase) *UserController { 18 | return &UserController{ 19 | createUserUseCase: createUserUseCase, 20 | sendWelcomeEmailUseCase: sendWelcomeEmailUseCase, 21 | } 22 | } 23 | 24 | func (u *UserController) HelloWorld(w http.ResponseWriter, r *http.Request) { 25 | w.Write([]byte("Hello, World!")) 26 | } 27 | 28 | func (u *UserController) CreateUser(w http.ResponseWriter, r *http.Request) { 29 | var requestData dto.CreateUserDTO 30 | json.NewDecoder(r.Body).Decode(&requestData) 31 | err := u.createUserUseCase.Execute(r.Context(), requestData.Name, requestData.Email) 32 | if err != nil { 33 | w.WriteHeader(http.StatusInternalServerError) 34 | w.Write([]byte(err.Error())) 35 | return 36 | } 37 | w.WriteHeader(http.StatusCreated) 38 | } 39 | 40 | func (u *UserController) SendWelcomeEmail(w http.ResponseWriter, r *http.Request) { 41 | var body event.UserRegisteredEvent 42 | json.NewDecoder(r.Body).Decode(&body) 43 | err := u.sendWelcomeEmailUseCase.Execute(r.Context(), &body) 44 | if err != nil { 45 | w.WriteHeader(http.StatusInternalServerError) 46 | w.Write([]byte(err.Error())) 47 | return 48 | } 49 | w.WriteHeader(http.StatusCreated) 50 | } 51 | -------------------------------------------------------------------------------- /internal/user/application/dto/create_user_dto.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type CreateUserDTO struct { 4 | Name string `json:"name"` 5 | Email string `json:"email"` 6 | } 7 | -------------------------------------------------------------------------------- /internal/user/application/usecase/create_user_usecase.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/NiltonMorais/event-driven-golang/internal/user/domain/entity" 8 | "github.com/NiltonMorais/event-driven-golang/internal/user/domain/event" 9 | "github.com/NiltonMorais/event-driven-golang/internal/user/domain/queue" 10 | ) 11 | 12 | type CreateUserUseCase struct { 13 | publisher queue.Publisher 14 | } 15 | 16 | func NewCreateUserUseCase(publisher queue.Publisher) *CreateUserUseCase { 17 | return &CreateUserUseCase{ 18 | publisher: publisher, 19 | } 20 | } 21 | 22 | func (u *CreateUserUseCase) Execute(ctx context.Context, name, email string) error { 23 | fmt.Println("--- CreateUserUseCase ---") 24 | user, err := entity.NewUserEntity(name, email) 25 | if err != nil { 26 | return err 27 | } 28 | event := event.UserRegisteredEvent{ 29 | ID: user.GetID(), 30 | Name: user.GetName(), 31 | Email: user.GetEmail(), 32 | } 33 | err = u.publisher.Publish(ctx, event) 34 | if err != nil { 35 | return err 36 | } 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /internal/user/application/usecase/send_welcomeemail_usecase.go: -------------------------------------------------------------------------------- 1 | package usecase 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/NiltonMorais/event-driven-golang/internal/user/domain/event" 8 | "github.com/NiltonMorais/event-driven-golang/internal/user/domain/queue" 9 | ) 10 | 11 | type SendWelcomeEmailUseCase struct { 12 | publisher queue.Publisher 13 | } 14 | 15 | func NewSendWelcomeEmailUseCase(publisher queue.Publisher) *SendWelcomeEmailUseCase { 16 | return &SendWelcomeEmailUseCase{ 17 | publisher: publisher, 18 | } 19 | } 20 | 21 | func (h *SendWelcomeEmailUseCase) Execute(ctx context.Context, input *event.UserRegisteredEvent) error { 22 | fmt.Println("--- SendWelcomeEmailUseCase ---") 23 | fmt.Printf("--- MAIL to %s: Welcome %s --- \n", input.Email, input.Name) 24 | h.publisher.Publish(ctx, event.WelcomeEmailSentEvent{Email: input.Email}) 25 | return nil 26 | } 27 | -------------------------------------------------------------------------------- /internal/user/domain/entity/user_entity.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import "github.com/google/uuid" 4 | 5 | type UserEntity struct { 6 | id string 7 | name string 8 | email string 9 | } 10 | 11 | func NewUserEntity(name, email string) (*UserEntity, error) { 12 | return &UserEntity{ 13 | id: uuid.New().String(), 14 | name: name, 15 | email: email, 16 | }, nil 17 | } 18 | 19 | func (u *UserEntity) GetID() string { 20 | return u.id 21 | } 22 | 23 | func (u *UserEntity) GetName() string { 24 | return u.name 25 | } 26 | 27 | func (u *UserEntity) GetEmail() string { 28 | return u.email 29 | } 30 | -------------------------------------------------------------------------------- /internal/user/domain/event/user_registered_event.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | type UserRegisteredEvent struct { 4 | ID string 5 | Name string 6 | Email string 7 | } 8 | -------------------------------------------------------------------------------- /internal/user/domain/event/welcome_email_sent_event.go: -------------------------------------------------------------------------------- 1 | package event 2 | 3 | type WelcomeEmailSentEvent struct { 4 | Email string 5 | } 6 | -------------------------------------------------------------------------------- /internal/user/domain/queue/publisher.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import "context" 4 | 5 | type Publisher interface { 6 | Publish(ctx context.Context, body interface{}) error 7 | } 8 | -------------------------------------------------------------------------------- /pkg/queue/listener.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "net/http" 5 | "reflect" 6 | ) 7 | 8 | type Listener struct { 9 | eventType reflect.Type 10 | callback func(w http.ResponseWriter, r *http.Request) 11 | } 12 | -------------------------------------------------------------------------------- /pkg/queue/memory_queue_adapter.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "log" 8 | "net/http" 9 | "reflect" 10 | ) 11 | 12 | type MemoryQueueAdapter struct { 13 | listeners map[string][]Listener 14 | } 15 | 16 | func NewMemoryQueueAdapter() *MemoryQueueAdapter { 17 | return &MemoryQueueAdapter{ 18 | listeners: make(map[string][]Listener), 19 | } 20 | } 21 | 22 | func (eb *MemoryQueueAdapter) ListenerRegister(eventType reflect.Type, handler func(w http.ResponseWriter, r *http.Request)) { 23 | eb.listeners[eventType.Name()] = append(eb.listeners[eventType.Name()], Listener{eventType, handler}) 24 | } 25 | 26 | func (eb *MemoryQueueAdapter) Publish(ctx context.Context, eventPayload interface{}) error { 27 | eventType := reflect.TypeOf(eventPayload) 28 | payloadJson, _ := json.Marshal(eventPayload) 29 | 30 | log.Printf("--- Publish %s ---", eventType) 31 | 32 | for _, listener := range eb.listeners[eventType.Name()] { 33 | w := NewQueueResponseWriter() 34 | body := bytes.NewBuffer(payloadJson) 35 | r, err := http.NewRequestWithContext(ctx, http.MethodPost, eventType.Name(), body) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | listener.callback(w, r) 41 | if err != nil { 42 | return err 43 | } 44 | } 45 | 46 | return nil 47 | } 48 | 49 | func (eb *MemoryQueueAdapter) Connect(ctx context.Context) error { 50 | log.Println("--- MemoryQueueAdapter connected ---") 51 | return nil 52 | } 53 | 54 | func (eb *MemoryQueueAdapter) Disconnect(ctx context.Context) error { 55 | log.Println("--- MemoryQueueAdapter disconnected ---") 56 | return nil 57 | } 58 | 59 | func (eb *MemoryQueueAdapter) StartConsuming(ctx context.Context, queueName string) error { 60 | log.Printf("--- MemoryQueueAdapter StartConsuming queue %s ---", queueName) 61 | return nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/queue/queue.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "reflect" 7 | ) 8 | 9 | type Queue interface { 10 | ListenerRegister(eventType reflect.Type, handler func(w http.ResponseWriter, r *http.Request)) 11 | Connect(ctx context.Context) error 12 | Disconnect(ctx context.Context) error 13 | Publish(ctx context.Context, body interface{}) error 14 | StartConsuming(ctx context.Context, queueName string) error 15 | } 16 | -------------------------------------------------------------------------------- /pkg/queue/rabbitmq_adapter.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "errors" 8 | "log" 9 | "net/http" 10 | "reflect" 11 | "time" 12 | 13 | amqp "github.com/rabbitmq/amqp091-go" 14 | ) 15 | 16 | type RabbitMQAdapter struct { 17 | uri string 18 | conn *amqp.Connection 19 | listeners map[string][]Listener 20 | } 21 | 22 | type QueueMessage struct { 23 | Body []byte 24 | } 25 | 26 | func NewRabbitMQAdapter(uri string) *RabbitMQAdapter { 27 | return &RabbitMQAdapter{ 28 | uri: uri, 29 | listeners: make(map[string][]Listener), 30 | } 31 | } 32 | 33 | func (r *RabbitMQAdapter) Connect(ctx context.Context) error { 34 | conn, err := amqp.Dial(r.uri) 35 | if err != nil { 36 | return err 37 | } 38 | r.conn = conn 39 | return nil 40 | } 41 | 42 | func (r *RabbitMQAdapter) Disconnect(ctx context.Context) error { 43 | return r.conn.Close() 44 | } 45 | 46 | func (r *RabbitMQAdapter) Publish(ctx context.Context, eventPayload interface{}) error { 47 | eventName := reflect.TypeOf(eventPayload).Name() 48 | 49 | ch, err := r.conn.Channel() 50 | if err != nil { 51 | return err 52 | } 53 | defer ch.Close() 54 | 55 | q, err := ch.QueueDeclare( 56 | eventName, // queue name 57 | true, // durable 58 | false, // delete when unused 59 | false, // exclusive 60 | false, // no-wait 61 | nil, // arguments 62 | ) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | ctx, cancel := context.WithTimeout(ctx, 5*time.Second) 68 | defer cancel() 69 | 70 | eventJson, err := json.Marshal(eventPayload) 71 | if err != nil { 72 | return errors.New("error converting struct to json") 73 | } 74 | 75 | err = ch.PublishWithContext(ctx, 76 | "", // exchange 77 | q.Name, // routing key 78 | false, // mandatory 79 | false, // immediate 80 | amqp.Publishing{ 81 | ContentType: "text/plain", 82 | Body: []byte(eventJson), 83 | }) 84 | if err != nil { 85 | return err 86 | } 87 | log.Printf(" [x] Sent to queue %s: %s\n", eventName, eventJson) 88 | return nil 89 | } 90 | 91 | func (r *RabbitMQAdapter) StartConsuming(ctx context.Context, queueName string) error { 92 | ch, err := r.conn.Channel() 93 | if err != nil { 94 | return err 95 | } 96 | defer ch.Close() 97 | 98 | q, err := ch.QueueDeclare( 99 | queueName, // name 100 | true, // durable 101 | false, // delete when unused 102 | false, // exclusive 103 | false, // no-wait 104 | nil, // arguments 105 | ) 106 | if err != nil { 107 | return err 108 | } 109 | 110 | msgs, err := ch.ConsumeWithContext( 111 | ctx, 112 | q.Name, // queue 113 | "", // consumer 114 | false, // auto-ack 115 | false, // exclusive 116 | false, // no-local 117 | false, // no-wait 118 | nil, // args 119 | ) 120 | if err != nil { 121 | return err 122 | } 123 | 124 | go func() { 125 | for d := range msgs { 126 | log.Printf("Received a message on queue %s: %s", queueName, d.Body) 127 | hasError := false 128 | for _, listener := range r.listeners[queueName] { 129 | w := NewQueueResponseWriter() 130 | body := bytes.NewBuffer(d.Body) 131 | r, err := http.NewRequestWithContext(ctx, http.MethodPost, queueName, body) 132 | if err != nil { 133 | log.Printf("Error processing message: %s", err) 134 | hasError = true 135 | break 136 | } 137 | 138 | listener.callback(w, r) 139 | if w.statusCode >= 400 { 140 | log.Printf("Error processing message: %s", string(w.body)) 141 | hasError = true 142 | break 143 | } 144 | } 145 | 146 | if !hasError { 147 | d.Ack(false) 148 | } 149 | } 150 | }() 151 | 152 | var forever chan struct{} 153 | log.Printf(" [*] Waiting for messages on queue %s. To exit press CTRL+C", queueName) 154 | <-forever 155 | return nil 156 | } 157 | 158 | func (r *RabbitMQAdapter) ListenerRegister(eventType reflect.Type, handler func(w http.ResponseWriter, r *http.Request)) { 159 | r.listeners[eventType.Name()] = append(r.listeners[eventType.Name()], Listener{eventType, handler}) 160 | } 161 | -------------------------------------------------------------------------------- /pkg/queue/response_writer.go: -------------------------------------------------------------------------------- 1 | package queue 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | type QueueResponseWriter struct { 9 | body []byte 10 | statusCode int 11 | header http.Header 12 | } 13 | 14 | func NewQueueResponseWriter() *QueueResponseWriter { 15 | return &QueueResponseWriter{ 16 | header: http.Header{}, 17 | } 18 | } 19 | 20 | func (w *QueueResponseWriter) Header() http.Header { 21 | return w.header 22 | } 23 | 24 | func (w *QueueResponseWriter) Write(b []byte) (int, error) { 25 | w.body = b 26 | // implement it as per your requirement 27 | return 0, nil 28 | } 29 | 30 | func (w *QueueResponseWriter) WriteHeader(statusCode int) { 31 | w.statusCode = statusCode 32 | } 33 | 34 | var okFn = func(w http.ResponseWriter, r *http.Request) { 35 | w.WriteHeader(http.StatusOK) 36 | } 37 | 38 | func main() { 39 | r := &http.Request{ 40 | Method: http.MethodPost, 41 | } 42 | w := NewQueueResponseWriter() 43 | okFn(w, r) 44 | fmt.Println(w.statusCode) 45 | } 46 | -------------------------------------------------------------------------------- /request.http: -------------------------------------------------------------------------------- 1 | ### CREATE USER 2 | POST http://localhost:8080/create-user HTTP/1.1 3 | content-type: application/json 4 | 5 | { 6 | "name": "Nilton Morais", 7 | "email": "nilton@gmail.com" 8 | } 9 | 10 | ### CREATE ORDER 11 | POST http://localhost:8080/create-order HTTP/1.1 12 | content-type: application/json 13 | 14 | { 15 | "products": [ 16 | { 17 | "id": 1, 18 | "qtd": 2 19 | }, 20 | { 21 | "id": 2, 22 | "qtd": 1 23 | } 24 | ], 25 | } --------------------------------------------------------------------------------