Stream-based Dependency Injection
In this lesson, you’ll learn how to inject stream-based services into your application components to achieve a decoupled, reactive architecture.
Decoupling Logic via Streams
Standard Dependency Injection (DI) usually involves passing objects or classes into constructors. However, when building high-throughput systems, passing static data structures can create bottlenecks. Instead, we can inject streams. By treating your dependencies as Readable or Writable interfaces, you decouple the "what" (data processing) from the "how" (data source/destination).
When a component expects a stream rather than a concrete implementation, it doesn't need to know if the data comes from a network socket, a file watcher, or a mock generator. It only needs to know how to handle the stream’s flow and backpressure.
Implementing the Pattern
Imagine a DataProcessor service that needs an input source. Instead of giving it a file path, you inject an already-initialized Readable stream:
class DataProcessor {
constructor(inputStream) {
this.inputStream = inputStream;
}
start() {
this.inputStream.on('data', (chunk) => {
// Process chunks as they arrive
console.log(`Processing: ${chunk.length} bytes`);
});
}
}
// Injection
const sourceStream = getNetworkStream(); // Our dependency
const processor = new DataProcessor(sourceStream);
processor.start();
By providing the stream as a dependency, the DataProcessor remains completely unaware of the underlying connection logic. This makes unit testing trivial—you can inject a PassThrough stream and manually push data into it to verify your processing logic without opening real network sockets.
Why This Matters
This pattern shines when combined with backpressure. Because the dependency is just a stream, your DataProcessor can signal the provider to pause if it is overwhelmed, simply by calling inputStream.pause(). The consumer controls the flow, and the dependency respects that constraint. This modular approach allows you to swap out your data sources—moving from a local file to a Kafka stream, for example—without changing a single line of code inside your processor.
Key Takeaway: Injecting stream interfaces into your components allows you to decouple data sources from processing logic while natively inheriting flow control and backpressure management.