Parallel Processing
Job Scheduling
Serialization
Concurrent Computing
Task Management

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 RebuildIndex must 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:

java
1import java.util.concurrent.ExecutorService;
2import java.util.concurrent.Executors;
3
4public class SchedulerExample {
5    private final ExecutorService parallelPool = Executors.newFixedThreadPool(8);
6    private final ExecutorService serialLane = Executors.newSingleThreadExecutor();
7
8    public void submit(Job job) {
9        if (job.mustBeSerialized()) {
10            serialLane.submit(job::run);
11        } else {
12            parallelPool.submit(job::run);
13        }
14    }
15}

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.

java
1import java.util.Map;
2import java.util.concurrent.*;
3
4public class KeyedScheduler {
5    private final ExecutorService parallelPool = Executors.newFixedThreadPool(8);
6    private final Map<String, ExecutorService> serialExecutors = new ConcurrentHashMap<>();
7
8    public void submit(Job job) {
9        if (job.serialKey() == null) {
10            parallelPool.submit(job::run);
11            return;
12        }
13
14        ExecutorService lane = serialExecutors.computeIfAbsent(
15            job.serialKey(),
16            key -> Executors.newSingleThreadExecutor()
17        );
18
19        lane.submit(job::run);
20    }
21}

With this approach:

  • jobs for key A run one at a time
  • jobs for key B run one at a time
  • jobs for A and B can 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.

Course illustration
Course illustration

All Rights Reserved.