Skip to main content

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:

  1. A component injects a feature service (e.g. FeaturePlaceService).
  2. The feature service injects dataService and syncService to fetch and sync data.
  3. dataService injects syncService internally; it creates DataObject instances and passes itself to them.
  4. syncService injects adhocService, storeService, stateService, etc. to coordinate HTTP, storage, and state.
  5. DataObject and ViewObject are plain classes — they receive dataService via 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

LayerLibraryKey servicesWhat you use it for
5FeaturesFeaturePlaceService, FeatureLoginService, …Domain logic, screens, workflows
4upp-wdgtuppRendererUI widgets and rendering
3upp-datadataService, syncService, cacheService, loginServiceData, sync, cache, auth
2upp-basestateService, viewService, httpService, adhocService, storeService, …Infrastructure: HTTP, storage, state, i18n, alerts
1upp-defsAppConstants, GenericUtilsConstants 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 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 providers array
  • 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_SCHEMA allows 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:

  1. APP_BASE_HREF: Reads the <base> tag from index.html to support deployment under a subpath (e.g., /unpispas-pos/).

  2. langsPath: Constructs the path to language files based on the base href. This token is injected by languageService to 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., Connection accesses sync.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.