di
Dependency Injection (DI) skill for the ikigai project
Installation
Follow the repository instructions for this skill and your agent. Review the files and dependencies before adding it to your workflow.
View source ↗GuidesSkill instructions
Dependency Injection (DI)
Core Concept
Dependency Injection - Pass dependencies to a module from the outside rather than having the module create or locate them internally.
Instead of:
void repl_init() {
config = load_config(); // Hidden dependency - where does it load from?
db = connect_database(); // Hidden dependency - what connection string?
llm = create_llm_client(); // Hidden dependency - which API key?
}
Do this:
void repl_init(config_t *cfg, db_t *db, llm_t *llm) {
// Dependencies are explicit - passed from caller
// This function doesn't know or care where they came from
}
Why DI Matters
Explicit Dependencies - Function signature reveals what it needs. No hidden file I/O, global state, or magic.
Testability - Pass mock/fake implementations in tests. Real implementations in production.
Composability - Modules become building blocks. Wire them together differently for different contexts.
No Global State - Eliminates hidden coupling. Multiple instances can coexist with different configs.
Clear Ownership - Caller controls lifecycle. Knows when dependencies are created and destroyed.
How ikigai Uses DI
Pattern 1: Explicit Constructor Injection
Main creates all dependencies, passes them down:
// From v1-architecture.md
int main(void) {
void *root_ctx = talloc_new(NULL);
// Load config
ik_cfg_t *cfg = NULL;
TRY(ik_cfg_load(root_ctx, "~/.ikigai/config.json", &cfg));
// Initialize database with config
ik_db_ctx_t *db = NULL;
TRY(ik_db_init(root_ctx, cfg->db_connection_string, &db));
// Initialize LLM client with config
ik_llm_client_t *llm = NULL;
TRY(ik_llm_init(root_ctx, cfg->openai_api_key, &llm));
// Initialize REPL - receives all dependencies
ik_repl_ctx_t *repl = NULL;
TRY(ik_repl_init(root_ctx, cfg, db, llm, &repl));
// Run
ik_repl_run(repl);
talloc_free(root_ctx);
return 0;
}
Key insight: main() is the composition root. It knows the full dependency graph. Everything else just receives what it needs.
Pattern 2: Context Parameter
First parameter is always the talloc context:
// Config loading
res_t ik_cfg_load(TALLOC_CTX *ctx, const char *path, ik_cfg_t **out_cfg);
// Database init
res_t ik_db_init(TALLOC_CTX *ctx, const char *conn_str, ik_db_ctx_t **out_db);
// REPL init
res_t ik_repl_init(TALLOC_CTX *ctx, ik_cfg_t *cfg, ik_db_ctx_t *db,
ik_llm_client_t *llm, ik_repl_ctx_t **out_repl);
Benefits:
- Caller controls memory lifetime
- Clear ownership hierarchy
- No hidden malloc() calls
- Everything hangs off root_ctx
Pattern 3: Struct Composition
REPL context holds injected dependencies:
struct ik_repl_ctx_t {
ik_cfg_t *cfg; // Injected config
ik_db_ctx_t *db; // Injected database
ik_llm_client_t *llm; // Injected LLM client
ik_term_ctx_t *term; // Created internally
ik_scrollback_t *scrollback;// Created internally
ik_input_buffer_t *input; // Created internally
// ... other fields
};
Two types of dependencies:
- External (cfg, db, llm) - Created before REPL, injected in
- Internal (term, scrollback, input) - REPL owns, creates during init
DI for Testing
Production Code
// production main.c
int main(void) {
ik_llm_client_t *llm = ik_llm_init(ctx, real_api_key);
ik_repl_init(ctx, cfg, db, llm, &repl);
// Uses real OpenAI API
}
Test Code
// test_repl.c
void test_repl_handles_llm_error(void) {
ik_llm_client_t *mock_llm = create_mock_llm_that_fails();
ik_repl_init(ctx, cfg, db, mock_llm, &repl);
// Uses mock that returns errors - no real API calls
}
Key: Same ik_repl_init() function works with real or mock dependencies.
Anti-Patterns to Avoid
Service Locator - Global registry of dependencies:
// BAD - hidden dependency
void repl_init() {
llm_client_t *llm = service_locator_get("llm"); // Where did this come from?
}
Singleton - Global static instance:
// BAD - global state
static config_t *g_config = NULL;
void repl_init() {
const char *key = g_config->api_key; // Hidden dependency on global
}
Constructor Does I/O - Function creates its own dependencies:
// BAD - hidden file I/O
void repl_init() {
config = load_config_from_disk(); // Which file? Can't test without filesystem
}
Null Checking Injected Deps - Don't do this:
// BAD - dependency is required, why allow NULL?
void repl_init(config_t *cfg, db_t *db, llm_t *llm) {
if (!cfg || !db || !llm) {
PANIC("Missing dependencies"); // If they're required, assert it
}
}
Use assertions for required dependencies:
// GOOD - document requirements
void repl_init(config_t *cfg, db_t *db, llm_t *llm) {
assert(cfg != NULL);
assert(db != NULL);
assert(llm != NULL);
// Proceed knowing dependencies exist
}
DI in C vs OOP Languages
No interfaces - C doesn't have interfaces. Use function pointers in structs for polymorphism:
typedef struct {
res_t (*send)(void *ctx, message_t *msg);
res_t (*stream)(void *ctx, message_t *msg, chunk_callback_t cb);
} llm_provider_vtable_t;
typedef struct {
llm_provider_vtable_t *vtable;
void *impl_ctx; // OpenAI context, Anthropic context, etc.
} llm_client_t;
No frameworks - No Spring, no Guice. Composition happens in main(). Simple and explicit.
Manual wiring - You write the composition logic. More code, but totally transparent.
Rules for ikigai
- main() is composition root - Creates all top-level dependencies, wires them together
- First param is TALLOC_CTX - Explicit memory ownership
- Pass dependencies as function parameters - Never load/create internally
- Modules don't know about config files - Config is loaded once, values passed down
- No global state - Everything owned by talloc hierarchy
- Assert required dependencies - Don't defensively check for NULL if it's a contract violation
Summary
Dependency Injection = Pass dependencies in, don't create them internally.
Benefits:
- Explicit dependencies (no surprises)
- Testable (mock dependencies)
- Composable (wire differently for different needs)
- No global state (multiple instances possible)
- Clear ownership (caller controls lifecycle)
In ikigai: main() creates config, database, LLM client. Passes them to REPL. REPL doesn't know where they came from. Easy to test with mocks. No hidden file I/O or global state.
Examples
Read the usage examples in the source repository.
View source ↗Source history
- Indexed on
- 2026-01-20
- Index updated
- 2026-01-26
- Branch
- main
Index and sync dates describe this directory. View the source repository for code history.
GitHub ↗chat Comments (0)
Sign in to join the discussion and leave a comment.
No comments yet. Be the first to share your thoughts!