Dependency Injection in Erlang: Parameters and Behaviors

Explore how to implement dependency injection in Erlang using function parameters and behaviors to create flexible and decoupled systems.

8.6 Dependency Injection via Parameters and Behaviors

In this section, we delve into the concept of dependency injection (DI) in Erlang, focusing on how it can be achieved through function parameters and behaviors. Dependency injection is a design pattern that helps in creating loosely coupled systems, making them easier to test, maintain, and extend.

Understanding Dependency Injection

Dependency Injection is a technique where an object or function receives its dependencies from an external source rather than creating them internally. This approach promotes separation of concerns and enhances modularity. The primary goals of dependency injection are:

  • Decoupling: Reduce the tight coupling between components, allowing them to evolve independently.
  • Testability: Facilitate unit testing by enabling the use of mock objects or stubs.
  • Flexibility: Allow easy swapping of components or services without modifying the dependent code.

Dependency Injection in Erlang

Erlang, being a functional language, approaches dependency injection differently compared to object-oriented languages. Instead of injecting objects, we inject functions or modules. Erlang’s functional nature, along with its powerful concurrency model, provides unique ways to implement DI.

Passing Dependencies as Parameters

One of the simplest ways to achieve dependency injection in Erlang is by passing dependencies as parameters to functions or modules. This method allows you to inject different implementations of a dependency at runtime.

Example:

Let’s consider a simple logging system where we want to inject different logging strategies.

1-module(logger).
2-export([log/2]).
3
4log(Message, LogFunction) ->
5    LogFunction(Message).

In this example, log/2 takes a Message and a LogFunction. The LogFunction is a higher-order function that defines how the message should be logged.

Usage:

11> ConsoleLogger = fun(Message) -> io:format("Console: ~s~n", [Message]) end.
22> FileLogger = fun(Message) -> file:write_file("log.txt", Message ++ "\n", [append]) end.
33> logger:log("Hello, World!", ConsoleLogger).
4Console: Hello, World!
54> logger:log("Hello, File!", FileLogger).

In this example, we define two logging strategies: ConsoleLogger and FileLogger. We can inject either of these into the log/2 function, demonstrating the flexibility of passing dependencies as parameters.

Using Behaviors to Abstract Dependencies

Erlang’s behaviors provide a way to define a common interface for different modules. By using behaviors, we can abstract dependencies and ensure that different implementations conform to the same contract.

Example:

Suppose we have a payment processing system with different payment gateways. We can define a behavior for the payment gateway interface.

1-module(payment_gateway).
2-export([process_payment/2]).
3
4-callback process_payment(Amount, Currency) -> ok | {error, Reason}.

Each payment gateway module must implement the process_payment/2 function.

Stripe Payment Gateway:

1-module(stripe_gateway).
2-behavior(payment_gateway).
3-export([process_payment/2]).
4
5process_payment(Amount, Currency) ->
6    % Stripe-specific implementation
7    io:format("Processing Stripe payment of ~p ~s~n", [Amount, Currency]),
8    ok.

PayPal Payment Gateway:

1-module(paypal_gateway).
2-behavior(payment_gateway).
3-export([process_payment/2]).
4
5process_payment(Amount, Currency) ->
6    % PayPal-specific implementation
7    io:format("Processing PayPal payment of ~p ~s~n", [Amount, Currency]),
8    ok.

Usage:

11> stripe_gateway:process_payment(100, "USD").
2Processing Stripe payment of 100 USD
3ok
42> paypal_gateway:process_payment(200, "EUR").
5Processing PayPal payment of 200 EUR
6ok

By using behaviors, we ensure that both stripe_gateway and paypal_gateway conform to the payment_gateway interface, allowing us to swap implementations easily.

Decoupling Modules with Dependency Injection

Decoupling modules is a key benefit of dependency injection. By injecting dependencies, we can separate concerns and allow modules to focus on their primary responsibilities.

Example:

Consider a notification system that can send notifications via email or SMS. We can decouple the notification sending logic from the notification creation logic.

Notification Module:

1-module(notification).
2-export([send_notification/3]).
3
4send_notification(User, Message, SendFunction) ->
5    SendFunction(User, Message).

Email Sender:

1-module(email_sender).
2-export([send/2]).
3
4send(User, Message) ->
5    io:format("Sending email to ~s: ~s~n", [User, Message]).

SMS Sender:

1-module(sms_sender).
2-export([send/2]).
3
4send(User, Message) ->
5    io:format("Sending SMS to ~s: ~s~n", [User, Message]).

Usage:

11> notification:send_notification("alice@example.com", "Hello, Alice!", fun email_sender:send/2).
2Sending email to alice@example.com: Hello, Alice!
32> notification:send_notification("1234567890", "Hello, Bob!", fun sms_sender:send/2).
4Sending SMS to 1234567890: Hello, Bob!

In this example, the notification module is decoupled from the actual sending mechanism, allowing us to inject different sending strategies.

Visualizing Dependency Injection in Erlang

To better understand how dependency injection works in Erlang, let’s visualize the process using a sequence diagram.

    sequenceDiagram
	    participant Client
	    participant Logger
	    participant ConsoleLogger
	    participant FileLogger
	
	    Client->>Logger: log("Hello, World!", ConsoleLogger)
	    Logger->>ConsoleLogger: execute("Hello, World!")
	    ConsoleLogger-->>Logger: Console: Hello, World!
	
	    Client->>Logger: log("Hello, File!", FileLogger)
	    Logger->>FileLogger: execute("Hello, File!")
	    FileLogger-->>Logger: (writes to file)

Diagram Description:

  • The Client calls the Logger with a message and a logging strategy.
  • The Logger delegates the logging task to the injected strategy (ConsoleLogger or FileLogger).
  • Each strategy handles the message according to its implementation.

Design Considerations

When implementing dependency injection in Erlang, consider the following:

  • Interface Consistency: Ensure that all implementations conform to the expected interface, especially when using behaviors.
  • Performance: Passing functions as parameters can introduce overhead. Evaluate the performance implications in performance-critical applications.
  • Complexity: While DI promotes flexibility, it can also introduce complexity. Use it judiciously to avoid over-engineering.

Erlang Unique Features

Erlang’s functional nature and concurrency model offer unique advantages for dependency injection:

  • Higher-Order Functions: Erlang’s support for higher-order functions makes it easy to pass functions as dependencies.
  • Behaviors: Erlang’s behavior mechanism provides a powerful way to define and enforce interfaces, promoting consistency across implementations.
  • Concurrency: Erlang’s lightweight processes allow for concurrent execution of different strategies, enhancing performance in distributed systems.

Differences and Similarities

Dependency injection in Erlang differs from object-oriented languages in that it focuses on injecting functions or modules rather than objects. However, the core principles of decoupling and flexibility remain the same.

Try It Yourself

Experiment with the code examples provided in this section. Try creating your own logging strategies or payment gateways. Modify the implementations to see how easily you can swap dependencies without changing the core logic.

Knowledge Check

  • What are the primary goals of dependency injection?
  • How does Erlang’s functional nature influence its approach to dependency injection?
  • What are the benefits of using behaviors for dependency injection in Erlang?

Embrace the Journey

Remember, this is just the beginning. As you progress, you’ll discover more ways to leverage dependency injection to build flexible and maintainable systems. Keep experimenting, stay curious, and enjoy the journey!

Quiz: Dependency Injection via Parameters and Behaviors

Loading quiz…
Revised on Thursday, April 23, 2026