Scenario and definition
A Hospital Management System's billing module needs a PaymentGatewayClient that should be swappable between a real payment processor in production and a mock implementation during integration tests, without any code in the billing service itself changing. Node.js (NestJS) achieves this through a decorator-based, reflection-driven DI container conceptually borrowed from Angular and Spring. Spring Boot achieves the same outcome through its mature, annotation-driven IoC container.
Keywords
Code comparison
export const PAYMENT_GATEWAY = 'PAYMENT_GATEWAY';
@Injectable()
export class StripeGatewayClient implements PaymentGatewayClient { /* ... */ }
@Injectable()
export class MockGatewayClient implements PaymentGatewayClient { /* ... */ }
@Module({
providers: [
{
provide: PAYMENT_GATEWAY,
useClass: process.env.NODE_ENV === 'test' ? MockGatewayClient : StripeGatewayClient
}
]
})
export class BillingModule {}
@Injectable()
export class BillingService {
constructor(@Inject(PAYMENT_GATEWAY) private gateway: PaymentGatewayClient) {}
}public interface PaymentGatewayClient { /* ... */ }
@Service
@Profile("!test")
public class StripeGatewayClient implements PaymentGatewayClient { /* ... */ }
@Service
@Profile("test")
public class MockGatewayClient implements PaymentGatewayClient { /* ... */ }
@Service
public class BillingService {
private final PaymentGatewayClient gateway;
public BillingService(PaymentGatewayClient gateway) {
this.gateway = gateway;
}
}Code explanation
node
Because TypeScript interfaces don't exist at runtime (erased during compilation), NestJS can't use the interface type itself as an injection token — hence the explicit string token PAYMENT_GATEWAY and the @Inject(PAYMENT_GATEWAY) decorator. The useClass conditional in the provider definition switches the concrete implementation based on environment at module-definition time.
springboot
Java interfaces DO exist at runtime via reflection, so BillingService's constructor can simply declare a dependency on the PaymentGatewayClient interface type directly, with Spring resolving which concrete @Service implementation to inject — no string token workaround needed. @Profile("test") and @Profile("!test") declaratively scope which implementation bean is active based on Spring's active profile, resolved automatically at startup.
Backend integration
node
The chosen PaymentGatewayClient implementation makes real or mocked HTTP calls to a payment processor's API; because BillingService only depends on the interface, integration tests can swap in MockGatewayClient via NODE_ENV without touching BillingService's code at all.
springboot
Identical integration pattern — StripeGatewayClient makes real API calls while MockGatewayClient returns canned responses; Spring's profile-based activation means running tests with -Dspring.profiles.active=test automatically swaps the implementation with zero code changes.
Interview questions
Why does NestJS need a separate string injection token for interface-based DI, while Spring doesn't?
TypeScript interfaces are a compile-time-only construct, erased entirely from the emitted JavaScript — a string (or Symbol) token is required as a stand-in identity for the dependency. Java interfaces persist at runtime as real reflective types, so Spring's container can use the interface type itself as the resolution key, scanning the classpath for any @Service implementing that interface.
How does Spring's @Profile-based bean activation compare to NestJS's conditional useClass in terms of maintainability as the number of environments grows?
@Profile scales more cleanly to many environments because each implementation declares its own profile membership independently, and activating a profile is a single externalized configuration flag with no code changes ever required. The NestJS useClass ternary hardcodes environment-branching logic directly inside the module's provider definition, becoming increasingly unwieldy as environments multiply.