Services Architecture
This page explains how Angular dependency injection (DI) works in the unpispas monorepo, the layered service structure, module configuration, and the conventions for service design.
Mental Model (Start Here)
Think of the architecture as a stack. Your feature sits on top. Each layer can only use layers below it — never above. This prevents circular dependencies and keeps the build order predictable.
Concrete flow when a component loads data:
- A component injects a feature service (e.g.
FeaturePlaceService). - The feature service injects
dataServiceandsyncServiceto fetch and sync data. - dataService injects
syncServiceinternally; it createsDataObjectinstances and passes itself to them. - syncService injects
adhocService,storeService,stateService, etc. to coordinate HTTP, storage, and state. - DataObject and ViewObject are plain classes — they receive
dataServicevia constructor, not Angular DI.
Angular DI resolves the service chain automatically. The data model layer stays outside DI: objects are created by ObjectFactory and receive dataService explicitly.
Layer Quick Reference
| Layer | Library | Key services | What you use it for |
|---|---|---|---|
| 5 | Features | FeaturePlaceService, FeatureLoginService, … | Domain logic, screens, workflows |
| 4 | upp-wdgt | uppRenderer | UI widgets and rendering |
| 3 | upp-data | dataService, syncService, cacheService, loginService | Data, sync, cache, auth |
| 2 | upp-base | stateService, viewService, httpService, adhocService, storeService, … | Infrastructure: HTTP, storage, state, i18n, alerts |
| 1 | upp-defs | AppConstants, GenericUtils | Constants and utilities |
Rule: Your feature (Layer 5) injects from Layers 3–4. You rarely inject upp-base services directly — upp-data already does that.
Service Layers (Detail)
Services follow the same layered dependency graph as the libraries. Each layer can only depend on services from the same layer or layers below. This prevents circular dependencies and ensures a clean build order.
Layer Overview
A simple stack: top depends on bottom.
┌─────────────────────────────────────┐
│ Layer 5: Features │ ← Your feature services
├─────────────────────────────────────┤
│ Layer 4: upp-wdgt (uppRenderer) │
├─────────────────────────────────────┤
│ Layer 3: upp-data │ ← dataService, syncService, cacheService, loginService
├─────────────────────────────────────┤
│ Layer 2: upp-base │ ← 12 services: state, view, http, adhoc, store, …
├─────────────────────────────────────┤
│ Layer 1: upp-defs │ ← AppConstants, GenericUtils
└─────────────────────────────────────┘
upp-base (Layer 2) — Internal Dependencies
upp-base has 12 services. Core ones: stateService, httpService, storeService. The rest support them (alerts, toast, language, clock, device ID, events, platform). For onboarding, you only need to know that upp-data uses these — you typically do not inject them from features.
Expand: full upp-base dependency graph
upp-data (Layer 3) — Internal Dependencies and Links to upp-base
upp-data has 4 services: dataService, syncService, cacheService, loginService. They depend on each other and on upp-base services. When you build a feature, you inject dataService and syncService; the rest is internal.
Expand: full upp-data dependency graph
providedIn: 'root' Pattern
Almost all services in the project use providedIn: 'root':
@Injectable({
providedIn: 'root'
})
export class syncService implements OnDestroy { ... }
This means:
- The service is a singleton — one instance for the entire application
- It is tree-shakable — if no component or service injects it, it is not included in the bundle
- It does not need to appear in any module's
providersarray - It is available immediately to any component or service that requests it
When Modules Are Still Needed
Modules (UppBaseModule, UppWdgtModule, feature modules) are needed only for:
- Declaring components and directives that need to be compiled
- Configuring module-level providers (e.g., Ionic Storage, HTTP client)
- Exporting components for use in other modules
UppBaseModule
The UppBaseModule in libs/upp-base/src/lib/upp-base.module.ts is the infrastructure module:
@NgModule({
declarations: [
modalInputComponent,
modalAlertComponent,
UiKioskBoardComponent,
UiKioskInputDirective,
modalAwayComponent,
UiAwayComponent
],
imports: [
CommonModule,
IonicModule.forRoot(),
IonicStorageModule.forRoot()
],
exports: [
UiKioskInputDirective,
UiKioskBoardComponent
],
providers: [
HTTP, // Cordova HTTP plugin
Network, // Cordova Network plugin
AndroidPermissions, // Android permissions
Geolocation, // Cordova Geolocation
provideHttpClient() // Angular HttpClient
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class UppBaseModule {}
Key responsibilities:
- Initializes Ionic and Ionic Storage with
forRoot() - Provides Cordova plugins for native device access
- Configures the Angular HTTP client
- Declares UI components used by the alert and kiosk systems
CUSTOM_ELEMENTS_SCHEMAallows Ionic web components in templates
Why IonicModule.forRoot() Is Here
Ionic must be initialized once at the root level. By placing it in UppBaseModule, any app that imports this module gets Ionic properly configured without needing to repeat the setup.
UppDataModule
The UppDataModule in libs/upp-data/src/lib/upp-data.module.ts is intentionally minimal:
@NgModule({
imports: [CommonModule],
})
export class UppDataModule {}
All upp-data services (dataService, syncService, cacheService, loginService) use providedIn: 'root', so they do not need to be declared in the module. The module exists primarily as a conventional entry point and for any future component declarations.
App Module Composition
The main application (unpispas-pos) composes everything in AppModule:
@NgModule({
declarations: [AppComponent, uppMainComponent],
imports: [
BrowserModule,
RouterModule.forRoot(appRoutes),
FormsModule,
ReactiveFormsModule,
UppBaseModule, // Infrastructure (Ionic, Storage, plugins)
UppWdgtModule, // UI widgets
FeatureLoginModule, // Login screens
FeatureTopbarModule, // Top navigation
FeatureUserModule, // User management
FeaturePlaceModule, // Place management
IonicModule.forRoot(),
NgcCookieConsentModule.forRoot({ ... })
],
providers: [
{ provide: APP_BASE_HREF, useFactory: ... },
{ provide: langsPath, useFactory: (baseHref) => `${baseHref}lang`, deps: [APP_BASE_HREF] }
],
bootstrap: [AppComponent],
})
export class AppModule {}
Provider Configuration
Two custom providers are defined at the app level:
-
APP_BASE_HREF: Reads the<base>tag fromindex.htmlto support deployment under a subpath (e.g.,/unpispas-pos/). -
langsPath: Constructs the path to language files based on the base href. This token is injected bylanguageServiceto load i18n JSON files.
Service Design Conventions
Constructor Injection
All dependencies are injected through the constructor:
@Injectable({ providedIn: 'root' })
export class syncService {
constructor(
public lang: languageService,
public store: storeService,
public cache: cacheService,
public adhoc: adhocService,
public state: stateService,
public clock: clockService,
public toast: toastService,
private deviceid: identificationService,
private alertCtrl: alertService
) { ... }
}
Note the visibility modifiers:
public— when child classes or helper objects need access (e.g.,Connectionaccessessync.adhoc)private— when only the service itself uses the dependency
Lazy Helper Initialization
Complex services use helper classes that are lazily initialized:
private _syncronizer: Syncronizator | null = null;
get synchonizer(): Syncronizator {
if (!this._syncronizer) {
this._syncronizer = new Syncronizator(this);
}
return this._syncronizer;
}
Helpers like Connection, PendingQueue, CacheManager, and Syncronizator are plain classes (not Angular injectables) that receive the parent service reference and access its injected dependencies through it.
OnDestroy Cleanup
Services that hold subscriptions or timers implement OnDestroy:
@Injectable({ providedIn: 'root' })
export class syncService implements OnDestroy {
ngOnDestroy() {
if (this._syncronizer) {
this._syncronizer.OnDestroy();
this._syncronizer = null;
}
}
}
Non-Injectable Classes
The data model classes (DataObject, ViewObject, and their subclasses) are not Angular injectables. They receive dataService as a constructor parameter:
export class Product extends _ProductClass {
constructor(
objid: string | null,
data: dataService, // passed explicitly, not injected
objoptions: ObjectOptions = {}
) {
super(productSchema, objid, data, objoptions);
}
}
The ObjectFactory creates instances by table name, passing the dataService reference:
ObjectFactory.object('PRODUCT', objid, dataService)
Dependency Flow Summary
The key insight: Angular DI handles service singletons (Component → Feature Service → dataService → syncService → …). The data model layer (DataObject, ViewObject) uses explicit constructor parameters — they receive dataService when created by ObjectFactory, not via Angular DI. This keeps model objects independent of the DI container while still having access to all necessary services.