“We may need this in another module one day” is both a useful observation and a dangerous architectural justification.
It is useful because some capabilities genuinely belong to a shared platform. It is dangerous because the team can easily build a framework for modules that do not exist and business needs it does not yet understand.
Platform and domain change for different reasons
The platform owns stable mechanisms used across several areas:
- identity and authentication,
- organizations, memberships and roles,
- authorization and impersonation,
- files,
- the audit log,
- request and job context,
- shared privacy rules.
Each domain module keeps its own meaning:
- models and states,
- use-case commands,
- domain policies,
- events,
- required documents,
- decision and completion rules.
A new module can use identity, organizations and audit without copying infrastructure. It does not have to fit a “universal workflow engine” designed before its requirements were known.
DRY does not mean similar-looking code
Two pieces of code are not the same abstraction merely because their conditional statements look alike. If they represent different business decisions and may change for different reasons, joining them creates coupling instead of removing duplication.
Share a mechanism when its contract is stable:
- recording an audit event,
- checking membership,
- propagating a correlation ID,
- storing a file,
- executing an idempotent command.
Do not automatically share meaning: who approves a decision, when a process is ready, which documents are required or what completion means in a specific domain.
Ask a class what it owns
A simple question helps: does this class describe a platform mechanism or a rule of one business process?
If the answer is “both”, it probably has too many reasons to change. Keep orchestration in the domain command and place the technical mechanism behind a small platform contract.
Domain Command
├─ Access Context
├─ File Storage
├─ Audit Recorder
└─ Domain Models
The domain depends on simple, stable services. The platform does not need to understand the meaning of a domain decision.
SOLID without ceremony
SOLID is not a contest for the largest number of interfaces. A class needs a clear reason to change, and dependencies should point from the changing domain toward stable contracts.
In Rails, ordinary Ruby objects are often enough:
- a command for a transactional use case,
- a query for a safe read,
- a policy for authorization,
- a value object for context,
- a presenter or component for UI.
An abstraction should emerge when a second real case reveals a common contract, not when the team tries to predict a hypothetical fifth module.
The simplest path to extensibility
Design the first module well. Observe what repeats in meaning. Extract a mechanism only when you can name its responsibility, input, output and failure behavior.
This allows a modular monolith to grow without either extreme: copying everything between modules or building an internal framework that nobody can safely change.
Design the platform for reuse, but do not design a domain you do not know yet.
See how we connect these boundaries with technology, delivery process and dedicated systems. If you need to restructure an existing product around clearer modules, start a conversation.

