Skip to content

Index

Having Coffee with Deno - Dynamic Names

Welcome to the second installment of our Deno series, where we build a script that pairs up people for coffee.

In the last post, we built a script that helped the Justice League meet up for coffee.

As of right now, our script looks like the following.

index.ts
const names = [
  "Batman",
  "Superman",
  "Green Lantern",
  "Wonder Woman",
  "Static Shock", // one of my favorite DC heroes!
  "The Flash",
  "Aquaman",
  "Martian Manhunter",
];
const pairs = createPairsFrom(shuffle(names));
const message = createMessage(pairs);
console.log(message);

function shuffle<T>(items: T[]): T[] {
  const result = [...items];
  for (let i = result.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * i);
    [result[i], result[j]] = [result[j], result[i]];
  }
  return result;
}
type Pair<T> = { first: T; second: T; third?: T };
function createPairsFrom<T>(items: T[]): Pair<T>[] {
  if (items.length < 2) {
    return [];
  }
  const results = [];
  for (let i = 0; i <= items.length - 2; i += 2) {
    const pair: Pair = { first: items[i], second: items[i + 1] };
    results.push(pair);
  }
  if (items.length % 2 === 1) {
    results[results.length - 1].third = items[items.length - 1];
  }
  return results;
}
function createMessage(pairs: Pair<string>[]): string {
  const mapper = (p: Pair<string>) =>
    `${p.first} meets with ${p.second}${p.third ? ` and ${p.third}` : ""}`;

  return pairs.map(mapper).join("\n");
}

Even though this approach works, the major problem is that every time there's a member change in the Justice League (which seems to happen more often than not), we have to go back and update the list manually.

It'd be better if we could get this list dynamically instead. Given that the League are great developers, they have their own GitHub organization. Let's work on integrating with GitHub's API to get the list of names.

The Approach

To get the list of names from GitHub, we'll need to do the following.

  1. First, we need to figure out which GitHub endpoint will give us the members of the League. This, in turn, will also tell us what permissions we need for our API scope.
  2. Now that we have a secret, we need to update our script to read from an .env file.
  3. Once we have the secret being read, we can create a function to retrieve the members of the League.
  4. Miscellaneous refactoring of the main script to handle a function returning complex types instead of strings.

Justice League Planning

Laying the Foundation

Before we start, we should reactor our current file. It works, but we have a mix of utility functions (shuffle and createPairsFrom) combined with presentation functions (createMessage). Let's go ahead and move shuffle and createPairsFrom to their own module.

utility.ts
type Pair<T> = { first: T; second: T; third?: T };

function shuffle<T>(items: T[]): T[] {
  const result = [...items];
  for (let i = result.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * i);
    [result[i], result[j]] = [result[j], result[i]];
  }
  return result;
}

function createPairsFrom<T>(items: T[]): Pair<T>[] {
  if (items.length < 2) {
    return [];
  }
  const results: Pair<T>[] = [];
  for (let i = 0; i <= items.length - 2; i += 2) {
    const pair: Pair<T> = { first: items[i], second: items[i + 1] };
    results.push(pair);
  }
  if (items.length % 2 === 1) {
    results[results.length - 1].third = items[items.length - 1];
  }
  return results;
}

export { createPairsFrom, shuffle };
export type { Pair };

With these changes, we can update index.ts to be:

index.ts
import { Pair, createPairsFrom, shuffle } from "./module.ts";

const names = [
  "Batman",
  "Superman",
  "Green Lantern",
  "Wonder Woman",
  "Static Shock", // one of my favorite DC heroes!
  "The Flash",
  "Aquaman",
  "Martian Manhunter",
];
const pairs = createPairsFrom(shuffle(names));
const message = createMessage(pairs);
console.log(message);

function createMessage(pairs: Pair<string>[]): string {
  const mapper = (p: Pair<string>) =>
    `${p.first} meets with ${p.second}${p.third ? ` and ${p.third}` : ""}`;

  return pairs.map(mapper).join("\n");
}

Getting GitHub

Now that our code is more tidy, we can focus on figuring out which GitHub endpoint(s) to use to figure out the members of the Justice League.

Taking a look at the docs, we see that there are two different options.

  1. Get members of an Organization
  2. Get members of a Team

What's the difference between the two? In GitHub parlance, an Organization is an overarching entity that consists of members which, in turn, can be part of multiple teams.

Using the Justice League as an example, it's an organization that contains Batman, and Batman can be part of the Justice League Founding Team and a member of the Batfamily Team.

Since we want to pair everyone up in the Justice League, we'll use the Get members of an Organization approach.

Working with Secrets

To interact with the endpoint, we'll need to create an API token for GitHub. Looking over the docs, our token needs to have the read:org scope. We can create this token by following the instructions here about creating a Personal Auth Token (PAT).

Once we have the token, we can invoke the endpoint with cURL or Postman to verify that we can communicate with the endpoint correctly.

After we've verified, we'll need a way to get this API token into our script. Given that this is sensitive information, we absolutely should NOT check this into the source code.

Creating an ENV File

A common way of dealing with that is to use an .env file which doesn't get checked in, but our application can use it during runtime to get secrets.

Let's go ahead and create the .env file and put our API token here.

.env
GITHUB_BEARER_TOKEN="INSERT_TOKEN_HERE"

Our problem now is that if we check git status, we'll see this file listed as a change. We don't want to check this in, so let's add a .gitignore file.

Adding a .gitignore File

With the .env file created, we need to create a .gitignore file, which tells Git not to check in certain files.

Let's go ahead and add the file. You can enter the below, or you can use the Node gitignore file (found here)

.gitignore
.env # ignores all .env files in the root directory

We can validate that we've done this correctly if we run git status and don't see .env showing up anymore as a changed file.

Loading Our Env File

Now that we have the file created, we need to make sure that this file loads at runtime.

In our index.ts file, let's make the following changes.

index.ts
1
2
3
4
5
6
7
8
9
import { config as loadEnv } from "https://deno.land/x/dotenv@v3.2.2/mod.ts";
// other imports

// This loads the .env file and adds them to the environment variable list
await loadEnv({ export: true });
// Deno.env.get("name") retrieves the value from an environment variable named "name"
console.log(Deno.env.get("GITHUB_BEARER_TOKEN"));

// remaining code

When we run the script now with deno run, we get an interesting prompt:

Deno requests read access to ".env".
- Requested by `Deno.readFileSync()` API.
- Run again with --allow-read to bypass this prompt
- Allow?

This is one of the coolest parts about Deno; it has a security system that prevents scripts from doing something that you hadn't intended through its Permissions framework.

For example, if you weren't expecting your script to read from the env file, it'll prompt you to accept. Since packages can be taken over and updated to do nefarious things, this is a terrific idea.

The permissions can be tuned (e.g., you're only allowed to read from the .env file), or you can give blanket permissions. In our cases, two resources are being used (the ability to read the .env file and the ability to read the GITHUB_BEARER_TOKEN environment variable).

Let's run the command with the allow-read and allow-env flags.

deno run --allow-run --allow-env ./index.ts

If the bearer token gets printed, we've got the .env file created correctly and can proceed to the next step.

Let's Get Dynamic

Now that we have the bearer token, we can work on calling the GitHub Organization endpoint to retrieve the members.

Since this is GitHub related, we should create a new file, github.ts, to host our functions and types.

Adding axiod

In the github.ts file, we're going to be use axiod for communication. It's similar to axios in Node and is better than then the built-in fetch API.

Let's go ahead and bring in the import.

github.ts
import axiod from "https://deno.land/x/axiod@0.26.2/mod.ts";

Calling the Organization Endpoint

With axiod pulled in, let's write the function to interact with the GitHub API.

github.ts
// Brining in the axiod library
import axiod from "https://deno.land/x/axiod@0.26.2/mod.ts";

async function getMembersOfOrganization(orgName: string): Promise<any[]> {
  const url = `https://api.github.com/orgs/${orgName}/members`;
  // Necessary headers are found on the API docs
  const headers = {
    Accept: "application/vnd.github+json",
    Authorization: `Bearer ${Deno.env.get("GITHUB_BEARER_TOKEN")}`,
    "X-GitHub-Api-Version": "2022-11-28",
  };

  try {
    const resp = await axiod.get<any[]>(url, {
      headers: headers,
    });
    return resp.data;
  } catch (error) {
    // Response was received, but non-2xx status code
    if (error.response) {
      return Promise.reject(
        `Failed to get members: ${error.response.status}, ${error.response.statusText}`
      );
    } else {
      // Response wasn't received
      return Promise.reject(
        "Failed for non status reason " + JSON.stringify(error)
      );
    }
  }
}

To prove this is working, we can call this function in the index.ts file and verify that we're getting a response.

index.ts
1
2
3
4
5
6
7
8
9
import { config as loadEnv } from "https://deno.land/x/dotenv@v3.2.2/mod.ts";
import { getMembersOfOrganization } from "./github.ts";
import { Pair, createPairsFrom, shuffle } from "./utility.ts";

await loadEnv({ export: true });

const membersOfOrganization = await getMembersOfOrganization("JusticeLeague");
console.log(JSON.stringify(membersOfOrganization));
// rest of the file

Now let's rerun the script.

deno run --allow-read --allow-env ./main.ts
Deno requests net access to "api.github.com"
- Requested by `fetch` API.
- Run again with --allow-net to bypass this prompt.

Ah! Our script is now doing something new (making network calls), so we'll need to allow that permission by using the --allow-net flag.

deno run --allow-read --allow-env --allow-net ./main.ts

If everything has worked, you should see a bunch of JSON printed to the screen. Success!

Creating the Response Type

At this point, we're making the call, but we're using a pesky any for the response, which works, but it doesn't help us with what properties we have to work with.

Looking at the response schema, it seems the main field we need is login. So let's go ahead and create a type that includes that field.

github.ts
type GetOrganizationMemberResponse = {
  login: string;
};

async function getMembersOfOrganization(
  orgName: string
): Promise<GetOrganizationMemberResponse[]> {
  //code
  const resp = await axiod.get<GetOrganizationMemberResponse[]>(url, {
    headers: headers,
  });
  // rest of the code
}

We can rerun our code and verify that everything is still working, but now with better type safety.

Cleaning Up

Now that we have this function written, we can work to integrate it with our index.ts script.

index.ts
import { config as loadEnv } from "https://deno.land/x/dotenv@v3.2.2/mod.ts";
import { getMembersOfOrganization } from "./github.ts";
import { Pair, createPairsFrom, shuffle } from "./utility.ts";

await loadEnv({ export: true });

const names = await getMembersOfOrganization("JusticeLeague");
const pairs = createPairsFrom(shuffle(names));
const message = createMessage(pairs);
console.log(message);

So far, so good. The only change we had to make was to replace the hardcoded array of names with the call to getMembersOfOrganization.

Not an issue, right?

Hmmm, what's up with this? createMessage has a type error

It looks like createMessage is expecting Pair<string>[], but is receiving Pair<GetOrganizationMemberResponse>[].

To solve this problem, we'll modify createMessage to work with GetOrganizationMemberResponse.

index.ts
// Need to update the input to be Pair<GetOrganizationMemberResponse>
function createMessage(pairs: Pair<GetOrganizationMemberResponse>[]): string {
  // Need to update mapper function to get the login property
  const mapper = (p: Pair<string>): string =>
    `${p.first.login} meets with ${p.second.login}${
      p.third ? ` and ${p.third.login}` : ""
    }`;

  return pairs.map(mapper).join("\n");
}

With this last change, we run the script and verify that we're getting the correct output, huzzah!

Current Status

Congratulations! We now have a script that is dynamically pulling in heroes from the Justice League organization instead of always needing to see if Green Lantern is somewhere else or if another member of Flash's SpeedForce is here for the moment.

A working version of the code can be found on GitHub.

Coaching Corner Volume 3

Welcome to Cameron's Coaching Corner, where we answer questions from readers about leadership, career, and software engineering.

In this week's post, we look at how Alan can help their engineer figure out what they want to be when they grow up.

Hey Cameron!

I have a front-end engineer who's sharp, but they're not sure what their career growth looks like. I get the sense that they're interested in other roles outside of software development. How do you navigate this and help them grow?

Having Coffee with Deno - Inspiration

In a previous post, I mention my strategy of building relationships through one-on-ones. One approach in the post was leveraging a Slack plugin, Random Coffee, to automate scheduling these impromptu conversations.

Dinosaur sitting in a coffee cup

I wanted to leverage the same idea at my current company; however, we don't use Slack, so I can't just use that bot.

High-Level Breakdown

Thinking more about it, the system wouldn't be too complicated as it has three moving parts:

  • Get list of people
  • Create random pairs
  • Post message

To make it even easier, I could hardcode the list of people, and instead of posting the message to our message application, I could print it to the screen.

With these two choices made, I would need to build something that can shuffle a list and create pairs.

Technology Choices

Even though we're hardcoding the list of names and printing a message to the screen, I know that the future state is to get the list of names dynamically, most likely through an API call. In addition, most messaging systems support using webhooks to create a message, so that would be the future state as well.

With these restrictions in mind, I know I need to use a language that is good at making HTTP calls. I also want this automation to be something that other people outside of me can maintain, so if I can use a language that we typically use, that makes this more approachable.

In my case, TypeScript fit the bill as we heavily use it in my space, the docs are solid, and it's straightforward to make HTTP calls. I'm also a fan of functional programming, which TypeScript supports nicely.

My major hurdle at this point is that I'd like to execute this single file of TypeScript, and the only way I knew how to do that was by spinning up a Node application and using something like ts-node to execute the file.

Talking to a colleague, they recommended I check out Deno as a possible solution. The more I learned about it, the more I thought this would fit perfectly. It supports TypeScript out of the box (no configuration needed), and a file can be ran with deno run, no other tools needed.

This project is simple enough that if Deno wasn't a good fit, I could always go back to Node.

With this figured out, we're going to create a Deno application using TypeScript as our language of choice.

Getting Started With Deno

  1. Install Deno via these instructions
  2. Setup your dev environment - I use VS Code, so adding the recommended extension was all I needed.

Trying Deno Out

Once Deno has been installed and configured, you can run the following script and verify everything is working correctly. It creates a new directory called deno-coffee, writes a new file and executes it via deno.

1
2
3
4
mkdir deno-coffee
cd deno-coffee
echo 'console.log("Hello World");' >> coffee.ts
deno run coffee.ts

We've got something working, so let's start building out the random coffee script.

Let's Get Percolating

As mentioned before, we're going to hardcode a list of names and print to the screen, so let's build out the rough shape of the script:

const names = [
  "Batman",
  "Superman",
  "Green Lantern",
  "Wonder Woman",
  "Static Shock", // one of my favorite DC heroes!
  "The Flash",
  "Aquaman",
  "Martian Manhunter",
];
const pairs = createPairsFrom(shuffle(names));
const message = createMessage(pairs);
console.log(message);

This code won't compile as we haven't defined what shuffle, createPairsFrom, or createMessage does, but we can tackle these one at a time.

Let's Get Random

Since we don't want the same people meeting up every time, we need a way to shuffle the list of names. We could import a library to do this, but what's the fun in that?

In this case, we're going to implement the Fisher-Yates Shuffle (sounds like a dance move).

function shuffle(items: string[]): string[] {
  // create a copy so we don't mutate the original
  const result = [...items];
  for (let i = result.length - 1; i > 0; i--) {
    // create an integer between 0 and i
    const j = Math.floor(Math.random() * i);
    // short-hand for swapping two elements around
    [result[i], result[j]] = [result[j], result[i]];
  }
  return result;
}

const words = ["apples", "bananas", "cantaloupes"];
console.log(shuffle(words)); // [ "bananas", "cantaloupes", "apples" ]

Excellent, we have a way to shuffle. One refactor we can make is to have shuffle be generic as we don't care what array element types are, as long as we have an array.

Making this refactor gives us the following:

1
2
3
4
5
6
7
8
function shuffle<T>(items: T[]): T[] {
  const result = [...items];
  for (let i = result.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * i);
    [result[i], result[j]] = [result[j], result[i]];
  }
  return result;
}

Now, we can shuffle an array of anything. Nice!

Two of a Kind

Let's take a look at the next function, createPairsFrom. We know its type signature is going fromstring[] to something, but what?

In the ideal world, our total list of names is even, so we always have equal pairs.

1
2
3
4
{first: 'Batman', second: 'Superman'},
{first: 'Green Lantern', second: 'Wonder Woman'},
{first: 'Static Shock', second: 'The Flash'},
{first: 'Aquaman', second: 'Martian Manhunter'}

But what happens if Martian Manhunter is called away and isn't available? That would leave Aquaman without a pair to have coffee with (sad trombone noise).

In the case that we have an odd number of heroes, the last pair should instead be a triple which would look like the following:

1
2
3
{first: 'Batman', second: 'Superman'},
{first: 'Green Lantern', second: 'Wonder Woman'},
{first: 'Static Shock', second: 'The Flash', third: 'Martian Manhunter'}

Given that we've been using the word Pair to represent this grouping, we have a domain term we can use. This also means that createPairsFrom has the following type signature.

1
2
3
function createPairsFrom(names: string[]): Pair[] {
  // logic
}

But what does the Pair type look like? We can model either using an optional property or by using a discriminated union.

// Using an optional property
type Pair {
  first: string,
  second: string,
  third?: string
}

// Using Discriminated Unions
type Pair = {kind: 'double', first:string, second: string}
          | {kind: 'triple', first:string, second: string; third: string}

For now, I'm thinking of going with the optional property and if we need to tweak it later, we can.

Let's go ahead and implement createPairsFrom.

function createPairsFrom(names: string[]): Pair[] {
  // if we don't have at least two names, then there are no pairs
  if (names.length < 2) {
    return [];
  }
  const results = [];
  for (let i = 0; i <= names.length - 2; i += 2) {
    const pair: Pair = { first: names[i], second: names[i + 1] };
    results.push(pair);
  }
  if (names.length % 2 === 1) {
    // we have an odd length
    // Assign the left-over name to the third of the triple
    results[results.length - 1].third = names[names.length - 1];
  }
  return results;
}

// Example execution
console.log(createPairsFrom(["apples", "bananas", "cantaloupes", "dates"])); // [{first:"apples", second:"bananas"}, {first:"cantaloupes", second:"dates"}]
console.log(createPairsFrom(["ants", "birds", "cats"])); // [{first:"ants", second:"birds", third:"cats"}]

Similarly to shuffle, we can make this function generic as it doesn't matter what the array element types are, as long as we have an array to work with.

Refactoring to generics gives us the following:

1
2
3
4
5
type Pair<T> = { first: T; second: T; third?: T };

function createPairsFrom<T>(items: T[]): Pair<T>[] {
  // same function as before
}

To the Presses!

For the last part, we need to implement createMessage. We know it has to have the following type signature:

function createMessage(pairs: Pair<string>[]): string {}

We know the following rules.

  • When it's a double, we want the message to say, X meets with Y
  • When it's a triple, we want the message to say, X meets with Y and Z

Based on this, we need a way to map from Pair to the above string. So let's write that logic.

1
2
3
4
5
6
function createMessage(pairs: Pair<string>[]): string {
  const mapper = (p: Pair<string>) =>
    `${p.first} meets with ${p.second}${p.third ? ` and ${p.third}` : ""}`;

  pairs.map(mapper);
}

From here, we can join the strings together using the \n (newline) character.

1
2
3
4
5
6
function createMessage(pairs: Pair<string>[]): string {
  const mapper = (p: Pair<string>) =>
    `${p.first} meets with ${p.second}${p.third ? ` and ${p.third}` : ""}`;

  return pairs.map(mapper).join("\n");
}

All Coming Together

With the implementation of createMessage, we can execute our script by running deno run coffee.ts

1
2
3
4
5
6
deno run coffee.ts

"Superman meets with Wonder Woman
Batman meets with The Flash
Martian Manhunter meets with Aquaman
Static Shock meets with Green Lantern"

From here, we have a working proof of concept of our idea. We could run this manually on Mondays and then post this to our messaging channel (though you might want to switch the names out). If you wanted to be super fancy, you could have this scheduled as a cron job or through Windows Task Scheduler.

The main takeaway is that we've built something we didn't have before and can continue to refine and improve. If no one likes the idea, guess what? We only had a little time invested. If it takes off, then that's great; we can spend more time making it better.

Wrapping Up

In this post, we built the first version of our Random Coffee script using TypeScript and Deno. We focused on getting our tooling working and building out the business rules for shuffling and creating the pairs.

In the next post, we'll look at making this script smarter by having it retrieve a list of names dynamically from GitHub's API!

As always, you can find a full working version of this bot on my GitHub.

Cameron's Coaching Corner Volume 2

Welcome to Cameron's Coaching Corner, where we answer questions from readers about leadership, career, and software engineering.

In this week's post, we look at how Chase can balance writing the perfect code and shipping something.

My question: As a young developer, I notice that sometimes I get paralyzed by options. I want to write the perfect piece of code. This helps me in writing good code but usually at the cost of efficiency. Especially when I am faced with multiple good options. Sometimes I want to KNOW I’m gonna write the right thing before I’m writing it when I my be better off with some trial and error

  1. Are these common problems that you see people face?
  2. What rules of thumb or other pieces of advice do you have to avoid writing nothing instead of something as a result of seeking the ideal?
  3. How important is planning vs trial and error ("failing fast" as they say) to good software development flow?

Five Minutes at Five Guys - When Metrics Conflict with UX

In a recent post, I spoke about the flaw of using a single metric to tell the story and how Goodhart's Law tells us that once we start measuring a metric, it stops being a useful metric.

Let's look at a real-world example with the popular fast food chain, Five Guys.

All I Wanted Was a Burger

Five Guys is known for making good burgers and delivering a mountain of piping hot fries as part of your order. Seriously, an order of small fries is a mountain of spuds. Five Guys make their fries to order, so they're not sitting around under a heat lamp.

Yo Dawg, I heard you wanted fries, so I put fries in your fries

This approach works great when ordering in person, but what happens if you order online? The process is essentially the same, the crew works on the burgers, but they won't start the fries until you're at the restaurant, so they're always guaranteeing that you get fresh made fries.

At this point, it's clear that receiving a mountain of hot, cooked-to-order fries is part of the experience and what customers expect, right?

Cameron's Coaching Corner - Mentoring an Intern

Welcome to Cameron's Coaching Corner, where we answer questions from readers about leadership, career, and software engineering.

In this week's post, we look at how test123 can improve the mentoring experience for their new intern.

I recently had an intern join my time and I’m going to be his mentor. I’ve had interns in the past, but this one doesn’t understand any fundamentals and struggles with everything.

My question to you is this, how can I help him? He doesn’t know HTML/CSS/JS, so I’m trying to teach him those, but it’s taking away a lot of time. I suggested for him to watch some videos and then we can sync twice a day to go over the topics and discuss them further.

My issue: I don’t want to just say “go watch videos.” Bc, that’s not the best way to learn - I want him to dive into the code and try things and break, that’s how I learned at least.

How do you think I should handle this? I wanna be a good mentor and I want him to learn and grow. I don’t wanna fail the kid bc I don’t know the proper way to mentor.

Five Tips to Improve Your Coaching Conversations

As a leader, you're responsible for coaching and growing your team, helping them be successful. To do this, you need to set the tone and example of the behaviors you want the team to have.

No matter how good of a team you have or how good of a leader you are, you will have to have a conversation about performance. Whether it's delivery, professional skills, or technology skills, you will have a moment where you need someone to change their behaviors.

Having these types of conversations can be scary, no matter how much experience in leadership you might have. However, when done correctly, these moments can greatly impact the other person, helping them grow tremendously.

On the other hand, poor coaching will do the absolute opposite. The other person can become confused or angry. They could even shut down and disengage altogether, making coaching them that much harder.

So what does good coaching look like? I can't guarantee that these steps will solve all your woes; however, I do guarantee that following these tips will increase the odds of the other person listening and at least consider your feedback. Remember, you want to do this coaching because some behavior has caught your attention, and you want to correct it. If the other person doesn't listen or want to engage, then you literally can't make this happen.

Step 1: Be Timely

The sooner you can have this conversation, the more effective it will be. Remember, the point of feedback is to let the other person know how they're doing and correct if needed. They can't do this if the behavior happened three weeks ago because there's no correlation at that point.

Imagine if you had a test suite that only told you about failing tests a week after the build started. There's no way you could make the right decisions, so why would we think that's the case for behavior?

If I've noticed a pattern and feel that it's time to coach, I will get that feedback to them that week, if not the next day.

Step 2: Be Specific

When giving this feedback, the behavior may be obvious to you but not even a thought for the other person. Because we can't control what the other person is thinking, we need to set the context for the feedback so they know what you're talking about.

Let's take my son, for example. He's particular about his food, so when he says that "dinner was awesome," that makes me feel great as I'm happy he enjoyed dinner. But I have no clue what he actually liked or why he thought that. Was it the food? The way it was served? The fact that we had a picnic? No idea, so I'd respond with, "What made it stand out to you?". When he mentions that he liked the pizza, I go "Ah! He enjoyed the food, nice!"

Providing this specific context is crucial for the other person because it lets them kow what caught your attention and drastically reduces the confusion in the conversation. For those who like more concrete details, sharing links to chats, emails, or other artifacts with the behavior can be helpful because you can use it as the foundation for the conversation.

Step 3: Explain Why

There's a reason that you're having this conversation. There's something that's important to you, and, in your opinion, it wasn't important to the other person. We've got to explain why it's important and why you're commenting on it.

I never want to remove someone's autonomy as I like to set the direction and let the team blaze a path, with me guiding to make sure we don't get lost in the wilderness. However, for someone to have autonomy, they need to understand the goals and the reasoning behind it.

If they don't have this knowledge, then it's that much harder for them to make the right decisions. Ensuring they know the why is a leader's responsibility.

Step 4: Seek To Understand

You're working with a team of professionals. A professional makes the right decisions based on their knowledge and experience. If the person is making mistakes, we need to understand why they made the choice that they did.

For example, let's say I'm coaching someone who's consistently missing meetings. I'm frustrated that they're unresponsive and that they don't care. The issue here is that it's okay for me to feel frustrated, but I can't make the judgment that they don't care. I don't know that, and it causes more problems than it solves. I won't vent my feelings to the other person because even though it'd make me feel better, it doesn't help the situation.

A better approach would be to understand why they're missing meetings. Is it something outside of work? Could it be that they don't know why they need to attend? What if they didn't receive an invite? In any of the above cases, there was a solid reason why they didn't attend, and I wouldn't have known that if I had not opened the conversation.

Don't assume malice or apathy when something happens. We are humans first, which means we're going to make mistakes.

Step 5: Working Together

The entire point of coaching is to help the person improve, and we also don't want to take away their autonomy. To make this happen, we need to work with the other person to come up with ideas that can help improve the situation. It's not any one person's responsibility, but it's your responsibility to brainstorm with them and help guide them down the correct path.

The key here is to have an open mind and really consider all ideas. One of my favorite leadership books, First, Break All The Rules, talks about how great leaders work with their people to have their strengths shine and to make their weaknesses a non-issue.

In the missing meeting example, I found out that the issue was that they didn't know why they needed to attend the meeting, so they didn't attend, in order to focus on their development work. Working together, I changed invites to include the reason for attending and encouraged them to chat with me so we could figure it out if they didn't know why they needed to be there.

Case Study - Bringing It Together

In this example, let's explore where we would need to do some coaching.

While reviewing a pull request from Bruce, you see a comment from Alvin, a member of your team, where they were particularly critical of the work. Reading through the pull request, you see Alvin has left more harsh comments about Bruce's work.

Talking with Bruce, they mention that they don't work well with Alvin as it seems like he's always critical of Bruce.

Based on this scenario, we know that Alvin has left some harsh words for Bruce, which makes them less likely to work together. If Alvin keeps this behavior up with other people, this will impact others wanting to work with him, reducing his effectiveness.

After collecting your thoughts, you reach out to Alvin to see if he's got a few minutes to chat about Bruce's pull request.

"Hey Alvin, I noticed you left some pretty harsh comments that in Bruce's pull request. For example, saying that 'this code is convoluted, rewrite it'. Even if that was the case, it's not clear why you think that. I'm more concerned with how the messaging came across because we work with others to accomplish our tasks, and that communication style can make people not want to work with us.

I don't believe you intend to alienate others, so can you walk me through your thought process here and why you thought this was the right approach?"

In this example, we've already hit four out of the five tips. Our feedback was timely and specific to the problem. We included why it caught our attention and started with an open-ended question for the conversation about the behavior.

In the follow-up conversation, Alvin mentions that he was having a rough day, particularly outside of work, and that he wasn't entirely focused on his tone. Given that this is the first time Alvin has done this, we want to focus on fixing the issue before it becomes a pattern.

"I understand that it can be hard to focus on your tone when you're having a rough time, however, we can't speak to others this way. I don't want this to become a pattern, so what are some things that we could do instead when we're not in the right mental head space for code reviews?"

At this point, we've acknowledged what was said and reaffirmed expectations. Using another open-ended question, we can start brainstorming things that we could do to help improve Alvin's tone. Since we're opening the conversation, Alvin is also giving feedback on what might work for him and what wouldn't work.

Wrapping Up

Giving critical feedback to someone is not the easiest thing to do, however, it can have the most impact for them. To help frame the conversation, our coaching should:

  • Be Timely
  • Be Specific
  • Explain the Why
  • Seeking to Understand
  • Be Collaborative

Building Relationships Through One-on-Ones

As a leader, one of your goals is to build a strong, high-performing team. To do this, you'll need to establish and develop relationships within the team and with each other. One approach I've found helpful to build and sustain these relations is through one-on-ones.

When most people think of one-on-ones, they typically think of some scheduled time, every so often, where they talk about work concerns or whatever is on the leader's mind. Some might find one-on-ones a waste of time and skip them.

Let's face it, we've all had bad one-on-ones where the conversation was forced and stiff. Or, it felt like the other person wasn't listening or cared about what was being talked about. If enough one-on-ones go down this route, it's no surprise that people don't want to have these conversations.

So, how do we improve the situation? In this post, I'm going to show you three things you can do to improve the one-on-ones you're having with the team by making sure that they're being heard, relationships are being built, and, just maybe, you might even get to know them better.

It's Not a Status Update

A common mistake is treating one-on-ones as status updates for work or projects. Even though it might be tempting to get an update (especially on important projects), remember this time is for the other person to talk with you about what's on their mind. They can't do this if you're asking for updates on work. If you're leveraging stand-ups or a task board, then you should be able to get updates from there. If this isn't sufficient, ask for updates outside this conversation. The one-on-one is where you give the other person your full attention.

Despite your best intent, this can be a tough habit to break. To help get out of this mindset, start time-boxing the updates to be a set period of time (e.g., ten minutes) with the goal of making this period shorter in subsequent one-on-ones.

Another technique I use is resetting, where I remind my teammate of the purpose and goal of the conversation. Doing this, it's a gentle nudge in the correct direction and reinforces that this time is for them, not for whatever is on my mind.

Don't Get Distracted

In the world of working remotely, it's easy to get distracted by chat notifications or emails. You're on the call, and you see the notification at the bottom of your screen, and before you realize it, you've read it and already thinking about a response. This may be great for you getting things done; however, for the other person, it's clear that you stopped paying attention. So what's important? Is it the notification or the other person?

pedestrians looking distracted in downtown
Credit to Matt Quinn via Unsplash

To help reinforce the focus, I put myself in Do Not Disturb mode, which will hide notifications from me until the meeting ends. If the issue is truly important, then someone would give me a call, in which case, I can excuse myself from the one-on-one to see what's up. In this rare case, I will reschedule our conversation as it's essential that we meet.

Allow Them to Set the Agenda

Another mistake I see leaders make is that they'll come to the one-on-one with a list of topics they want to discuss for the day. This can be particularly true if you're coaching this person and you want to provide concrete feedback. However, remember, the goal is to build and strengthen the relationship, and you can't do that if you're always setting the agenda for the conversations.

I find it amazing that you can learn quite a bit about the other person based on what they bring up. For example, if they're speaking about a conflict with another person, that lets me know that they're aware of relationships and how they're being perceived. On the other hand, if they're talking about a project and the concerns they have, then they're thinking outside of a task and are thinking at a higher level.

In one case, I was in a one-on-one with an engineer where they brought up their concern about supporting an API as they didn't know much about it. My first impression was that they didn't know how to read the code, but digging in, it turned out that they didn't see how the API fit into the bigger picture of the system. Learning this was a great fact check because I thought it was a technical issue, but in reality, it was a system question, which changed my perspective on them.

(Bonus) - Seeding a Conversation

I mentioned that the other person should own the agenda, but sometimes, they may not have much on their mind or a topic that sticks out to them. For these cases, I come prepared with a list of questions to help start the conversation.

pedestrians looking distracted in downtown
Credit to Markus Spiske via Unsplash

In Warren Berger's The Book of Beautiful Questions, he talks about the power of open-ended questions and how they can help people be more open. So instead of asking, "How's the project coming along?", which can be answered in a binary fashion, we could instead ask, "What's one thing about the project that stands out to you?", which could give us a richer response and allow you to learn more.

For those looking for questions, here's an excerpt of questions I've used to help seed a conversation:

  • What's one thing about the current project that turned out to be harder than you thought?
  • What was your biggest win last week, and why does it stand out?
  • What's something that happened in the past week that you want to learn more about?
  • I know you've been working more with this past week, talk to me about that experience
  • What's something that you're looking forward to?

Wrapping Up

To build great teams, you will need to foster relationships with the team and the individual members. Having one-on-ones can help with build these relationships, but only if you treat them like so. By allowing your teammate to set the agenda, giving them your full attention, and cutting out project updates, you can improve the quality of your conversations and get to know them better.

Building Relationships with Intent

As a leader, one of your superpowers is how you can connect your team with someone else. For example, if your team is struggling to work with an API and you know someone who's made recent changes or is a Subject Matter Expert, you can get your team unblocked and moving faster.

I've worked with leaders like this, and it's amazing how fast and helpful it is to get unstuck quickly. Don't get me wrong, sometimes it's the right strategy to "burn the time" to learn, but it's helpful to know someone who can get you unstuck if you need it.

With one leader, it seemed like they always knew a guy, no matter the topic, and I was amazed at how they did it. So like a lifelong learner, I asked, and they told me networking.

large group of people networking
Credit to ProductSchool via Unsplash

Ugh

If you're like me, you hear networking and think about dozens (if not hundreds) of people milling around, introducing themselves, and sharing business cards. Don't get me wrong, that approach can work for some people, but to me, that sounds exhausting. Instead of a hummingbird, going from flower to flower, I'm more like a bear. I just want to sit down and eat my jar of honey.

So what am I supposed to do? How am I supposed to network if I don't like large groups?

Given time, you can work on becoming more comfortable in large groups. However, why fight your natural tendencies and what you're good at?

For me, it's small groups and one-on-one conversations. As such, that's my approach to networking. Though it takes a bit longer, I find that I build stronger relationships with those people, and in turn, can be just as successful. I like to think about one-on-ones as lazy river conversations as I never know where it will take us.

man sitting in inner tube
Credit to Kiara Kulikova via Unsplash

One Cup of Coffee

At a previous company, we used Slack and, as such, had an integration called Random Coffee that would pair the members of a channel up to get together for the week. Such a simple idea, but so powerful when you now have a built-in excuse to chat with someone.

Coffee Chat
Credit to Priscilla Du Preez via Unsplash

After a couple of weeks of getting to know people, I started learning what others did, what interests them, and who to go to about specific issues. Combine that with asking, "How did you know that?" I found that I could quickly fill gaps in my knowledge.

But something else happened. Once I knew the person, I didn't see them as a name in the chat anymore, I saw them as their selves. I'd find myself saying, "Oh, it's Chris, and Chris is cool, so I'll help him out," instead of thinking, "Ugh, another thing to do." In a way, these conversations humanized those I worked with, and I found myself caring more about them.

Caring By Knowing

To me, this is the most important thing about relationship building and networking. Deep down, I want to care about those I work with because I want them to be successful. I can't help them be successful if I don't know them both as a colleague and as a person.

Having one-on-ones is how I know my people and how I continue building care. It might be as simple as knowing what types of things they like to work on or what they did over the weekend. However, having these conversations helps both of us open up, and I get to know them so much better. Once I know them, I can guide and direct them better, looking for opportunities I wouldn't have thought of before.

How Do I Start?

If you want to start this for your own company, you don't have to have Slack to make this happen. The important thing is getting buy-in from others and explaining the why behind the exercise.

Once you have buy-in, you can start low-tech by using an Excel sheet and randomizing the list of names. This isn't the most robust solution, but it's a start and you can iterate as you figure out the timing, the frequency, and the other steps that setting this up would look like.

Once you've got something in motion, you can always work on automating the process later. Don't let a perfect solution stop you from starting with a good solution.

What I've found successful is having either a weekly or fortnightly scheduled message in our main channel that assigns the groups. From there, participants are encouraged to share something they've learned about their counterparts during their conversation. To help make sure that people meet, scheduling a set time during the week for all the groups can be helpful as it removes another barrier (e.g., if you know that you'll have coffee at 10:30 am on Tuesdays, you learn to expect it).

If you have a group that is just starting, it might be helpful to provide some starting questions to help jump-start the conversation. A good list of questions can be found in one of my gists.

Wrapping Up

To be a successful leader, you must cultivate and grow relationships with those you work with. Not only does it help your team be successful, but it allows you to have a richer experience with your work and helps solidify that we're all working together.

Better Domain Modeling with Discriminated Unions

When I think about software, I like designing software so that doing the right things are easy and doing the wrong things are impossible (or at least very hard). This approach is typically called falling into the pit of success.

Having a well-defined domain model can prevent many mistakes from happening just because the code literally won't let it happen (either through a compilation error or other mechanisms).

I'm a proponent of functional programming as it allows us to model software in a better way that can reduce the number of errors we make.

Let's at one of my favorite techniques discriminated unions.

Motivation

In the GitHub API, there's an endpoint that allows you to get the events that have occurred for a pull request.

Let's take a look at the example response in the docs.

[
  {
    "id": 6430295168,
    "url": "https://api.github.com/repos/github/roadmap/issues/events/6430295168",
    "event": "locked",
    "commit_id": null,
    "commit_url": null,
    "created_at": "2022-04-13T20:49:13Z",
    "lock_reason": null
  },
  {
    "id": 6430296748,
    "url": "https://api.github.com/repos/github/roadmap/issues/events/6430296748",
    "event": "labeled",
    "commit_id": null,
    "commit_url": null,
    "created_at": "2022-04-13T20:49:34Z",
    "label": {
      "name": "beta",
      "color": "99dd88"
    }
  },
  {
    "id": 6635165802,
    "url": "https://api.github.com/repos/github/roadmap/issues/events/6635165802",
    "event": "renamed",
    "commit_id": null,
    "commit_url": null,
    "created_at": "2022-05-18T19:29:01Z",
    "rename": {
      "from": "Secret scanning: dry-runs for enterprise-level custom patterns (cloud)",
      "to": "Secret scanning: dry-runs for enterprise-level custom patterns"
    }
  }
]

Based on the name of the docs, it seems like we'd expect to get back an array of events, let's call this TimelineEvent[].

Let's go ahead and define the TimelineEvent type. One approach is to start copying the fields from the events in the array. By doing this, we would get the following.

type TimelineEvent = {
  id: number;
  url: string;
  event: string;
  commit_id?: string;
  commit_url?: string;
  created_at: string;
  lock_reason?: string;
  label?: {
    name: string;
    color: string;
  };
  rename?: {
    from: string;
    to: string;
  };
};

The Problem

This definition will work, as it will cover all the data. However, the problem with this approach is that lock_reason, label, and rename had to be defined as nullable as they can sometimes be specified, but not always (for example, the lock_reason isn't specified for a label event).

Let's say that we wanted to write a function that printed data about TimelineEvent, we would have to write something like the following:

1
2
3
4
5
6
7
8
9
function printData(event: TimelineEvent) {
  if (event.event === "labeled") {
    console.log(event.label!.name); // note the ! here, to tell TypeScript that I know it'll have a value
  } else if (event.event == "locked") {
    console.log(event.lock_reason);
  } else {
    console.log(event.rename!.from); // note the ! here, to tell Typescript that I know it'll have a value
  }
}

The main problem is that the we have to remember that the labeled event has a label property, but not the locked property. It might not be a big deal right now, but given that the GitHub API has over 40 event types, the odds of forgetting which properties belong where can be challenging.

The pattern here is that we have a type TimelineEvent that can have different, separate shapes, and we need a type that can represent all the shapes.

The Solution

One of the cool things about Typescript is that there is a union operator (|), that allows you to define a type as one of the other types.

Let's refactor our TimelineEvent model to use the union operator.

First, we need to define the different events as their own types

type LockedEvent = {
  id: number;
  url: string;
  event: "locked"; // note the hardcoded value for event
  commit_id?: string;
  commit_url?: string;
  created_at: string;
  lock_reason?: string;
};

type LabeledEvent = {
  id: number;
  url: string;
  event: "labeled"; // note the hardcoded value for event
  commit_id?: string;
  commit_url: string;
  created_at: string;
  label: {
    name: string;
    color: string;
  };
};

type RenamedEvent = {
  id: number;
  url: string;
  event: "renamed"; // note the hardcoded value for event
  commit_id?: string;
  commit_url?: string;
  created_at: string;
  rename: {
    from: string;
    to: string;
  };
};

At this point, we have three types, one for each specific event. A LockedEvent has no knowledge of a label property and a RenamedEvent has no knowledge of a lock_reason property.

Next, we can update our definition of TimelineEvent to use the union operator as so.

type TimelineEvent = LockedEvent | LabeledEvent | RenamedEvent;

This would be read as A TimelineEvent can either be a LockedEvent or a LabeledEvent or a RenamedEvent.

With this new definition, let's rewrite the printData function.

1
2
3
4
5
6
7
8
9
function printData(event: TimelineEvent) {
  if (event.event == "labeled") {
    console.log(event.label.name); // note that we no longer need !
  } else if (event.event == "locked") {
    console.log(event.lock_reason);
  } else {
    console.log(event.rename.to); // note that we no longer need !
  }
}

Not only do we not have to use the ! operator to ignore type safety, but we also have better autocomplete (note that locked_reason and rename don't appear when working with a labeled event). Better autocomplete

Deeper Dive

At a general level, what we've modeled is a sum type and it's great for when you have a type that can take on a finite number of differing shapes.

Sum types are implemented as either tagged unions or untagged unions. Typescript has untagged unions, however, other languages like Haskell and F#, use tagged unions. Let's see what the same implementation in F# would have looked like.

// specific type definitions omitted since they're
// similar to typescript definition
// ....
type TimelineEvent = Locked of LockedEvent | Labeled of LabeledEvent | Renamed of RenamedEvent

let printData e =
    match e with
    | Locked l -> printf "%s" l.lock_reason
    | Labeled l -> printf "%s" l.label.name
    | Renamed r -> printf "%s" r.rename.``to`` // the `` is needed here as to is a reserved word in F#

A tagged union is when each shape has a specific constructor. So in the F# version, the Locked is the tag for the LockedEvent, Labeled is the tag for the LabeledEvent, so on and so forth. In the Typescript example, we worked around it because the event property is on every TimelineEvent and is a different value.

If that wasn't true, then we would had to have added a field to TimelineEvent (typically called kind or tag) that would help us differentiate between the various shapes.

Wrapping Up

When defining domain models where the model can have different shapes, you can use a sum type to define the model.