Concurrency Patterns

The CSV Import That Brought Down Production

It was a Wednesday morning when our support team reported: "Customer uploads are timing out."

We had a feature that let customers upload CSV files to import product data. A typical file had 1,000 products. Each product needed:

  1. Validation (check required fields, format)

  2. Image download from URL (if provided)

  3. Price calculation (apply tax, discounts)

  4. Database insertion

The naive implementation:

def import_products(csv_file):
    products = parse_csv(csv_file)  # 1000 products
    
    for product in products:
        # Sequential processing
        validate_product(product)         # ~10ms
        image = download_image(product)   # ~200ms
        price = calculate_price(product)  # ~5ms
        save_to_database(product)         # ~15ms
        
    return f"Imported {len(products)} products"

The math: 1,000 products × 230ms = 230 seconds (3 minutes 50 seconds)

One customer uploaded a file with 10,000 products. The request timed out after 30 seconds. They tried again. And again. Each attempt spawned a worker process that kept running. Within an hour, our server ran out of memory.

I spent that afternoon learning Go's concurrency patterns and rewrote the import system:

Result: 10,000 products imported in 12 seconds. No memory issues. No timeouts.

This article covers the concurrency patterns that made that possible.


Worker Pool Pattern

The most common pattern - a fixed number of workers processing jobs from a queue.

Basic Worker Pool

Real Example: Image Processor


Fan-Out/Fan-In Pattern

Fan-out: Distribute work across multiple goroutines Fan-in: Combine results from multiple goroutines

Real Example: Log Aggregator


Pipeline Pattern

Chain multiple processing stages together.

Real Example: Data Processing Pipeline


Context Package

The context package provides cancellation, timeouts, and request-scoped values.

Context Cancellation

Context Timeout

Context Values

Real Example: HTTP Request with Context


Sync Package Primitives

WaitGroup

Wait for a collection of goroutines to finish:

Mutex (Mutual Exclusion)

Protect shared data:

RWMutex (Read-Write Mutex)

Allow multiple readers or one writer:

sync.Once

Execute code exactly once:


Real Example: Complete Import System

Putting it all together - the CSV import system I built:


Your Challenge

Build a concurrent URL health checker:


Key Takeaways

  1. Worker pools: Fixed number of workers process jobs from queue

  2. Fan-out/fan-in: Distribute work, merge results

  3. Pipelines: Chain processing stages for clean data flow

  4. Context: Handle cancellation, timeouts, request-scoped values

  5. WaitGroup: Wait for multiple goroutines to complete

  6. Mutex: Protect shared data from concurrent access

  7. RWMutex: Allow multiple readers or exclusive writer

  8. sync.Once: Initialize code exactly once


What I Learned

That CSV import rewrite taught me that Go's concurrency patterns aren't just about speed:

  • Pipeline pattern made the code readable - clear data flow

  • Worker pools provided predictable resource usage - no memory explosions

  • Context enabled graceful cancellation - no orphaned goroutines

  • 12-second imports vs. 230-second sequential processing

Coming from Python's multiprocessing complexity, Go's patterns felt elegant. The import system has processed millions of products over 18 months with zero concurrency bugs.

The 20x speedup was impressive. The zero downtime was better.


Next: Package Management

In the next article, we'll explore Go modules and dependency management. You'll learn how I escaped dependency hell and why go.mod changed how I think about package management.

Last updated