DEV Community

Matthias Friedrich
Matthias Friedrich

Posted 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, Factory Functions, and Named Services.

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. Factory Functions: 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.

The Pattern: Functions Returning Constructors

In Parsley, a "Factory Function" is a pattern where you create a function that returns a constructor. This allows you to "bake in" configuration values via closures.

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. 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.

Summary

Advanced registration patterns transform Parsley from a simple DI container into a robust tool for managing complex application architectures. Modules provide organization, factory functions enable dynamic 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 factory function to inject a configuration value into a service.
  • Explore the Register Module documentation for more advanced examples.

Top comments (0)