Synchronous architecture with asynchronous repository
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
A synchronous application layer can call an asynchronous repository, but the design only stays clean if the asynchronous boundary is handled deliberately. In most cases, once persistence is asynchronous, the surrounding application services and request handlers should also become asynchronous up to the nearest natural boundary.
Why Repositories Become Asynchronous
Repositories are often asynchronous because data access is I/O-bound. Database queries, network calls, and storage APIs spend most of their time waiting, so an asynchronous API lets the runtime use threads more efficiently.
A repository interface in C# might look like this:
That interface reflects the real nature of the work: the result is not available immediately.
What Happens if the Upper Layer Stays Synchronous
A purely synchronous service sitting on top of this repository has only a few options:
- block on the asynchronous call
- restructure itself to become asynchronous
- move the async work behind a queue or background boundary
Blocking is the easiest to write and usually the worst long-term choice. It can waste threads, increase latency under load, and create deadlock risks in some environments.
This is the anti-pattern:
The code compiles, but it throws away most of the benefit of having an asynchronous repository in the first place.
A Better Split: Synchronous Domain, Asynchronous Application Boundary
The clean compromise is to keep pure domain logic synchronous while making the application service asynchronous.
The repository remains async because it does I/O. The domain mapping stays ordinary and synchronous inside the method body.
This separation works well because only the I/O boundary is async. The core business rules do not need fake asynchrony.
Propagate Async to the Request Boundary
Once the service becomes async, the controller or handler should usually be async too.
This is usually the right architectural shape for web applications, APIs, and services that already run on async-friendly frameworks.
When a Synchronous Upper Layer Can Still Make Sense
There are cases where the top layer stays synchronous for legitimate reasons:
- a command-line tool with simple sequential flow
- a legacy application framework with synchronous extension points
- a boundary that intentionally delegates async work to a background queue
In those cases, you may block once at the outer edge, but it should be a conscious boundary decision, not something repeated throughout the codebase.
The key question is whether the synchronous layer is a true boundary or just accidental inertia.
Design the Whole Call Chain Consistently
Mixed sync and async code becomes painful when the design is inconsistent. A good rule is:
- asynchronous for I/O and workflows that depend on I/O
- synchronous for pure computation and domain rules
That keeps the code honest. Methods are async because they wait, not because the team standardized on a suffix.
Common Pitfalls
A common mistake is blocking on every repository call with .Result or .GetAwaiter().GetResult(). That turns async code back into sync code with extra risk.
Another mistake is making every domain method asynchronous even when it performs no waiting. That adds ceremony without architectural benefit.
Teams also underestimate how far async often needs to propagate. If persistence is async in a request-driven system, controllers and handlers usually need to be async too.
Finally, avoid hiding asynchronous work behind a synchronous facade unless that facade is a deliberate, well-understood boundary.
Summary
- An asynchronous repository usually implies asynchronous application services and handlers.
- Keep pure business logic synchronous inside those async methods.
- Blocking on async repository calls should be a rare boundary decision, not a normal pattern.
- Mixed sync and async architecture is workable only when the boundaries are explicit.
- The right design is usually “async for I/O, sync for pure computation.”

