Skip to content

Dependency Injection

Ashutosh Gangwar edited this page Jun 19, 2025 · 4 revisions

When working with background jobs in Goose, a common requirement is providing stateful dependencies, such as database connections or caches, to your job functions. This document outlines the recommended and alternative patterns for dependency injection.

Injecting Arguments using Middlewares (Recommended)

We recommend using a custom middleware for passing dependencies to job functions. The middleware intercepts a job before execution and appends or prepends the necessary dependency to the job function's argument list. Consider a job that requires a database connection. You can create a middleware that adds the db connection as the first argument to the job.

(defn inject-db
  [db]
  (fn [next]
    (fn [opts job]
      (next opts (update job :args #(cons db %))))))

(defn init-worker [db & other-dependencies]
  (let [worker-opts (assoc w/default-opts
                           :broker     consumer
                           :middleware (comp ; top to bottom in first-in-last-out order.
                                        (some-other-middleware)
                                        (inject-db db))]
    (w/start worker-opts)))

The inject-db middleware takes a db connection and returns a function that modifies the job's arguments before the next middleware or the job itself is called. It prepends the db object to the existing arguments without modifying other arguments.

When you enqueue a job, you do so without the dependency argument:

(defn my-task [db arg-1 arg-2]
  (do-something db arg-1 arg-2))

(client/perform-async `my-task arg-1 arg-2)

Tip

All your job functions will receive db as the first argument when using the inject-db middleware. Any job function that doesn't require the db dependency will need to ignore the first argument explicitly.

Using Global State

With this approach, we use global state to hold the dependencies. We do not recommend it because it creates an unpredictable state.

  • It allows any part of the application to access a component's internal state, violating the system's lifecycle and boundaries.
  • It makes testing complex and leads to tightly coupled code.

Dynamic Bindings

(def ^:dynamic *database* nil)

(defn inject-db
  [db]
  (fn [next]
    (fn [opts job]
      (binding [*database* db]
        (next opts job)))))

With this middleware in place, your job function (my-task) can access the database connection through the *database* var without passing it as an explicit argument. This can simplify the function signature, but relies on dynamic vars.

Global Var

Another method is to set the dependency in a globally accessible var when your application components initialise. The job functions can reference that var as needed.

Clone this wiki locally