Skip to main content

What Is an Invoker?

Invokers are the engine that makes Contract fast. They’re pre-compiled method callers that let Contract call your service methods without the overhead of reflection on every request. Think of invokers like pre-addressed envelopes: instead of looking up the address every time you send a letter, you prepare the envelope once and just drop letters in it. When you register a service, Contract analyzes your methods using Go’s reflection system. This analysis happens once at startup, and the results are stored as “invokers” - optimized callers that know exactly how to call each method.
Registration time (once, at startup):
Service → Reflection Analysis → Invoker Created and Cached

Runtime (every request):
Request → Invoker.Call() → Your Method

    No reflection overhead!

Why Invokers Matter

Without invokers, calling a method by name would require reflection on every single request:
// Slow way: Reflection on every call
// This is expensive because Go has to look up the method, check types, etc.
method := reflect.ValueOf(service).MethodByName("Create")
args := []reflect.Value{reflect.ValueOf(ctx), reflect.ValueOf(input)}
result := method.Call(args)  // Expensive reflection call!
With invokers, the reflection cost is paid once at startup:
// Fast way: Pre-compiled invoker
// The invoker already knows how to call the method directly
result, err := method.Invoker.Call(ctx, input)  // Just a regular function call

Basic Usage

Most of the time, you don’t interact with invokers directly - transports use them automatically behind the scenes. But understanding how they work helps you build custom transports or debugging tools.

Getting a Method and Its Invoker

package main

import (
    "context"

    contract "github.com/go-mizu/mizu/contract/v2"
    "yourapp/todo"
)

func main() {
    impl := todo.NewService()
    svc := contract.Register[todo.API](impl,
        contract.WithDefaultResource("todos"),
    )

    // Get a specific method by name
    method := svc.Resolve("create")  // lowercase method name

    // method.Invoker is ready to use
    // method.Input tells you the input type
    // method.Output tells you the output type
}

Calling a Method via Its Invoker

// Create input
input := &todo.CreateInput{Title: "Buy milk"}

// Call the method using its invoker
ctx := context.Background()
result, err := method.Invoker.Call(ctx, input)
if err != nil {
    log.Fatal(err)
}

// Type assert the result to get the actual type
createdTodo := result.(*todo.Todo)
fmt.Println(createdTodo.ID, createdTodo.Title)

The Invoker Interface

Invokers implement a simple interface that matches Contract’s method patterns:
type Invoker interface {
    Call(ctx context.Context, in any) (any, error)
}
Parameters:
ParameterTypeDescription
ctxcontext.ContextRequest context (carries timeouts, cancellation signals, values)
inanyInput value (or nil for methods without input)
Returns:
ReturnTypeDescription
resultanyOutput value (or nil for methods without output)
errerrorAny error returned by your method

Calling Different Method Types

Contract supports four method patterns. Here’s how to call each with invokers:

Method With Input and Output

This is the most common pattern:
package todo

// Your service method:
func (s *Service) Create(ctx context.Context, in *CreateInput) (*Todo, error) {
    return &Todo{ID: "1", Title: in.Title}, nil
}
// Calling with invoker:
method := svc.Resolve("create")
input := &todo.CreateInput{Title: "Test todo"}
result, err := method.Invoker.Call(ctx, input)
if err != nil {
    log.Fatal(err)
}
createdTodo := result.(*todo.Todo)  // Type assert the result

Method With Output Only (No Input)

package todo

// Your service method:
func (s *Service) List(ctx context.Context) (*ListOutput, error) {
    return &ListOutput{Items: s.todos, Count: len(s.todos)}, nil
}
// Calling with invoker:
method := svc.Resolve("list")
result, err := method.Invoker.Call(ctx, nil)  // Pass nil for input
if err != nil {
    log.Fatal(err)
}
list := result.(*todo.ListOutput)

Method With Input Only (No Output)

package todo

// Your service method:
func (s *Service) Delete(ctx context.Context, in *DeleteInput) error {
    delete(s.todos, in.ID)
    return nil
}
// Calling with invoker:
method := svc.Resolve("delete")
input := &todo.DeleteInput{ID: "123"}
_, err := method.Invoker.Call(ctx, input)  // Result is nil, just check error

Method With Neither Input Nor Output

package health

// Your service method:
func (s *Service) Ping(ctx context.Context) error {
    return nil  // Just returns success/failure
}
// Calling with invoker:
method := svc.Resolve("ping")
_, err := method.Invoker.Call(ctx, nil)  // Both input and result are nil
if err != nil {
    // Service is unhealthy
}

Creating Input Instances Dynamically

Use NewInput() to create a new input instance for a method. This is essential when you don’t know the input type at compile time (like when building a transport):
method := svc.Resolve("create")

// Check if the method expects input
if method.Input != nil {
    // Create a new input instance (returns any)
    input := method.NewInput()

    // Type assert and populate
    createInput := input.(*todo.CreateInput)
    createInput.Title = "Dynamically created todo"

    // Call the method
    result, err := method.Invoker.Call(ctx, createInput)
    if err != nil {
        log.Fatal(err)
    }
}
This pattern is exactly what transports use: they create an input instance, unmarshal JSON into it, then call the invoker.

How Transports Use Invokers

Here’s simplified code showing how a transport uses invokers internally. This helps you understand what happens when a request comes in:
// Simplified transport handler
func handleRequest(svc *contract.RegisteredService, methodName string, body []byte) (any, error) {
    // 1. Find the method by name
    method := svc.Resolve(methodName)
    if method == nil {
        return nil, errors.New("method not found")
    }

    // 2. Parse input if the method expects one
    var input any
    if method.Input != nil {
        input = method.NewInput()                    // Create empty input instance
        if err := json.Unmarshal(body, input); err != nil {
            return nil, errors.New("invalid input")
        }
    }

    // 3. Call the method through its invoker
    result, err := method.Invoker.Call(context.Background(), input)
    if err != nil {
        return nil, err
    }

    // 4. Return result (transport will serialize it)
    return result, nil
}

Transport Invoker

For building transports, there’s a higher-level interface called TransportInvoker that handles JSON unmarshaling:
type TransportInvoker interface {
    Invoke(ctx context.Context, method *Method, input []byte) (any, error)
}
Key difference from Invoker:
  • Invoker.Call() takes a parsed Go value as input
  • TransportInvoker.Invoke() takes raw JSON bytes and handles unmarshaling

Using the Default Transport Invoker

// Get the default transport invoker for a service
invoker := contract.DefaultInvoker(svc)

// Call methods with raw JSON
method := svc.Resolve("create")
result, err := invoker.Invoke(ctx, method, []byte(`{"title":"Test"}`))
if err != nil {
    log.Fatal(err)
}

Creating a Custom Transport Invoker

You can wrap the default invoker to add logging, metrics, or other cross-cutting concerns:
// LoggingInvoker wraps a TransportInvoker with logging
type LoggingInvoker struct {
    inner contract.TransportInvoker
}

func (l *LoggingInvoker) Invoke(ctx context.Context, method *contract.Method, input []byte) (any, error) {
    // Log before
    log.Printf("Calling: %s", method.Name)
    start := time.Now()

    // Call the actual method
    result, err := l.inner.Invoke(ctx, method, input)

    // Log after
    duration := time.Since(start)
    if err != nil {
        log.Printf("Failed: %s (took %v): %v", method.Name, duration, err)
    } else {
        log.Printf("Success: %s (took %v)", method.Name, duration)
    }

    return result, err
}

// Use it when mounting a transport
loggingInvoker := &LoggingInvoker{inner: contract.DefaultInvoker(svc)}
mcp.Mount(mux, "/mcp", svc, mcp.WithInvoker(loggingInvoker))

Error Handling

Errors from your service method are returned directly through the invoker:
package todo

func (s *Service) Get(ctx context.Context, in *GetInput) (*Todo, error) {
    if in.ID == "" {
        return nil, contract.ErrInvalidArgument("id is required")
    }
    todo, ok := s.todos[in.ID]
    if !ok {
        return nil, contract.ErrNotFound("todo not found")
    }
    return todo, nil
}
// Caller receives the error from your method
result, err := method.Invoker.Call(ctx, &todo.GetInput{ID: ""})
if err != nil {
    // err is the ErrInvalidArgument error from your method
    fmt.Println(err.Error())  // "id is required"
}

Context Handling

The context is passed directly to your method, preserving timeouts, cancellation, and values:
// With timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

result, err := method.Invoker.Call(ctx, input)
if errors.Is(err, context.DeadlineExceeded) {
    // Handle timeout - your method took too long
}

// With values (e.g., from authentication middleware)
ctx = context.WithValue(ctx, "userID", currentUser.ID)
result, err := method.Invoker.Call(ctx, input)
// Your method can access ctx.Value("userID")

Type Safety

Always use type assertions when working with invoker results:
result, err := method.Invoker.Call(ctx, input)
if err != nil {
    return err
}

// Safe type assertion with check (recommended)
todo, ok := result.(*todo.Todo)
if !ok {
    return fmt.Errorf("unexpected type: %T", result)
}

// Or direct assertion when you're certain of the type
// (panics if the type is wrong)
todo := result.(*todo.Todo)

Building a Custom Transport

Here’s a complete example of using invokers to build a custom transport. This example creates a simple HTTP transport that takes the method name from a query parameter:
package customtransport

import (
    "encoding/json"
    "io"
    "net/http"

    contract "github.com/go-mizu/mizu/contract/v2"
)

// Mount creates a custom transport handler at the given path
func Mount(mux *http.ServeMux, path string, svc *contract.RegisteredService) {
    invoker := contract.DefaultInvoker(svc)

    mux.HandleFunc(path, func(w http.ResponseWriter, r *http.Request) {
        // 1. Get method name from query string
        methodName := r.URL.Query().Get("method")
        if methodName == "" {
            http.Error(w, "method query parameter required", http.StatusBadRequest)
            return
        }

        // 2. Find the method
        method := svc.Resolve(methodName)
        if method == nil {
            http.Error(w, "method not found", http.StatusNotFound)
            return
        }

        // 3. Read request body
        var body []byte
        if r.Body != nil {
            body, _ = io.ReadAll(r.Body)
        }

        // 4. Call the method through the transport invoker
        result, err := invoker.Invoke(r.Context(), method, body)
        if err != nil {
            // Check if it's a Contract error for proper status code
            var cErr *contract.Error
            if errors.As(err, &cErr) {
                http.Error(w, err.Error(), cErr.HTTPStatus())
            } else {
                http.Error(w, err.Error(), http.StatusInternalServerError)
            }
            return
        }

        // 5. Write response as JSON
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(result)
    })
}
Usage:
svc := contract.Register[todo.API](impl, contract.WithDefaultResource("todos"))
customtransport.Mount(mux, "/api", svc)

// Call: GET /api?method=create with JSON body {"title": "Test"}

Performance Characteristics

Understanding when operations are expensive helps you build efficient systems:
OperationWhenCost
Reflection analysisRegistration (startup)Slow, but only once
Invoker.Call()Every requestFast (direct function call)
Type assertionEvery requestVery fast
JSON parsingEvery requestNormal cost
Key insight: Heavy reflection happens once at startup. Runtime calls through invokers are as fast as regular function calls.

Common Patterns

Checking Method Capabilities

method := svc.Resolve("create")

// Does it expect input?
if method.Input != nil {
    fmt.Println("Input type:", method.Input.Name)
}

// Does it return output?
if method.Output != nil {
    fmt.Println("Output type:", method.Output.Name)
}

Iterating All Methods

desc := svc.Descriptor()

for _, res := range desc.Resources {
    fmt.Printf("Resource: %s\n", res.Name)
    for _, method := range res.Methods {
        fmt.Printf("  Method: %s\n", method.Name)
        fmt.Printf("    Has Input: %v\n", method.Input != nil)
        fmt.Printf("    Has Output: %v\n", method.Output != nil)
    }
}

Dynamic Method Dispatch

Useful for building generic tools that work with any service:
func callMethod(svc *contract.RegisteredService, name string, inputJSON []byte) ([]byte, error) {
    method := svc.Resolve(name)
    if method == nil {
        return nil, fmt.Errorf("unknown method: %s", name)
    }

    invoker := contract.DefaultInvoker(svc)
    result, err := invoker.Invoke(context.Background(), method, inputJSON)
    if err != nil {
        return nil, err
    }

    return json.Marshal(result)
}

See Also