Skip to content

oshturhq/medix

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

3 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

medix

Go implementation of the Mediator pattern with support for request/response, notifications (pub/sub), and pipeline behaviors.

Installation

go get github.com/oshturhq/medix

Requires Go 1.21+ (generics support).

Quick Start

package main

import (
    "context"
    "log/slog"
    "github.com/oshturhq/medix"
)

// Define request and response
type GetUserRequest struct {
    ID int
}

type GetUserResponse struct {
    Name string
}

// Implement handler
type GetUserHandler struct{}

func (h *GetUserHandler) Handle(ctx context.Context, req GetUserRequest) (GetUserResponse, error) {
    return GetUserResponse{Name: "John"}, nil
}

func main() {
    // Register handler
    medix.RegisterRequestHandler[GetUserRequest, GetUserResponse](&GetUserHandler{})

    // Send request
    resp, err := medix.Send[GetUserRequest, GetUserResponse](context.Background(), GetUserRequest{ID: 1})
    if err != nil {
        panic(err)
    }
    slog.Info("user fetched", "name", resp.Name)
}

Features

Request/Response

One request, one handler. Use Send to dispatch.

// Register
medix.RegisterRequestHandler[MyRequest, MyResponse](&MyHandler{})

// Or with factory (new instance per request)
medix.RegisterRequestHandlerFactory[MyRequest, MyResponse](func() medix.RequestHandler[MyRequest, MyResponse] {
    return &MyHandler{db: connectDB()}
})

// Send
response, err := medix.Send[MyRequest, MyResponse](ctx, MyRequest{...})

Notifications (Pub/Sub)

One notification, multiple handlers. Use Publish to broadcast.

type UserCreated struct {
    UserID int
    Email  string
}

type EmailHandler struct{}
func (h *EmailHandler) Handle(ctx context.Context, e UserCreated) error {
    // Send welcome email
    return nil
}

type AuditHandler struct{}
func (h *AuditHandler) Handle(ctx context.Context, e UserCreated) error {
    // Log to audit trail
    return nil
}

// Register multiple handlers
medix.RegisterNotificationHandlers[UserCreated](
    &EmailHandler{},
    &AuditHandler{},
)

// Publish (all handlers are called sequentially)
err := medix.Publish(ctx, UserCreated{UserID: 1, Email: "user@example.com"})

Pipeline Behaviors

Cross-cutting concerns that wrap all request handlers.

type LoggingBehavior struct {
    logger *slog.Logger
}

func (b *LoggingBehavior) Handle(ctx context.Context, request any, next medix.RequestHandlerFunc) (any, error) {
    b.logger.Info("before handler")
    response, err := next(ctx)
    b.logger.Info("after handler")
    return response, err
}

type TimingBehavior struct {
    logger *slog.Logger
}

func (b *TimingBehavior) Handle(ctx context.Context, request any, next medix.RequestHandlerFunc) (any, error) {
    start := time.Now()
    response, err := next(ctx)
    b.logger.Info("request completed", "duration", time.Since(start))
    return response, err
}

// Register behaviors (order matters: first registered = outermost)
medix.RegisterRequestPipelineBehaviors(
    &LoggingBehavior{logger: slog.Default()},
    &TimingBehavior{logger: slog.Default()},
)

Execution order:

LoggingBehavior.Before -> TimingBehavior.Before -> Handler -> TimingBehavior.After -> LoggingBehavior.After

Validation Pattern

type Validator interface {
    Validate() error
}

type ValidationBehavior struct{}

func (b *ValidationBehavior) Handle(ctx context.Context, request any, next medix.RequestHandlerFunc) (any, error) {
    if v, ok := request.(Validator); ok {
        if err := v.Validate(); err != nil {
            return nil, err
        }
    }
    return next(ctx)
}

// Request with validation
type CreateUserRequest struct {
    Name  string
    Email string
}

func (r CreateUserRequest) Validate() error {
    if r.Name == "" {
        return errors.New("name is required")
    }
    return nil
}

API Reference

Request Handlers

Function Description
RegisterRequestHandler[TReq, TRes](handler) Register a singleton handler
RegisterRequestHandlerFactory[TReq, TRes](factory) Register a factory (new instance per request)
Send[TReq, TRes](ctx, request) Dispatch request to handler

Notification Handlers

Function Description
RegisterNotificationHandler[T](handler) Register a single notification handler
RegisterNotificationHandlers[T](handlers...) Register multiple handlers at once
RegisterNotificationHandlerFactory[T](factory) Register with factory
Publish[T](ctx, notification) Publish to all registered handlers

Pipeline Behaviors

Function Description
RegisterRequestPipelineBehaviors(behaviors...) Register pipeline behaviors

Cleanup (for testing)

Function Description
ClearRequestRegistrations() Clear all request handlers
ClearNotificationRegistrations() Clear all notification handlers
ClearPipelineBehaviors() Clear all pipeline behaviors

Example: Hexagonal Architecture

The library works well with hexagonal (ports & adapters) architecture:

internal/
├── domain/                 # Core business logic
│   └── user/
│       ├── entity.go       # User entity
│       └── repository.go   # Repository interface (port)
│
├── application/            # Use cases
│   ├── command/            # Write operations (CreateUser, DeleteUser)
│   ├── query/              # Read operations (GetUser, ListUsers)
│   ├── event/              # Domain events (UserCreated, UserDeleted)
│   └── behavior/           # Pipeline behaviors (logging, validation)
│
├── infrastructure/         # Adapters
│   ├── persistence/        # Database implementations
│   └── notification/       # Event handlers (email, audit)
│
└── main.go                 # Composition root

See internal/example for a complete working example.

Thread Safety

  • All registration functions are thread-safe
  • Send and Publish are safe for concurrent use
  • Context cancellation is respected

About

A lightweight Mediator pattern implementation for Go with pipeline behaviors.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages