How can I process most jobs in parallel but serialize a subset?
Master System Design with Codemia
Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.
Introduction
A common concurrency requirement is: run most jobs in parallel, but ensure that certain jobs never overlap because they touch the same resource. The clean solution is usually not "make everything sequential," but to route only the conflicting subset through a serialized lane while keeping the rest on a parallel executor.
Identify What Actually Needs Serialization
The first design step is to define the conflict rule precisely.
Examples:
- jobs for the same customer must run one at a time
- jobs that write to the same file must be serialized
- jobs of type
RebuildIndexmust never overlap
Once you know the rule, you can stop treating all work as if it had the same concurrency requirement.
The Core Pattern: Parallel Pool Plus Serial Lane
One simple architecture uses:
- a normal thread pool for independent jobs
- a single-thread executor for the serialized subset
A Java sketch:
This is appropriate when all serialized jobs belong to one global category.
When Serialization Depends on a Key
Often the rule is more specific: jobs with the same key must be serialized, but different keys can still run in parallel.
Examples:
- one lane per customer id
- one lane per account
- one lane per document id
In that case, use keyed serialization instead of one global serial queue.
With this approach:
- jobs for key
Arun one at a time - jobs for key
Brun one at a time - jobs for
AandBcan still overlap with each other
That usually gives much better throughput than pushing every constrained job through one global bottleneck.
Avoid Locking the Whole Job Body Blindly
Another tempting approach is to run everything on the same parallel pool and put a global lock around the dangerous section. That can work for small cases, but it is often harder to reason about because:
- queueing becomes implicit
- lock contention is harder to observe
- long jobs may hold the lock longer than intended
Routing conflicting work through explicit serialized executors usually makes the concurrency policy clearer.
Ordering Guarantees Matter
When you serialize a subset, ask whether order matters too.
A single-thread executor gives both:
- no overlap
- FIFO execution order for submitted tasks
If all you need is mutual exclusion but not ordering, a semaphore or keyed lock may be enough. If you need predictable order for a resource, an explicit serial queue is often the better fit.
Watch Resource Lifecycle
If you create one single-thread executor per key, you also need a plan for cleanup. In systems with many transient keys, an unbounded map of executors can become its own resource leak.
Practical strategies include:
- fixed lane striping by hash bucket
- expiring idle lanes
- using a custom keyed serial executor abstraction instead of raw executor creation per key
The right approach depends on how many distinct keys your workload can generate.
Common Pitfalls
The biggest mistake is serializing too much work. If only a narrow subset conflicts, do not turn the whole pipeline into a single-file line.
Another mistake is using one global serial executor when the real requirement is keyed serialization. That destroys throughput unnecessarily.
People also forget that serialization policy should be part of job routing, not hidden inside random synchronized blocks across the codebase.
Finally, if ordering matters, do not assume a lock gives it to you. A lock prevents overlap, but it does not automatically define a stable queueing policy the way a serial executor does.
Summary
- Keep independent jobs on a parallel executor.
- Route only conflicting jobs through a serialized lane.
- If the constraint is per key, use keyed serialization rather than one global bottleneck.
- Prefer explicit scheduling policy over scattered locking when possible.
- Plan for lifecycle and cleanup if serialized lanes are created dynamically.

