What is a good approach to develop a synchronous/blocking and an asynchrounous/non-blocking library-api in parallel? JavaScript
System Design practice on Codemia
Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.
Introduction
If a JavaScript library needs both synchronous and asynchronous APIs, the main goal is to avoid implementing the same business logic twice. The cleanest design is usually a shared pure core plus thin sync and async wrappers only where the underlying runtime actually supports both execution models.
Keep the Core Logic Shared
The easiest way to create maintenance problems is to build one full sync implementation and one full async implementation that gradually drift apart. Instead, isolate parsing, validation, transformation, and domain rules in shared pure functions.
Now the only difference between sync and async entry points is how the text arrives:
That keeps the API surface broader without duplicating the actual library rules.
Do Not Offer Sync APIs for Inherently Async Work
This is the most important design constraint. A synchronous API makes sense only if the platform can really do the work synchronously.
Reasonable sync cases:
- CPU-bound parsing
- in-memory transformations
- Node.js filesystem utilities for scripts or tooling
Bad sync cases:
- browser networking
- server-side operations that would block the event loop badly
- any runtime where the underlying resource is only available asynchronously
In other words, do not invent fake blocking wrappers around promise-based work just for API symmetry. In JavaScript, that usually creates worse ergonomics, not better.
Let Async Be the Primary I/O API
For modern JavaScript libraries, the async API should usually be the main public interface when I/O is involved. Promises compose naturally with the rest of the ecosystem and are the least surprising choice for application code.
If a sync version also exists, it should be limited to cases where a synchronous source really exists, such as local file reads in Node.js.
Keep Naming Explicit
If you publish both styles, name them clearly. Common patterns are:
- '
read()andreadSync()' - '
load()andloadSync()' - '
parse()for pure CPU work andload()for I/O'
This is better than giving the sync and async variants nearly identical names and forcing users to infer behavior from documentation or return types. Clear naming also mirrors the conventions of Node.js core modules, which many JavaScript developers already expect.
Consider Inverting the Design for Better Separation
Another strong approach is to make the library core completely independent of I/O. Then the library exports only pure operations, and the application or adapter layer decides whether data is supplied synchronously or asynchronously.
That can look like this:
Then a Node.js caller can use fs.readFileSync, while another caller can use fs.promises.readFile, and both pass data into the same core function. This design often produces the cleanest long-term architecture because the library never owns blocking versus non-blocking I/O in the first place.
Common Pitfalls
- Writing full sync and async implementations that duplicate the same business logic.
- Exposing a synchronous API for work that the runtime cannot really perform synchronously.
- Making sync and async variants behave slightly differently on validation or error handling.
- Designing API symmetry first and core architecture second.
- Hiding blocking behavior behind a harmless-looking function name.
Summary
- Share the core logic and keep sync versus async differences at the boundary.
- Expose both APIs only when the underlying work genuinely supports both models.
- Prefer async as the primary public API for I/O-heavy JavaScript libraries.
- Use clear names such as
readSync()so blocking behavior is obvious. - When possible, keep the library core pure and let callers decide how data is obtained.
Related reading
- What is a headless service, what does it do/accomplish, and what are some legitimate use cases for it?
- What is a non-blocking Rest Client?
- What is a proxy? What is it in Apache? Does it have many different meanings?
- What is an 'endpoint' in Flask?
- What is a mutex?
- What is a mutex?
- What is a SIMPLE implementation of async.series?
- What is 'Currying'?

System Design Fundamentals
Build a strong foundation in designing scalable, reliable distributed systems.
View the courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
System Design practice on Codemia
Work through 120+ system design problems with detailed solutions, from rate limiters to multi-region storage.