Mods
A mod in StarterKit is best understood through the lens of a plugin architecture: a self-contained unit of functionality that can be plugged into the host application to deliver a specific business functionality out of the box.
This model differs fundamentally from a microservices decomposition. Microservices provide limited extension points (typically only startup configuration), and introduce well-known challenges around distributed transactions and data consistency. Mods, by contrast, execute within the same process and the same database transaction as the host application, giving them a much richer and more reliable set of extension mechanisms.
Core idea: a mod is an installable bounded business capability that runs inside the host application boundary and can be adapted by the host without forking the mod.
Each mod is expected to solve a specific business problem inside the application. Examples include billing, scheduling, task management, assessments, coverages, and integrations that are useful only for a subset of product installations.
Architectural Intent
Microservices provide operational and deployment isolation, but they offer a limited extension surface to the host application itself. In many cases, the host can only influence a service through textual configuration passed at startup or through remote API contracts. This boundary also introduces additional complexity around distributed transactions, consistency guarantees, retries, idempotency, and cross-service orchestration.
Mods are designed for a different trade-off. They keep the feature implementation inside the same deployable application boundary, which gives the host application stronger control over extension points while preserving a modular package structure. A mod can contribute domain models, interactors, repositories, API endpoints, GraphQL types, client-side API definitions, widgets, and optional integration packages without forcing the rest of the product into a distributed consistency model.
The result is an architecture where common product capabilities can be reused as installable packages, while product-specific behavior can still decorate, extend, or replace selected parts of the implementation.
Bounded Context and Naming
Principle: a mod should represent one bounded context: one coherent language, one model, and one set of business rules for a well-defined area of the application.
The bounded context is the semantic boundary of the mod. Inside this boundary, terms should have precise meanings and models should be designed around the mod's own business concepts. Outside this boundary, the host application may adapt the mod to a concrete product workflow through decorators, GraphQL extensions, UI composition, and integration code.
This means that a mod should not become a generic container for unrelated features. If two capabilities use different domain languages, have different invariants, or evolve for different reasons, they should usually be modeled as separate mods or as an explicit optional integration package.
The bounded context also defines where product-specific assumptions are allowed and how mod concepts should be named. Mod naming should follow the language of the mod's own bounded context, not the language of the current host application.
The current host application is a medical application, but mods should not automatically inherit medical terminology. Names such as Patient, Encounter, MedicalProvider, or similar concepts should be used only when they are native to the mod's own domain language.
For example, a coverages mod may use the term Patient because working with medical insurance companies naturally involves patients, coverage holders, payers, and insurance-specific concepts. In that bounded context, Patient is part of the domain language and can be a valid model name.
By contrast, a tasks mod should avoid medical names such as Patient, Encounter, or MedicalProvider when the task model itself does not require them. Task management should use task-specific terminology such as Assignee, Reporter, Participant, DueDate, Priority, Status, or TaskType. The host application may still attach a task to a patient or encounter through extensions, links, metadata, or host-level composition, but the core task language should remain independent from the medical domain.
This keeps mods reusable across different product installations and prevents the host application's current domain from leaking into packages that should remain broadly applicable.
Extension Principles
Mods should expose business capabilities, but they should not own every policy decision around how those capabilities are used in a concrete application.
The extension model is built around several stable capabilities:
- Interactor decoration for host policies. Mods do not authorize user actions by themselves. Authorization rules are applied by the host application by decorating mod interactors. The same mechanism can be used to add product-specific validation rules without modifying the mod source code. This keeps access control and validation close to the product context where roles, permissions, tenant rules, feature flags, and workflow constraints are known.
- Extensible domain models. Mods can expose models that are intentionally extensible.
mod-tasksis an example of this approach: the application can define task models and attributes that are meaningful for its own business workflow. This allows a mod to provide reusable behavior while leaving controlled space for domain-specific shape and metadata. - Composable reactions to model changes. The host application can react to changes in mod models by subscribing to domain or integration events, or by decorating interactors that mutate those models. Decorators are especially useful when the reaction must be coupled tightly to the original command execution.
- In-process transactional consistency. When a reaction to a mod model change is executed inside the same application boundary, it can participate in the same transaction as the original change. This is one of the core advantages of the mod architecture: the host can preserve data consistency without introducing distributed transaction coordination between independent services.
- Server-side schema extension. Hot Chocolate allows the host application to extend the data returned by a mod by adding fields to the GraphQL schema. This makes the server API composable: a mod can define the base GraphQL contract, while the host can attach additional fields, resolvers, and data loaders required by the concrete product.
- Client-side model extension. The frontend can use extensible GraphQL schemas to extend client-side models with additional fields. This keeps client data contracts aligned with server-side schema extensions and allows product-specific UI flows to consume additional data without forking the mod.
- UI composition and replacement. The frontend uses
@webinex/flexyto replace, add, and remove UI components. This makes the UI layer extensible in the same way as the backend layers: the mod can provide default widgets and screens, while the host application can adapt the user experience to specific product requirements.
Structure
Mods usually have backend and frontend parts. The backend side is distributed as one or more .NET packages, while the frontend side is distributed as a single NPM package.
Backend
Backend packages are split by responsibility. The exact package set depends on the mod size, dependency profile, and extension requirements.
Simple Mod Schema
Complex Mod Schema
Mod.Core
Example: StarterKit.Mod.Billing
The core package contains the business logic of the mod. It typically includes aggregate roots, entities, value objects, interactors, repository interfaces, domain services, domain events, and other classes that describe the mod's behavior.
The core package should remain focused on business rules and domain behavior. Infrastructure concerns should be represented through interfaces or abstractions instead of direct dependencies on concrete persistence or integration implementations.
Mod.Api
Example: StarterKit.Mod.Billing.Api
The API package exposes the public server-side API of the mod. It typically contains IEndpointRouteBuilder extensions, GraphQL object types, query extensions, mutation endpoints, data loaders.
This layer translates the mod's business capabilities into HTTP and GraphQL contracts consumed by the host application and frontend clients.
Mod.Infrastructure
Example: StarterKit.Mod.Billing.Infrastructure
The infrastructure package contains technical implementations required by the core package. It typically includes repository implementations, Entity Framework model configurations, persistence adapters, external service clients, and wrappers around third-party libraries.
Infrastructure packages also help keep the core package dependency-light. When the mod needs to interact with a third-party library or service, the infrastructure layer should usually provide the concrete implementation behind a core-facing abstraction.
Mod.Interactors (optional)
An interactors package contains application-level operations that describe how users interact with the system through the mod. These interactors are the primary extension point for authorization, validation, transactional reactions, and other host-level policies.
Mod.Abstractions (optional)
An abstractions package contains contracts for code that needs to use the mod without depending on the full core package. Typical contents include facade interfaces, argument types, result types, and small supporting contracts.
This package is useful when the core package contains dependencies that are irrelevant or undesirable for consumers, such as repository interfaces, domain implementation details, or third-party library references.
Mod.<Addon> Packages (optional)
A mod may provide additional optional packages for integrations or use cases that are not required by every installation. For example, a billing mod may expose a package for a specific external payment provider. Applications that need the integration can reference it, while other applications can use the mod without taking that dependency.
Frontend
Frontend mods are represented as a single NPM package.
The package contains the frontend surface required by the mod: UI widgets, Redux Toolkit Query endpoints, GraphQL documents, TypeScript models, reusable components, and sometimes pages when a mod is a standalone functional unit.
The frontend package should provide useful default UI building blocks, but it should also be designed for host-level composition. Components should be replaceable or removable through @webinex/flexy, and data models should be extendable through GraphQL schema extension so the host application can adapt the mod to product-specific workflows.