DEV Community

pepnova
pepnova

Posted on

SMS Delivery Pipeline Project using Go #1

gopher

My first post on DEV! As I began this project, I wanted to find a place where I could gather feedback and connect with others, which brought me to this platform. Iโ€™d be happy if this project helps someone, and I look forward to all the learning opportunities that come from interacting with you all.

I previously worked on developing an SMS Delivery Platform, a project filled with common challenges in backend and distributed systems, including:

  • High throughput
  • Low latency
  • Fault tolerance
  • High concurrency
  • Tenant fairness
  • Strict rate limiting
  • Just-in-time processing and more

Initially, I approached this project with questions such as, "๐™ƒ๐™ค๐™ฌ ๐™›๐™–๐™จ๐™ฉ ๐™˜๐™ค๐™ช๐™ก๐™™ ๐™„ ๐™™๐™š๐™ซ๐™š๐™ก๐™ค๐™ฅ ๐™ฉ๐™๐™ž๐™จ ๐™ช๐™จ๐™ž๐™ฃ๐™œ ๐™˜๐™ช๐™ง๐™ง๐™š๐™ฃ๐™ฉ ๐˜ผ๐™„ ๐˜พ๐™ค๐™™๐™ž๐™ฃ๐™œ ๐˜ผ๐™œ๐™š๐™ฃ๐™ฉ๐™จ?" and "๐˜ผ๐™ฉ ๐™ฌ๐™๐™–๐™ฉ ๐™จ๐™˜๐™–๐™ก๐™š ๐™ฌ๐™ค๐™ช๐™ก๐™™ '๐™ซ๐™ž๐™–๐™—๐™ก๐™š ๐™˜๐™ค๐™™๐™ž๐™ฃ๐™œ'โ€”๐™ง๐™š๐™ก๐™ฎ๐™ž๐™ฃ๐™œ ๐™š๐™ฃ๐™ฉ๐™ž๐™ง๐™š๐™ก๐™ฎ ๐™ค๐™ฃ ๐™–๐™œ๐™š๐™ฃ๐™ฉ๐™จโ€”๐™—๐™š๐™˜๐™ค๐™ข๐™š ๐™ž๐™ข๐™ฅ๐™ค๐™จ๐™จ๐™ž๐™—๐™ก๐™š?" However, I soon realized that these questions were less meaningful, as the answers depend heavily on the specifications and how much of the business requirements are reproduced.

As I continued developing, I began to reflect on my earlier decisions: "๐™’๐™๐™ฎ ๐™™๐™ž๐™™ ๐™„ ๐™˜๐™๐™ค๐™ค๐™จ๐™š ๐™ฉ๐™๐™ž๐™จ ๐™จ๐™ฅ๐™š๐™˜๐™ž๐™›๐™ž๐™˜ ๐™™๐™š๐™จ๐™ž๐™œ๐™ฃ ๐™—๐™–๐™˜๐™  ๐™ฉ๐™๐™š๐™ฃ? ๐™’๐™๐™–๐™ฉ ๐™ฌ๐™š๐™ง๐™š ๐™ฉ๐™๐™š ๐™ฉ๐™ง๐™–๐™™๐™š-๐™ค๐™›๐™›๐™จ?" and "๐™’๐™๐™–๐™ฉ ๐™ฌ๐™ค๐™ช๐™ก๐™™ ๐™„ ๐™™๐™ค ๐™™๐™ž๐™›๐™›๐™š๐™ง๐™š๐™ฃ๐™ฉ๐™ก๐™ฎ ๐™ž๐™› ๐™„ ๐™˜๐™ค๐™ช๐™ก๐™™ ๐™œ๐™ค ๐™—๐™–๐™˜๐™  ๐™ฉ๐™ค ๐™ฉ๐™๐™–๐™ฉ ๐™ฉ๐™ž๐™ข๐™š?" Through this process, I discovered many "better ways" to tackle these challenges today.

The real interest lies in writing the code, considering the design and trade-offs, and addressing challenges step-by-step. To start, I created a minimal and naive Go implementation. This version does not utilize goroutines; itโ€™s a monolith running as a single process that meets only the bare minimum functional requirements. While it cannot satisfy the non-functional requirements of an early-stage product, I believe it provides a clear understanding of the system's functionality.

Moving forward, I plan to evolve this code by setting specific challenges for each iteration and sharing my reflections on the design decisions and trade-offs involved.

// Start begins the background polling loop.
func (w *Worker) Start(ctx context.Context) {
    // poll every 5 seconds check for pending campaigns
    // TODO: what if the worker crashes? what if we have many campaigns?
    ticker := time.NewTicker(5 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            log.Println("Worker context cancelled, stopping...")
            return
        case <-ticker.C:
            w.processNextCampaign(ctx)
        }
    }
}

func (w *Worker) processNextCampaign(ctx context.Context) {
    // Find the next available campaign
    // TOOD: what if company A has 1000 campaigns and company B has 1 campaign? Company A will monopolize the worker.
    campaign, err := w.db.FindPendingCampaign(ctx)
    if err != nil {
        log.Printf("Error checking for pending campaigns: %v\n", err)
        return
    }
    if campaign == nil {
        // No pending campaign found
        return
    }

    log.Printf("Found pending campaign %s, starting processing...\n", campaign.ID)

    // Fetch the CSV file using the storage port
    csvData, err := w.storage.FetchCSV(ctx, campaign.DestinationsFilePath)
    if err != nil {
        // TODO: what if temporary storage is down? or what if the csv file was accidentally deleted?
        log.Printf("Failed to fetch CSV for campaign %s: %v\n", campaign.ID, err)
        return
    }

    // Parse the CSV
    reader := csv.NewReader(bytes.NewReader(csvData))
    // TODO: what if the csv file is huge?
    records, err := reader.ReadAll()
    if err != nil {
        log.Printf("Failed to parse CSV for campaign %s: %v\n", campaign.ID, err)
        return
    }

    // Loop through records, create SMS messages, and send to the Carrier API
    targetMessage := campaign.TemplateBody
    for i, record := range records {
        if len(record) == 0 {
            continue
        }

        // Assuming the first column is the phone number
        phoneNumber := record[0]
        if phoneNumber == "phone_number" && i == 0 {
            // Skip header row if it exists
            continue
        }

        // to make this code readable, I'm not going to replace variables in the template body for now.

        // TODO: yes, CarrerAPI has rate limits. We need to respect that.
        err = w.carrier.SendSMS(ctx, phoneNumber, targetMessage)

        dispatch := &domain.SmsDispatch{
            SMSMessageID: uuid.New(),
            CampaignID:   campaign.ID,
            TenantID:     campaign.TenantID,
            PhoneNumber:  phoneNumber,
            DispatchedAt: time.Now(),
            IsSuccessful: err == nil,
        }

        if err != nil {
            log.Printf("Failed to send SMS to %s: %v\n", phoneNumber, err)
        } else {
            log.Printf("Successfully sent SMS to %s\n", phoneNumber)
        }

        // record the result
        // TODO: what if this worker crashes after sending some SMS of a campaign, but still have pending SMS to send?
        if saveErr := w.db.SaveSmsDispatch(ctx, dispatch); saveErr != nil {
            log.Printf("Failed to save SMS dispatch for %s: %v\n", phoneNumber, saveErr)
        }

        if err != nil {
            continue
        }
    }

    // Mark the campaign as completed in the DB
    err = w.db.MarkCampaignCompleted(ctx, campaign.ID)
    if err != nil {
        log.Printf("Failed to mark campaign %s as completed: %v\n", campaign.ID, err)
    } else {
        log.Printf("Campaign %s processing completed.\n", campaign.ID)
    }
}
Enter fullscreen mode Exit fullscreen mode

๐Ÿ“– Overview

This repository demonstrates the evolutionary system design of an event-driven SMS delivery pipeline.

This project is inspired by a real-world SMS delivery system I previously worked on. I chose this specific domain because it serves as an excellent crucible for tackling the core challenges of modern backend and distributed systems. It naturally demands robust solutions for

  • high throughput
  • low latency
  • fault tolerance
  • high concurrency
  • tenant fairness
  • strict rate limiting
  • just-in-time processing
  • and more

Instead of starting with a complex distributed architecture, this project begins as a minimal, Go monolith. It progressively evolves into a decoupled system to address specific backend challenges as the hypothetical scale grows.

๐ŸŽฏ Core Service Specifications

This platform enables client businesses to reliably manage and deliver SMS communications for various use cases, such as marketing campaigns and appointment reminders. The actual physical delivery to end-user devices is handled by mobile carriers (e.g., Verizon, T-Mobileโ€ฆ




Top comments (0)