Functional Programming Part 4 - Implementing Coupon Newsletter Code

Implementing Coupon Newsletter Code

November 17, 2025

The previous article focused on practicing how to distinguish "actions," "calculations," and "data." You can refer to this article Functional Programming Part 2 - Distinguishing Actions, Calculations, and Data, Today, I'll implement the coupon newsletter code. Before that, let's review the practical scenario from the previous article.

Reading Subscriber Data from Database and Determining Coupon Rank, Then Categorizing Coupon List

In this process, there is one "calculation" and one "data":

The subscriber data is an array, where each element in the array is an object. This object contains the subscriber's email and referral count.

const subscribers = [
  {
    email: 'alice@example.com',
    re_count: 3
  },
  {
    email: 'bob@test.com',
    re_count: 0
  },
  {
    email: 'carol@demo.org',
    re_count: 7
  },
  {
    email: 'david@sample.net',
    re_count: 12
  }
]

The above has already "read out" all the necessary data. Next, we need to calculate which subscribers should receive which rank of coupon codes based on this data. In the previous article, we mentioned the condition - "For every 10 friends referred, both the referrer and the friend will receive better coupon codes." Therefore, for the calculation in this process, we can first derive the calculation formula:

// Referral count >= 10 is best
// Referral count < 10 is good
 
const getSubscriberRank = subscribers.map(sub => {
  if(sub.re_count >= 10) {
    return 'best';
  }
  return 'good';
})

Finding Given Coupon Codes from the Coupon List in the Database

This process has one "data" and one "calculation":

The coupon data is like an object, where this object contains the coupon code and rank. Here, we'll simply list three pieces of data:

const coupons = [
  {
    code: 'WELCOME10',
    rank: 'bad'
  },
  {
    code: 'FRIEND20',
    rank: 'good'
  },
  {
    code: 'VIP30',
    rank: 'good'
  },
]

For this calculation, we need two arguments: one is the coupon list, and one is the rank. Given the rank good, we will get a coupon list that only contains good rank coupons. Conversely, given the rank best, we will get a coupon list that only contains best rank coupons.

const selectCouponsByRank = (coupons, rank) => {
  let couponList = [];
  for (let i = 0; i < coupons.length; i++) {
    let coupon = coupons[i];
    if (coupon.rank === rank) {
      couponList.push(coupon.code)
    }
  }
  return couponList;
}

If you understand advanced JS syntax, you can also simplify it to:

const selectCouponsByRank = (coupons, rank) => {
  // Can add checks for whether coupons is an array and whether rank is a string
  if (!Array.isArray(coupons)) {
    return [];
  }
 
  if (typeof rank !== 'string' || !['best', 'good', 'bad'].includes(rank)) {
    return [];
  }
 
  return coupons.filter(coupon => coupon.rank === rank).map(coupon => coupon.code);
}

Determining Email Content for a Single Subscriber

From the above process, we obtained several types of data:

Next, we need to use the first calculation getSubscriberRank to determine the email content that each subscriber should receive. This calculation will need three arguments: one is subscriber information, one is the good coupon list, and one is the best coupon list.

let emailContent = {
  from: 'coupon@example.com',
  to: 'alice@example.com',
  subject: 'Your Weekly Coupon Newsletter',
  body: 'Here are your coupons for this week:',
}
 
// Separate coupon list into good and best ranks
const couponsByRank = {
  good: selectCouponsByRank(coupons, 'good'),
  best: selectCouponsByRank(coupons, 'best'),
}
 
const decideEmailContentForOne = (subscriber) => {
  const rank = getSubscriberRank(subscriber);
 
  return {
    from: 'coupon@example.com',
    to: subscriber.email,
    subject: 'Your Weekly Coupon Newsletter',
    body: `Here are your coupons for this week:\n${couponsByRank[rank].join('\n')}`
  }
}

Determining Email Content for All Subscribers

const decideEmailContentForAll = (subscribers, couponsByRank) => {
  return subscribers.map(subscriber => decideEmailContent(subscriber, couponsByRank));
}

Combining into a "Send Newsletter" Action

const sendAllEmails = () => {
  // Fetch coupons from DB, this wasn't mentioned above, but in practice, it needs to be read from the database
  const coupons = fetchCouponsFromDB();
  const couponsByRank = {
    good: selectCouponsByRank(coupons, 'good'),
    best: selectCouponsByRank(coupons, 'best'),
  }
  // Fetch subscribers from DB, this wasn't mentioned above, but in practice, it needs to be read from the database
  const subscribers = fetchSubscribersFromDB();
  const emailContent = decideEmailContentForAll(subscribers, couponsByRank);
  emailContent.forEach(email => {
    sendEmail(email);
  })
}

Optimization: Refactoring into Pure Functions

In the above sendAllEmails function, there are two places that don't meet the definition of a pure function - namely, the parts that read from the DB. In functional programming, reading from a database has side effects (I/O operations), so we need to separate these two parts.

const fetchCouponsFromDB = async () => {
  const response = await fetch('https://api.example.com/coupons');
  return await response.json();
}
 
const fetchSubscribersFromDB = async () => {
  const response = await fetch('https://api.example.com/subscribers');
  return await response.json();
}

Complete Code

// Database reading (I/O operation)
const fetchCouponsFromDB = async () => {
  const response = await fetch('https://api.example.com/coupons');
  return await response.json();
}
 
const fetchSubscribersFromDB = async () => {
  const response = await fetch('https://api.example.com/subscribers');
  return await response.json();
}
 
// Core calculations
const getSubscriberRank = (subscriber) => {
  if(subscriber.re_count >= 10) {
    return 'best';
  }
  return 'good';
}
 
const selectCouponsByRank = (coupons, rank) => {
  return coupons.filter(coupon => coupon.rank === rank).map(coupon => coupon.code);
}
 
const decideEmailContentForOne = (subscriber, couponsByRank) => {
  const rank = getSubscriberRank(subscriber);
  return {
    from: 'coupon@example.com',
    to: subscriber.email,
    subject: 'Your Weekly Coupon Newsletter',
    body: `Here are your coupons for this week:\n${couponsByRank[rank].join('\n')}`
  }
}
 
const decideEmailContentForAll = (subscribers, couponsByRank) => {
  return subscribers.map(subscriber => decideEmailContentForOne(subscriber, couponsByRank));
}
 
// Application layer
const sendAllEmails = async () => {
  const coupons = await fetchCouponsFromDB();
  const subscribers = await fetchSubscribersFromDB();
  
  const couponsByRank = {
    good: selectCouponsByRank(coupons, 'good'),
    best: selectCouponsByRank(coupons, 'best'),
  };
 
  const allEmails = decideEmailContentForAll(subscribers, couponsByRank);
 
  allEmails.forEach(email => {
    sendEmailToUser(email);
  });
}

Summary

When first starting to learn functional programming, it might seem like we're just splitting out many functions, giving me a feeling of having to manage many functions. This might just be because I haven't yet mastered which ones to split and which ones not to split. By asking myself the following questions, I can help myself decide whether to split.

  1. Is this logic "reused"?
  2. Does this logic have a "clear single purpose"?
  3. Can the function be understood through "composition"?
  4. Does this logic need to be tested separately?
  5. If not split, will it become a giant function?

The goal of FP is to separate "pure logic" and "side effects":

Pure logic

Side effects

Back to Blog 🏃🏽‍♀️