DEV Community

Matthias Friedrich
Matthias Friedrich

Posted on • Edited on

Part 4: Advanced Registration Patterns in Go with Parsley

Scaling Beyond Simple Registrations

In the previous parts of this series, we explored the basics of service registration and the critical role of lifetimes and scopes. As your Go application grows from a handful of services to dozens or even hundreds, managing all registrations in a single main.go function becomes impractical. It leads to "god files" that are difficult to navigate and maintain.

Furthermore, real-world applications often require more than just static wiring. You might need to:

  • Group related services into logical, reusable units.
  • Pass runtime configuration parameters to services during initialization.
  • Manage multiple implementations of the same interface (e.g., different storage backends).

In this article, we will explore Advanced Registration Patterns in Parsley that address these challenges: Modules, Parameterized Constructor Closures, Named Services, and Context-Aware Factory Functions.

1. Service Modules: Organizing for Growth

Parsley Modules provide a structured way to group related service registrations. Instead of cluttering your entry point, you can encapsulate the registration logic for a specific feature or package within a module function.

Architecture: The Module Pattern

A Parsley module is simply a function that accepts a types.ServiceRegistry and returns an error. This approach allows you to keep implementation types private while exposing only the interfaces and the registration module.

func greeterModule(registry types.ServiceRegistry) error {
    // Register related services here
    registry.Register(NewGreeter, types.LifetimeTransient)
    return nil
}
Enter fullscreen mode Exit fullscreen mode

Practical Example: Registering a Module

You integrate a module into your registry using the RegisterModule method.

package main

import (
    "github.com/matzefriedrich/parsley/pkg/registration"
    "github.com/matzefriedrich/parsley/pkg/types"
)

func main() {
    registry := registration.NewServiceRegistry()

    // Group registrations into a logical unit
    registry.RegisterModule(userModule)
    registry.RegisterModule(orderModule)

    // ...
}
Enter fullscreen mode Exit fullscreen mode

Conditional Registration

Parsley also supports RegisterModuleIf, allowing you to enable or disable entire sets of services based on environment variables or configuration flags—ideal for feature flagging or environment-specific mocks.

// Register the DebugModule only in development
_ = registry.RegisterModuleIf(os.Getenv("ENV") == "dev", DebugModule)
Enter fullscreen mode Exit fullscreen mode

2. Parameterized Constructor Closures: Dynamic Configuration

Standard constructor functions are excellent for static dependency wiring. However, sometimes you need to inject values that aren't known until registration time, such as a specific API endpoint or a localized salutation.

Note: This is a plain Go closure technique, not Parsley's built-in RegisterFactory feature (covered later in this article). It relies only on standard Go functions, so it works with any registration method.

The Pattern: Functions Returning Constructors

A function that returns a constructor lets you "bake in" configuration values via closures before registering the result with Parsley.

func NewGreeterFactory(salutation string) func() Greeter {
    return func() Greeter {
        return &greeter{salutation: salutation}
    }
}
Enter fullscreen mode Exit fullscreen mode

Registration and Usage

When you register the result of this factory, Parsley treats the returned anonymous function as the actual service constructor.

// Register the greeter with a specific salutation
_ = registration.RegisterTransient(registry, NewGreeterFactory("Hi"))
Enter fullscreen mode Exit fullscreen mode

3. Context-Aware Factory Functions: On-Demand Resolution

Sometimes you don't want a service resolved eagerly at registration time. You want a handle you can call later, with a specific context.Context, to resolve the service exactly when you need it. Parsley's features.RegisterFactory[T] addresses this by registering a FactoryFunc[T] alongside the actual service.

The FactoryFunc Type

A context-aware factory function has the following shape:

type FactoryFunc[T any] func(ctx context.Context) (T, error)
Enter fullscreen mode Exit fullscreen mode

When invoked, it uses the underlying Parsley resolver to find and return the requested service for the given context.

Registering and Resolving a Factory

func main() {
    registry := registration.NewServiceRegistry()

    // Register the actual service
    _ = registration.RegisterSingleton(registry, NewGreeter)

    // Register a FactoryFunc for the Greeter service
    _ = features.RegisterFactory[Greeter](registry, types.LifetimeSingleton)

    resolver := resolving.NewResolver(registry)
    ctx := context.Background()

    // Resolve the factory function
    factory, _ := resolving.ResolveRequiredService[features.FactoryFunc[Greeter]](ctx, resolver)

    // Use the factory to resolve the service on demand
    svc, _ := factory(ctx)
    fmt.Println(svc.Greet())
}
Enter fullscreen mode Exit fullscreen mode

This pattern is useful for deferred or contextual resolution, and for breaking circular dependencies by resolving one side of the cycle on demand rather than at construction time.

4. Named Services: Managing Multiple Implementations

A common architectural requirement is having multiple implementations of the same interface coexist. For example, you might have a DataService that reads from a local cache and another that fetches from a remote API.

Registering Named Services

You can associate a unique name with each implementation using RegisterNamed. This allows you to specify different implementations and even different lifetimes for each.

_ = features.RegisterNamed[DataService](ctx, registry,
    registration.NamedServiceRegistration("remote", 
        NewRemoteDataService, 
        types.LifetimeTransient),
    registration.NamedServiceRegistration("local", 
        NewLocalDataService, 
        types.LifetimeTransient))
Enter fullscreen mode Exit fullscreen mode

Resolving via Service Factory

To resolve a specific named implementation, Parsley provides a powerful "Service Factory" resolution pattern. You resolve a function that takes a string (the name) and returns the service.

// Resolve the factory function
factory, _ := resolving.ResolveRequiredService[func(string) (DataService, error)](scope, resolver)

// Request the specific implementation by name
remoteService, _ := factory("remote")
localService, _ := factory("local")
Enter fullscreen mode Exit fullscreen mode

Note: Implementation types registered with names are also automatically available as a list. We will explore Service Lists in detail in the next part of this series.

Operational Considerations

Encapsulation and Security

By using modules, you can keep implementation structs unexported in your packages. This enforces the use of interfaces and prevents developers from bypassing the DI container to instantiate services manually, leading to a more consistent architecture.

Separation of Concerns

Advanced registration patterns help separate your Application Logic from your Wiring Logic. Your services remain clean and unaware of how they are being registered or grouped, making them more portable and easier to test in isolation.

Tradeoffs and Limitations

  • Complexity: While named services and factory functions offer great flexibility, they can make the dependency graph harder to visualize. Use them sparingly for clear use cases rather than as a default for every service.
  • Type Safety: When resolving via the service factory func(string) (T, error), passing an incorrect name string will only be caught at runtime. Ensure you have proper error handling or use constants for service names.
  • Indirection: features.RegisterFactory[T] adds a layer of indirection between the caller and the resolved service. Reach for it when you specifically need deferred or contextual resolution, not as a default replacement for direct constructor injection.

Summary

Advanced registration patterns transform Parsley from a simple DI container into a robust tool for managing complex application architectures. Modules provide organization, parameterized constructor closures and RegisterFactory enable dynamic and on-demand configuration, and named services offer the flexibility to manage multiple implementations of the same contract.

In the next part of this series, we will explore Dependency Resolution Techniques, where we dive into lazy loading, resolving lists of services, and providing manual dependencies during resolution.

Next Steps

  • Implement a module in your current project to group related services.
  • Try using a constructor closure to inject a configuration value into a service.
  • Try features.RegisterFactory[T] where you need deferred or contextual resolution.
  • Explore the Register Module and Context-Aware Factory Functions documentation for more advanced examples.

Top comments (0)