“The Koans” series consists of excerpts from my conversations with other engineers whom I turn to when I have questions. I usually come up with a question, ask them, and then try to understand their response. I then add some of my own insights to ensure my understanding (and potentially help others understand as well) before publishing them on this blog.

Pretext

Today, I started a project from scratch and added a core module. I then asked myself, “It might be useful to place this and that into the core module for reuse later—but what if I end up injecting it into every module and submodule? Wouldn’t that violate loose coupling and the Single Responsibility Principle?” Seeking clarity, I turned to Mohsen for guidance. That conversation led to a moment of enlightment.

What is a core module?

A :core typically refers to a library module that contains shared code or utilities used across multiple modules in a project (e.g., :core, :common, :base, etc.). These modules are often created to avoid duplication and promote reuse of code such as:

  • Extensions functions
  • Utility classes
  • Network/data models
  • Constants
  • Helper methods

How should I inject them in my other modules?

Short answer: You shouldn’t. You should try to get rid of this module.

Why?

Introducing a core module sets off a chain reaction (like tipping the first of three dominos) each leading to increasing architectural degradation.

1. Ambiguity of “Core”

The term core, or its equivalent, is ambiguous and often confusing. There is no clear consensus among developers about what should reside within a core module. Should it contain core business logic? Should it be limited to base classes and foundational abstraction layers? Should it even include any core functionality at all? Or should it be a completely independent module containing only tools and utility classes? This lack of clarity inevitably leads to reduced readability and long-term confusion and also makes the core module

2. Prone to being misuesd

At some point, someone will inevitably start adding code to the core module that was never intended to be placed there. Due to this ambiguity, developers may treat the core module as a dumping ground for code they’re unsure where to place. As a result, the core module (originally meant to promote clean, low-footprint architecture) can devolve into a verbose, catch-all module containing everything from network handlers to formatters and mapper utilities.

3. Inevitablity of a refactor

Eventually, someone will need to address the problems introduced by the core module. If they want to avoid side effects—such as having to include the entire core module just to use a simple logger, or multiple developers simultaneously modifying it because they each decide their logic belongs there—then a refactor becomes inevitable. The outcome? The monolithic core module gets broken down into smaller, focused modules like network, local_storage, logger, and so on. So why not distribute these responsibilities from the start and avoid unnecessary overhead later?

4. An orphan module (Lack of ownership)

When a core module becomes a dumping ground for everything that might be useful somewhere else (utility functions, constants, extensions, logging helpers, DI setup, network models, etc.) no one feels responsible for maintaining it. It becomes a “shared” place where anyone can add anything without clear rules or ownership. Without ownership, bugs go unfixed, outdated code piles up, and new developers don’t know where to look. Developers don’t know what belongs in :core, and Because everyone uses :core, no one wants to touch it for fear of breaking everything.

Verdict

While the idea of a core module may seem like a smart way to centralize reusable components, in practice it often introduces more problems than it solves. Its vague purpose, tendency to attract unrelated code, and lack of clear ownership can lead to significant architectural decay over time. Rather than relying on a monolithic core, favor small, well-named, purpose-specific modules from the start. This approach not only promotes clean boundaries and single responsibility but also makes your codebase easier to navigate, test, and evolve. When in doubt, split it out.

Mantra

“Core isn’t a junk drawer. Organize from day one.”