2

We can use Angular's @Injectable decorator to make a service available for dependency injection throughout the application. In this case, the service class itself will be used as the injection token; whoever injects it must know the class.

We can also define injection providers based on arbitrary injection tokens, but as far as I could find out, we can pass these only to newly created nested injectors.


I am working on a large Angular-based application that consists of many modules for different subject areas. Plenty of the modules will have to inject services from other modules, which invariably leads to circular references (on the TypeScript level) between the modules.

circular references between modules

The code of the injectable services must remain inside the modules (for otherwise, splitting everything up in modules would be pointless and I would end up with a single huge shared module).

However, I can extract interfaces for each service and place them into one or more modules that are compiled before any of the actual code modules. This results in the following structure (depicted in a simplified manner - actually, what is injected is an injection token typed to the interface):

circular references resolved

But now, where and how do I define static providers for these tokens?

  • Where: Presumably, somewhere as close to the root as possible ... is the idea that I concatenate all the providers into the providers array in the component that happens to represent the root of my UI?
  • How: This one seems harder. I cannot define the providers in the shared modules, as those must not have access to the modules with the service classes. I think they cannot be in the modules together with the services, either, because I do not know when those are loaded (upon request). How do I add this last missing link?

The only solution I can think of right now is to essentially write my own dependency injection system, separately from Angular's, in such a way that it includes lazy loading of modules, then register each pair of token + lazily loaded service class from one overarching module that is compiled last, but loaded first.


Context: The application I am working on consists of roughly 250 modules. Each module contains functionality related to a specific domain topic. These modules are typically lazy-loaded when the respective functionality in the application is accessed.

The ~150 developers working on the application are split up into teams of (on average) 5 to 10 people each. Each team is responsible for the features, internal technical design, and implementation of a couple of modules, about whose domain topic they are knowledgeable about.

Most of the domain-specific modules use a couple of services from several other domain-specific modules to link or combine functionality. Again, each module team is responsible for design and development of the publicly reachable interface of their modules (e.g. services usable by other modules).

F-H
  • 663
  • 1
  • 10
  • 21
  • 1
    If you are only importing types then why do you need angular's dependency injection for those modules? You can just use `import type {...} from '...'` – Mike Jerred Jul 22 '23 at 08:45
  • Also, you can specify `providedIn: 'root'` on your singleton services that are available application-wide. Then you don't need to specify them as providers anywhere. – Mike Jerred Jul 22 '23 at 08:49
  • @MikeJerred: "why do you need angular's dependency injection for those modules?" - well, why should anyone be using Angular's dependency injection at all instead of just importing? I suppose answers to that question apply equally to my project. But I am by no means sure about that! Beside the (non-production) advantage of being able to replace dependencies, e.g. for unit testing, the major benefit from using DI back in JavaScript + AngularJS was to connect code from multiple files in an organized fashion in the first place. Now, with TS and `import` statements, that is a bit of a moot point. – F-H Jul 22 '23 at 09:10
  • @MikeJerred: "Also, you can specify providedIn: 'root' on your singleton services that are available application-wide." - but this way, the only way to inject the service is via the service class. That is, the calling code (e.g. `FirstComponent`) has to *know* the service class (e.g. `SecondService`). On the TypeScript level, this leads the described circular reference between modules if the constellation is as depicted in my first UML diagram. – F-H Jul 22 '23 at 09:11
  • Imho you want a workaround and leave the root problem unsolved. Solve the circular dependency first. – derstauner Jul 22 '23 at 10:22
  • @derstauner: Could you be more specific, please? How do I "solve" the circular dependency, other than by introducing and referencing the interfaces I described? – F-H Jul 22 '23 at 11:07
  • For me, it sounds like the module structure is simply not correct. Actually putting the stuff into one module would actually be a solution because it is coupled, just moving it into another package will only obfuscate that. It seems to me the answer to this really depends on the actual code, e.g. are there several methods on each Service and are some methods only used in one particular module? Is it a REST or whatever Service where one could argue that the services belong into an "api" module? And so on. There is no silver bullet without at least getting some more info. – Loop Jul 22 '23 at 15:55
  • For angular circular DI you can use [`forwardRef`](https://github.com/MintPlayer/mintplayer-ng-bootstrap/blob/master/libs/mintplayer-ng-bootstrap/navbar/src/navbar-dropdown/navbar-dropdown.component.ts#L20). For typescript circular references you must use `import type` – Pieterjan Jul 22 '23 at 22:16
  • @Loop: I have edited a brief outline of the project dimension and structure (see section "Context"). It seems you are suggesting I lump much of the functionality together in a huge monolithic "shared-for-everyone" module where all developers from all teams should edit around. I am not convinced of this approach; the separation into modules also works well on the backend-side, where we are using C# that has the same restrictions concerning build order of assemblies. Indeed, it is often multiple methods of services that are invoked from code in other modules, some only by one ... – F-H Jul 22 '23 at 22:44
  • ... specific other module, others by many. The services accessed by multiple modules (effectively, several hundred, if not more than thousand) offer specific business domain functionality; they are not part of the basic technological framework for the application (as a generic REST service would be, for instance). – F-H Jul 22 '23 at 22:46
  • @Pieterjan: Thank you, I will look into the `forwardRef` functionality. I am also going to try out [`import type`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-8.html), though some remarks in the docs make me worry it might not be the right feature for my problem: "It *always* gets fully erased" sounds like it behaves just like interfaces, which cannot be used in Angular's injection tokens, either. And "you can’t do things like extend from it" may be problematic for properly working with the imported types, as well. I'll have to check this. – F-H Jul 22 '23 at 22:53
  • If module A depends on B, then B can't depend on A. It's a logical issue that must be solved before even Angular is involved (think of not using Angular at all, you would still have this issue with just imports). It's possible, although cumbersome, to split these modules in parts (or extract common parts into a separate module) to have one-way dependency graph – Max Jul 22 '23 at 23:03
  • and of course `import type` has no application here, since as you already know, types and interfaces have no runtime value (thank god) and can't be used as injection tokens – Max Jul 22 '23 at 23:08
  • looking at the first picture where Module1 uses something from Module2 and Module2 uses something from Module1, what is the reason to keep these in separate modules? The only reason would be to be able to consume (load) one module without the other, but it's not the case here, they both depend on each other – Max Jul 22 '23 at 23:13
  • @Max: "If module A depends on B, then B can't depend on A. It's a logical issue that must be solved before even Angular is involved" - please look at the second picture, where exactly this is already done by means of interfaces. "what is the reason to keep these in separate modules?" - please read the section starting at "Context:" to get an idea of the landscape and organization of the application in question. Let me know if any information is missing that should be added for further clarity, please. "The only reason would be to be able to consume (load) one module without the other, ... – F-H Jul 23 '23 at 00:01
  • ... but it's not the case here" - yes, it actually is. When a user uses functionality from `Module1`, for example, it is not guaranteed (but entirely possible) that they do use the particular functions that require `SecondService`. Obviously, in real life, we're not talking about four classes, but more about 10,000, with at least a dozen components and services in each module. – F-H Jul 23 '23 at 00:03
  • Yes, i had to write [my own](https://github.com/MintPlayer/mintplayer-ng-bootstrap/blob/9505fc5f0e7de235d44312a768c5d4e845924083/libs/mintplayer-ng-bootstrap/resizable/src/providers/resizable.provider.ts#L2) InjectionToken – Pieterjan Jul 23 '23 at 00:15
  • [This](https://github.com/MintPlayer/mintplayer-ng-bootstrap/blob/9505fc5f0e7de235d44312a768c5d4e845924083/libs/mintplayer-ng-bootstrap/resizable/src/resize-glyph/resize-glyph.directive.ts#L3C1-L3C1) is the place where I had my problem – Pieterjan Jul 23 '23 at 00:23

0 Answers0