Skip to content
CMO & CTO
CMO & CTO

Closing the Bridge Between Marketing and Technology, By Luis Fernandez

  • Digital Experience
    • Experience Strategy
    • Experience-Driven Commerce
    • Multi-Channel Experience
    • Personalization & Targeting
    • SEO & Performance
    • User Journey & Behavior
  • Marketing Technologies
    • Analytics & Measurement
    • Content Management Systems
    • Customer Data Platforms
    • Digital Asset Management
    • Marketing Automation
    • MarTech Stack & Strategy
    • Technology Buying & ROI
  • Software Engineering
    • Software Engineering
    • Software Architecture
    • General Software
    • Development Practices
    • Productivity & Workflow
    • Code
    • Engineering Management
    • Business of Software
    • Code
    • Digital Transformation
    • Systems Thinking
    • Technical Implementation
  • About
CMO & CTO

Closing the Bridge Between Marketing and Technology, By Luis Fernandez

Cleaner APIs with Streams

Posted on August 22, 2015 By Luis Fernandez

I keep coming back to the same thought when I sketch APIs on a napkin at a cafe: streams make code cleaner.

We ship more JSON across the wire every week. We smash more collections into memory than we want to admit. The more I build services, the more I see the same pattern. When an API speaks in streams, the composition reads like a sentence, memory stays calm, and the intent gets obvious. Java 8 gave us Streams last year and they are finally showing up in real projects. Node lives on streams. Reactive Streams got its first spec. io dot js is merging back to Node under a foundation. The air smells like pipelines.

When we say stream I mean a sequence that can be processed as it arrives, not after it all lands. That tiny shift changes a lot. You can return a stream instead of a list. You can push items over time. You can add backpressure so producers do not drown consumers. And your API becomes a small set of pipes that people wire together without drama.

Let me ground this with code. First in Java, since many teams are now on Java 8 in production or at least testing it in staging. The usual method returns a List. It looks simple until that list grows and spills memory. Try a Stream instead.

// before
public List<Order> findRecentOrders(User user) {
    return orderRepository.findByUser(user).stream()
            .filter(o -> o.getCreatedAt().isAfter(cutoff()))
            .sorted(Comparator.comparing(Order::getCreatedAt).reversed())
            .collect(Collectors.toList());
}

// after
public Stream<Order> streamRecentOrders(User user) {
    return orderRepository.streamByUser(user)
            .filter(o -> o.getCreatedAt().isAfter(cutoff()))
            .sorted(Comparator.comparing(Order::getCreatedAt).reversed());
}

// usage
try (Stream<Order> s = service.streamRecentOrders(user)) {
    BigDecimal total = s
        .map(Order::total)
        .reduce(BigDecimal.ZERO, BigDecimal::add);
}

Notice the story. The method name starts with stream. The call site reads left to right. The terminal operation decides when to pull. The try with resources makes the lifetime clear if you tie the stream to a cursor. Your API exposes a verb that is natural to chain. Your users can filter, map, reduce, group, or collect into a paged response if they want. You did not force an eager list. You gave a pipeline.

Now jump to Node. Streams are not new here, but we still see modules that throw arrays around. If your API already works with a Readable stream, keep it in that shape. If it does not, it probably wants to. It sets you up for backpressure and it plugs into everything with pipe.

// create a Transform stream that scrubs and flattens lines of JSON
var through = require('through2');

function parseJsonLines() {
  return through.obj(function (chunk, enc, cb) {
    var lines = chunk.toString('utf8').split('\n');
    for (var i = 0; i < lines.length; i++) {
      if (!lines[i]) continue;
      try {
        this.push(JSON.parse(lines[i]));
      } catch (e) {
        // decide whether to emit error or skip
      }
    }
    cb();
  });
}

// API: return a readable stream instead of an array
function streamUsers(req) {
  var source = fetchFromDbAsStream(req.query);
  return source.pipe(parseJsonLines());
}

// caller
streamUsers({ query: {} })
  .on('data', function (user) { /* handle user */ })
  .on('error', function (e) { /* handle error */ })
  .on('end', function () { /* done */ });

Your code did not need a giant array. It gave a readable stream that callers can pipe into gzip, a file, or an HTTP response. This is the Node sweet spot. The shape of the API matches the runtime. You can push a million users through without sweating a heap spike. And your intent is plain English. A stream of users flows through a parser and out to the consumer.

When you need async composition across process boundaries, Rx fits. RxJava, RxJS, and RxNet are everywhere right now. If you need a stream that can complete, error, or stay alive, an Observable is a solid way to describe that contract in your API. The key is to keep your surfaces simple. Name things for what they do and return an Observable of your domain types.

// RxJava style API
public Observable<Event> eventsFor(String userId) {
    return eventStore.tail(userId)
        .filter(e -> e.seen() == false)
        .onBackpressureBuffer(10_000)
        .observeOn(Schedulers.io());
}

// caller
Subscription sub = eventsFor("u123")
    .map(this::enrich)
    .subscribe(
        e -> log.info("event {}", e.id()),
        err -> log.error("boom", err),
        () -> log.info("stream done")
    );

There is a gotcha. Streams look clean on paper, then they meet I O, slow clients, and flaky networks. That is where backpressure and cancellation matter. Reactive Streams gave us a tiny protocol for that game. Akka, Reactor, RxJava, and others are playing along. On the Node side, pause and resume already exist at the stream level. In Java 8 you may need to wire your own flow control around database cursors or use libraries that bridge to Reactive Streams. The idea is boring and powerful. The consumer tells the producer how much it can handle. Your API should make that easy.

Some practical tips for cleaner APIs with streams:

  • Return a stream type by default when the result can be large or open ended. Stream in Java, Observable in Rx, Readable in Node, Iterator in Python if you must keep it simple.
  • Use names that say what flows. streamOrders, events, userLines. People should guess the type from the name without reading the doc.
  • Make streaming the first class path. Do not add a boolean to switch between list and stream. Offer two methods or pick the stream only.
  • Keep items small and plain. Push domain objects, not framework blobs. That keeps test code human friendly.
  • Let callers choose the terminal operation. In Java that means do not collect inside your method unless you really must.
  • Think about time. Many streams are infinite. Let callers cancel easily. In Rx that is a subscription. In Node that is .pause or .unpipe. In Java expose Closeable where it makes sense.
  • Document backpressure behavior in one sentence. Does the stream buffer, drop, or sizzle when a consumer slows down.

Streaming also changes how we ship data over HTTP. With chunked transfer you can drip JSON lines while you work. The client starts processing right away. You reduce timeout pain and you do not need a heavy pagination story for long running jobs. Keep the payload simple. One object per line is friendly in many languages.

// server
res.writeHead(200, { 'Content-Type': 'application/json', 'Transfer-Encoding': 'chunked' });
streamUsers(q).on('data', function (user) {
  res.write(JSON.stringify(user) + '\n');
}).on('end', function () {
  res.end();
});

// client pseudo code
http.get(url).on('data', parseLineByLine).on('end', done);

You can still offer classic pages when needed. A clean streaming API makes that simple. The caller can collect the first one thousand items into a page and move on, while big batch jobs can drink from the same faucet until the end. The surface area stays small.

Tooling is ready enough right now. Java Streams live in the JDK. RxJS and RxJava are a one line install. Node streams are everywhere from request to fs to zlib. Reactive Streams gives you a tiny vocabulary to keep producers and consumers honest across library lines. With the Node foundation work moving and the ES6 story getting real through Babel and engine updates, stream friendly code is not a gamble. It is a simple way to make code tell the truth.

One last thought on readability. A good stream based API should read like a short paragraph. Where is the data from. What happens to it. Where does it go. If your chain answers those three in order, you are on the right track. If your call sites bounce between callbacks and nested loops and temporary variables, step back and give the stream a chance to carry the load.

I am not saying everything should be a stream. Some things are small and done in a blink. Return a value and be happy. For the rest, the kind that grow over time or size, design your API so it can breathe. It will pay off the next time traffic spikes, a report runs forever, or a consumer hangs longer than you like.

Cleaner APIs with streams is not a trend piece. It is a field note from code that felt nicer to read and easier to ship. Give your callers a clear path and get out of the way. Pipes do the rest.


Got a stream pattern that made your API easier to read or test. Send it my way, or paste a gist and ping me.

Code Development Practices Software Engineering coding-practicesjavaspring

Post navigation

Previous post
Next post
  • Digital Experience (94)
    • Experience Strategy (19)
    • Experience-Driven Commerce (5)
    • Multi-Channel Experience (9)
    • Personalization & Targeting (21)
    • SEO & Performance (10)
  • Marketing Technologies (92)
    • Analytics & Measurement (14)
    • Content Management Systems (45)
    • Customer Data Platforms (4)
    • Digital Asset Management (8)
    • Marketing Automation (6)
    • MarTech Stack & Strategy (10)
    • Technology Buying & ROI (3)
  • Software Engineering (310)
    • Business of Software (20)
    • Code (30)
    • Development Practices (52)
    • Digital Transformation (21)
    • Engineering Management (25)
    • General Software (82)
    • Productivity & Workflow (30)
    • Software Architecture (85)
    • Technical Implementation (23)
  • 2025 (12)
  • 2024 (8)
  • 2023 (18)
  • 2022 (13)
  • 2021 (3)
  • 2020 (8)
  • 2019 (8)
  • 2018 (23)
  • 2017 (17)
  • 2016 (40)
  • 2015 (37)
  • 2014 (25)
  • 2013 (28)
  • 2012 (24)
  • 2011 (30)
  • 2010 (42)
  • 2009 (25)
  • 2008 (13)
  • 2007 (33)
  • 2006 (26)

Ab Testing Adobe Adobe Analytics Adobe Target AEM agile-methodologies Analytics architecture-patterns CDP CMS coding-practices content-marketing Content Supply Chain Conversion Optimization Core Web Vitals customer-education Customer Data Platform Customer Experience Customer Journey DAM Data Layer Data Unification documentation DXP Individualization java Martech metrics mobile-development Mobile First Multichannel Omnichannel Personalization product-strategy project-management Responsive Design Search Engine Optimization Segmentation seo spring Targeting Tracking user-experience User Journey web-development

©2025 CMO & CTO | WordPress Theme by SuperbThemes