Building a Custom Drupal 11 Module: The Architecture Decisions That Matter
One of the most common Drupal questions is:
How do I build a custom module?
The internet has thousands of answers.
Most look something like this:
- function mymodule_form_alter() {}
Or:
- function mymodule_node_insert() {}
Those tutorials aren’t wrong.
They’re just incomplete.
Because the difficult part of building a Drupal module isn’t creating the module.
The difficult part is building a module that remains maintainable three years later.
Most custom modules start simple.
Then requirements grow.
Features accumulate.
Business rules multiply.
Eventually the module becomes difficult to understand, difficult to test, and difficult to modify.
The difference between a maintainable module and a future headache usually comes down to architecture.
This article focuses on those decisions.
The First Question
Before writing code, ask:
Should this be a custom module at all?
This is surprisingly important.
I’ve inherited projects where developers built:
- Custom redirects
- Custom metadata systems
- Custom workflow systems
- Custom media solutions
even though mature solutions already existed.
Custom code creates responsibility.
Every line of custom code becomes something your organization must:
- Maintain
- Upgrade
- Test
- Support
The best custom module is often the one you never had to write.
Think In Terms Of Business Capabilities
One mistake developers make is organizing modules around technical features.
Example:
- custom_formscustom_apicustom_utilscustom_helpers
These names tell us almost nothing.
Instead, think about business capabilities.
Examples:
- event_registrationcourse_managementmember_directoryartist_portal
The module should represent a business concern.
Not a technical implementation detail.
This creates cleaner boundaries and makes systems easier to understand.
Keep Business Logic Out Of Hooks
Hooks are powerful.
They’re also one of the fastest ways to create technical debt.
Example:
- function mymodule_node_insert() { // 200 lines of business logic}
This works.
Until it doesn’t.
Over time:
- More conditions appear
- More dependencies appear
- More exceptions appear
The hook becomes difficult to maintain.
Better Approach
Use hooks to trigger behavior.
Use services to implement behavior.
Example:
- Hook ↓Service ↓Business Logic
The hook remains simple.
The business logic remains reusable.
The architecture scales more effectively.
Embrace Dependency Injection
This is one of the most important architectural shifts in modern Drupal.
Many legacy examples still use:
- \Drupal::service()
or
\Drupal::entityTypeManager()
inside business logic.
While convenient, this creates tight coupling.
Better Approach
Inject dependencies.
Benefits include:
- Testability
- Maintainability
- Clarity
- Reduced coupling
A class should clearly communicate what it depends on.
Dependency injection makes those relationships visible.
Services Should Own Business Logic
When I review Drupal projects, I often look for service architecture first.
Questions include:
Where does the business logic live?
Is it reusable?
Is it testable?
Is it isolated?
The strongest modules generally have service layers.
Example:
- ArtistManagerMembershipServiceNotificationServiceReportingService
These names communicate intent.
Intent is valuable.
Event Subscribers Are Often Better Than Hooks
Developers frequently reach for hooks first.
Modern Drupal increasingly benefits from event-driven architecture.
Examples:
- Instead of:
hook_entity_insert()
Consider:
- Event Subscriber
Benefits:
- Better organization
- Clearer responsibility
- Easier testing
- Improved maintainability
Not every situation requires events.
But they are often worth evaluating.
Design Configuration Carefully
Configuration mistakes become painful over time.
Questions include:
What should be configurable?
What should be code?
What belongs in configuration management?
The wrong answer creates operational problems later.
Example
Good configuration candidates:
- API endpoints
- Feature toggles
- Business settings
Poor configuration candidates:
- Application logic
- Architectural decisions
Configuration should influence behavior.
Not define architecture.
Create Clear Module Boundaries
One of the easiest ways to create technical debt is allowing modules to do too much.
Example:
- event_manager
begins handling:
- Registration
- Payments
- Notifications
- Reporting
- Analytics
Suddenly the module owns everything.
Complexity increases rapidly.
Better Approach
Establish boundaries.
Example:
- event_managementevent_notificationsevent_reporting
Responsibilities remain clear.
Change becomes easier.
Think About APIs Early
Even if your project isn’t currently headless, ask:
Could this functionality eventually be exposed through an API?
The answer is increasingly yes.
Designing services properly creates future flexibility.
The API becomes another consumer of business logic rather than a separate implementation.
Logging Matters More Than Most Developers Think
Many custom modules provide almost no visibility.
When problems occur:
Nobody knows why.
Good modules provide useful logging.
Examples:
- Failed API calls
- Validation failures
- Business rule violations
Logs should help answer:
What happened?
without requiring code analysis.
Avoid The Utility Module Trap
I see this repeatedly.
Developers create:
- custom_utilshelper_modulecommon_functions
Over time these become dumping grounds.
Eventually nobody knows what belongs there.
If a module’s purpose cannot be explained clearly, that’s usually a warning sign.
Testability Is An Architectural Concern
Testing is often treated as a later activity.
It shouldn’t be.
Questions include:
Can this service be tested independently?
Are dependencies isolated?
Is business logic centralized?
Strong architecture naturally supports testing.
Weak architecture makes testing difficult.
Common Mistakes
Mistake #1
Putting Business Logic In Hooks
Hooks should trigger behavior.
Not contain it.
Mistake #2
Overusing Static Service Calls
Dependency injection creates healthier systems.
Mistake #3
Building Generic Utility Modules
Business capabilities create better boundaries.
Mistake #4
Ignoring Configuration Strategy
Configuration mistakes accumulate over time.
Mistake #5
Thinking Only About Today
Modules often live far longer than expected.
Design accordingly.
Module Architecture Checklist
Structure
- Clear module purpose
- Business capability defined
Services
- Business logic isolated
- Dependencies injected
Events
- Event subscribers evaluated
- Hooks minimized where appropriate
Configuration
- Settings externalized
- Logic kept in code
Testing
- Services testable
- Dependencies isolated
Operations
- Logging implemented
- Error handling reviewed
Final Thoughts
Building a Drupal module is easy.
Building a maintainable Drupal module is harder.
The difference rarely comes down to syntax.
It comes down to architecture.
Strong modules:
- Have clear responsibilities
- Isolate business logic
- Use services effectively
- Establish healthy boundaries
- Remain understandable over time
Those qualities matter far more than any individual API or hook.
Because eventually every module becomes legacy.
Good architecture simply makes that future easier to manage.
Need Help Reviewing Custom Drupal Architecture?
DrupalRX helps organizations evaluate custom modules, technical debt, service architecture, modernization strategies, and long-term maintainability.
If your Drupal codebase is becoming difficult to evolve, the problem may not be the feature set.
It may be the architecture.