Avoiding circular refs

in NestJS

@Global()

@Global()

@Global()
@Module({
  imports: [...],
  controllers: [...],
  providers: [...],
  exports: [...],
})
export class SomeModule {}

@Global()

  • You don't need to import global module into feature modules 
  • You           need to import global module once into AppModule

@Global()

  • No circular refs between modules
  • No way to provide overrides for different feature modules
    (I don't think we use it)
  • Still can have circular refs between services
  • NestJS says it's bad practice

I don't care

@Global()

Module per service

Module per service

Crazy idea, huh?

Module per service

Nope

Module per service




@AtomicService({
  imports: [AppConfigModule],
})
export class SurveyScheduleAutoremindersFn {
  constructor(
    private readonly appConfig: AppConfigService,
    private readonly someOtherFn: SomeOtherFn
  ) {}
  
  ...
}

automated sugar

Module per service

@Module({
  imports: [AppConfigModule, SomeOtherFn],
  providers: [SurveyScheduleAutoremindersFn],
  exports: [SurveyScheduleAutoremindersFn]
})
@Injectable()
export class SurveyScheduleAutoremindersFn {
  constructor(
    private readonly appConfig: AppConfigService,
    private readonly someOtherFn: SomeOtherFn
  ) {}
  
  ...
}

which will be convered into smth like this

(that's a lie, it's a bit more complex than that, but PoC works)

Module per service

@Module({
  imports: [AppConfigModule, SomeOtherFn],
  providers: [SurveyScheduleAutoremindersFn],
  exports: [SurveyScheduleAutoremindersFn]
})
@Injectable()
export class SurveyScheduleAutoremindersFn {
  constructor(
    private readonly appConfig: AppConfigService,
    private readonly someOtherFn: SomeOtherFn
  ) {}
  
  ...
}

auto-imports

Module per service

@Module({
  providers: [
    ...,
    // uses SurveyScheduleAutoremindersFn
    SurveysService, 
  ],
  imports: [
    ...,
    // thus we need to import module
    getModule(SurveyScheduleAutoremindersFn)
  ],
  exports: [...]
})
export class SurveysModule

interop with regular modules (ugly)

Module per service

@SpanModule({
  providers: [
    ...,
    // uses SurveyScheduleAutoremindersFn
    SurveysService, 
  ],
  imports: [
    ...,
    // we don't need to import AtomicModule 
    // cuz @SpanModule does that for us
  ],
  exports: [...]
})
export class SurveysModule

interop with regular modules (nice)

Module per service

what's left in regular modules?

  • controller definitions
  • bullmq-related definitions
  • services with cron

Module per service

Why?

Module per service

@Global()

@Module({
  imports: [
    FirstControllerModule,
    SecondControllerModule,
    
    // ugly imports
    FirstServicesModule,
    SecondServicesModule,
    ThirdServicesModule,
  ]
})
export class AppModule {}

Module per service

Atomic Services

@Module({
  imports: [
    FirstControllerModule,
    SecondControllerModule,
    
    // no ugly imports
    // at all
    // forever
    // cuz it's automated
  ]
})
export class AppModule {}

easier to add services → more services → granular services

Module per service

Circular refs in services

Circular refs in services

The vast majority (70%? 80%? 90%?)
of code is not modeled in OOD paradigm

(or any paradigm at all)

(and the 25% of OOD code is modeled in wrong way)

Metrics Engine is interesting example, while we can say it's proper OOD-modeling, it doesn't use NestJS DI

Circular refs in services

We wanna some new code

We write new function

We push that function into service class

Circular refs in services

"Oh, new function doing smth with people, that should be a PeopleService"

— my mental process

there is no model
behind this process

Circular refs in services

WHYYYYYYYY

Because it's too hard to write a separate service with a single function

 

Because you need to import new service not only into callee but also provide it in feature module (or app module)

and because we don't have time to invest into proper design - we are a startup after all

Circular refs in services

Let's fix this

trying to achieve functions with DI

and without too much boilerplate

not trying to add any new model
we won't adopt that easily
thus why bother

Circular refs in services

Let's fix this

Oh wait, Atomic Services already almost fix that!

Circular refs in services

Let's fix this

@AtomicService({
  imports: [
    AppConfigModule
  ]
})
class SomeFn {
  constructor(
  	private readonly appConfig: AppConfigService
  ) {}
  
  async handle(...) {
    ...
  }
}

Circular refs in services

Let's fix this





@AtomicService()
class SomeFn {
  constructor(
  	private readonly appConfig: AppConfigService
  ) {}
  
  async handle(...) {
    ...
  }
}

AppConfigModule (and some other basic modules) are @Global() now

No need to import it

Circular refs in services

Let's fix this





@AtomicService()
class SomeFn {
  constructor(
  	private readonly appConfig: AppConfigService
  ) {}
  
  async handle(...) {
    ...
  }
}

AppConfigModule (and some other basic modules) are @Global() now

No need to import it

For sure:

  • AppConfigModule
  • DbModule
  • FlagsModule
  • RedisModule

Circular refs in services

Let's fix this





@AtomicService()
class SomeFn {
  constructor(
  	private readonly appConfig: AppConfigService
  ) {}
  
  async handle(...) {
    ...
  }
}

AppConfigModule (and some other basic modules) are @Global() now

No need to import it

Maybe:

  • PeopleModule
  • GroupsModule
  • AssetsModule
  • MetricsModule
  • IntegrationsModule ??

For sure:

  • AppConfigModule
  • DbModule
  • FlagsModule
  • RedisModule

Circular refs in services

Fn per service pattern

@AtomicFn()
class SomeFn {
  constructor(
  	private readonly appConfig: AppConfigService
  ) {}
  
  async handle(...) {
    ...
  }
    
  private someHelper(...) {
  }
}

Simple directive, easy to use

← DI always DI

 Small amount of deps - cuz fns are small

← single public method

(convention, maybe eslint rule?)

← helpers are private

Circular refs in services

Fn per service pattern

It's kinda hard to introduce new services because you need to manually name files:
SurveysService -> surveys.service.ts.

 

Let's just name it as SurveysService.ts

One more trouble

VS Code generates name SurveysService.ts

when using "Move to a new file"

Circular refs in services

Fn per service pattern

When?

Old code:

  • Almost never cuz we have a bridge between worlds
  • But might refactor some existing circular refs (or just use @Global)

Circular refs in services

Fn per service pattern

When?

New code:

  • ~Always in new code if there is no significant reason (like actual OOD)

still can be a little pain in some cases

 

Old code:

  • Almost never cuz we have a bridge between worlds
  • But might refactor some existing circular refs (or just use @Global)

Circular refs in services

Fn per service pattern

When?

New code:

  • ~Always in new code if there is no significant reason (like actual OOD)
  • When there is circular ref or there are too much lines in file / methods in file

still can be a little pain in some cases

 

 

need to have some new eslint rule or smth

Old code:

  • Almost never cuz we have a bridge between worlds
  • But might refactor some existing circular refs (or just use @Global)

I really prefer this

 

Circular refs in services

Fn per service pattern

Why?

  • people.service.ts - 921 lines / 17 methods
  • members.service.ts - 1100 lines / 19 methods
  • integrations.service.ts - 1038 lines / 26 methods
  • surveys.service.ts - 1182 lines / 15 methods
  • (and circular refs ofc)

How soon we will reach 5k LoC per file?

it just becomes hard to navigate

my childhood trauma was being locked in project with 10k LoC files

Circular refs in services

Fn per service pattern

Why not nestjs/cqrs?

  • manual imports everywhere (much more of boilerplate)
  • a lot of other boilerplate (like repetition of explicit types)
  • (special hate for rxjs sagas)
  • universal bus that may fail in runtime

Circular refs in services

Fn per service pattern

Why not use Query/Command separation?

I like the approach if that's just a naming convention.

Not sure if we are ready to embed anything more powerful

(I have experience building that, I can build smth like that, but that just would take a time)

Circular refs in services

Fn per service pattern

Pros

  • Static
  • Simple
  • Can be pain in controllers

Cons

 

we can introduce some AtomicApiService
that can be imported only from controllers 
so we can have strict separation guarantees
(not sure about this idea)

Circular refs

By Viktor Lova