Skip to content

Dispatches

Oh, Adeline

An original murder ballad, inspired by the traditional folk songs “Poor Ellen Smith” and “Two Sisters”

Trigger Warning

This piece includes themes of drowning, death, guilt, and implied violence. Please proceed with care.

Lyrics

Oh, Adeline

We walked where the sycamores shade the old track
With a heavy gray sky riding low on our backs
She talked of a ticket, I spoke of the land
How the past is a shackle folks won’t understand
The river ran quiet, the moon low and thin
I swore to my Savior I’d bring her back in

     Oh, Adeline
     The river took your breath away
     Oh, Adeline
     There’s a price that I’ll pay
     The slate is slick, the water’s black
     And there’s no trail to lead us back
     Poor Adeline
     I’ve crossed that heavy, dark line

One step on the edge where the slate turned to slick
The air went to static, the silence turned thick
The ground gave a shudder and gave to the stream
A crack in the night, a tear in a dream
I shouted her name to the heavy black air
But the river was hungry and she wasn’t there

     Repeat Chorus

The law found a shoe in the mud by the bend
Said my story starts ragged and won’t find an end
I tell ’em I loved her, I swear that I stayed
I show ’em the river, the bed that it made
But late in the night when the house settles low
I hear that dark water and know what I know

If guilt is a mountain, I’m lost on the peak
If sin is a language, it’s all that I speak
Don’t call it murder, that’s a label, a lie
Call it a moment that simply passed by
I never pushed her, I made no demand
I just didn’t tighten the grip of my hand

     Repeat Chorus

Writer: J. Ryan Johnson (BMI)
Copyright: © 2026 J. Ryan Johnson. All rights reserved.
Phone: +1 (407) 902-5419
Email: hello [at] tenthirtyam [dot] org

Songwriter Note

I am a songwriter and a musician, but I'm not confident that I have the voice that's needed to inhabit these verses.

My hope is that these words eventually find an artist and storyteller to help me breathe life into them. If that collaboration never comes, I may record them myself rather than let them settle into silence.

Until then, they remain as they were born: quiet reflections on the grit, grace, and shadows just north of the county line.

For It All

Lyrics

For It All

The radiator’s humming
The lights are turned down low
I’m leaning on the counter
Watching headlights on the road
There’s a pair of my boots
And the mail I haven’t read
By their faded crayon drawings
Hanging on by a thread
I’m pacing in my mind
They’re sleeping down the hall
I’m carrying this weight in silence
Knowing I’ve hit a wall

     I don’t say it
     I just carry it around
     Like the dirt in the floorboards
     And the rain that won’t come down
     I love them more than living
     But some days I disappear
     Struggling to be the man they need
     Still looking for me here

She asks me if I’m tired
I say, “Baby, I’m alright”
But there’s tired in my body
And there’s tired in this fight
There's a kind that comes from working
Another comes from fear
Of the monsters at my doorway
That I won't let get too near
But I can’t always stop my temper
I don’t always hold my tongue
And I pray they don't just remember
All the times I’ve come undone

     Repeat Chorus

I don’t cry where they can see me
I never fall apart in the light
I just hug them in the doorway
And I make it through the night
I hope they know I love them
When my hands and heart are tired
But underneath the man they need
There’s another child inside

     Repeat Chorus

But when the house gets quiet
And I’m standing in this hall
I listen to them dreaming
And thank God for it all

Writer: J. Ryan Johnson (BMI)
Copyright: © 2026 J. Ryan Johnson. All rights reserved.
Phone: +1 (407) 902-5419
Email: hello [at] tenthirtyam [dot] org

Songwriter Note

I am a songwriter and a musician, but I'm not confident that I have the voice that's needed to inhabit these verses.

My hope is that these words eventually find an artist and storyteller to help me breathe life into them. If that collaboration never comes, I may record them myself rather than let them settle into silence.

Until then, they remain as they were born: quiet reflections on the grit, grace, and shadows just north of the county line.

How to Structure an Ansible Collection

Ansible

I recently wrapped up work on an Ansible collection I developed for a customer through my role at Broadcom. With the project complete, I wanted to write down the structure it followed and the reasons behind it. This guide is for developers building their first collection and maintainers checking an existing one against Ansible's recommended practices.

Those decisions matter once an Ansible repository has been in use for a while. Roles sit beside playbooks that depend on custom filters. Lookup plugins get copied from one repository to another. The same automation has to run locally and in Ansible Automation Platform (AAP), where repository-specific plugin paths become difficult to maintain.

A collection gives that content a package name and a standard layout. Roles, plugins, playbooks, documentation, and tests can ship together as a versioned package under a namespace such as example.platform. Once you understand where each part belongs, the structure is straightforward.

Farmer's Daughter

Reflection

Published alongside Field After Field, Song After Song, a reflection on the long Saturday drives my dad, my brother, and I made across Southwest Georgia in the 1990s, checking rain gauges for a local peanut company. From field to field and county to county, all while 90s country music poured through the open windows of an old truck and carried us through the heat, dust, backroads, and memory of those days.

Lyrics

Farmer's Daughter

Sun’s dippin’ down like a fishing lure
Cruisin’ in the country where the air is pure
I’m pulling up slow in a beat-up Ford
She’s hoppin’ in saying, “Thank the Lord!”
She’s got those cutoffs on and a tan-line smile
Saying, “Hey baby, let’s drive a while…”
Don’t need a map, don’t need a plan
Just a touch of her hand and the night just began

The preacher’s still talkin’ ‘bout the promised land
But I got a little piece of it holdin’ my hand
Yeah we’re lockin’ it in, we’re shiftin’ it down
Leavin’ the lights of this sleepy-eyed town

     Seminole Wind over muddy water
     Ridin’ with a Wild One, the farmer’s daughter
     Under that big bright Neon Moon
     Blasting Chattahoochee to a backbeat boom
     Friends in Low Places raising ’em high
     Baby Likes to Rock It shaking the sky
     Young, dumb, a little Dust On the Bottle
     Time Marches On, no throttle

Check the rearview, yeah the dust is high
Like a Georgia storm in the July sky
She’s got her boots on the dash, singin’ every word
Prettiest harmony I ever heard
We’re crankin’ up the radio, yeah we’re ready to go
Taking it fast while we’re living it slow
Ain’t nothing changed since ’94
‘cept the speakers hit a little bit more

The preacher’s still talking ‘bout the promised land
But I got a little piece of it on her left hand
Yeah we’re lockin’ it in, we’re shiftin’ it down
Still the talk of this sleepy-eyed town

     Repeat Chorus

We killed the engine by the riverside
Nowhere to run and nowhere to hide
She’s tasting sweeter than muscadine wine
Yeah, I’m gonna love her for a long, long time
Radio hummin’ low, holdin’ us tight
She’s leanin’ on me, feels so right
Yeah, the static is fuzzy, but the feeling is clear
It’s always a Friday night when we’re out here

     Repeat Chorus

Writer: J. Ryan Johnson (BMI)
Copyright: © 2026 J. Ryan Johnson. All rights reserved.
Phone: +1 (407) 902-5419
Email: hello [at] tenthirtyam [dot] org

Songwriter Note

I am a songwriter and a musician, but I'm not confident that I have the voice that's needed to inhabit these verses.

My hope is that these words eventually find an artist and storyteller to help me breathe life into them. If that collaboration never comes, I may record them myself rather than let them settle into silence.

Until then, they remain as they were born: quiet reflections on the grit, grace, and shadows just north of the county line.

Field After Field, Song After Song

90s country music takes me back to Saturday mornings in Southwest Georgia, when my dad, my brother, and I would climb into an old two-toned orange and white 1979 Chevy truck we called "Timex," because that thing "took a lickin' and kept on tickin'"

No air conditioning. No mercy from the sun.

Just rolled-down windows, hot vinyl seats, sweaty towels, ham sandwiches, chips, and a cooler full of sodas sweating right along with us.

Those Saturdays, we drove hundreds of miles across several counties checking rain gauges for one of the local peanut companies. Each field had its own little measure of mercy standing out there in the dirt, waiting to tell us what the sky had given and what it withheld.

Some were off highways. Some were down dirt roads.

Some were tucked so far back it felt like the land had swallowed the road whole.

One of us would run the gauge while the other wrote down the number for the week. Then we'd reset it and move on.

Field after field. County after county.

The data went back to the peanut company so they could see which contracted farmers were getting rain, especially the dryland fields with no irrigation to save them, just like our own land.

And while those gauges weren't supposed to sit under a center pivot, more than once we found ourselves caught in the wrong place when one started walking across a field like some long-legged iron ghost.

The rain mattered. In that country, rain wasn't just weather. It was debt, crop, prayer, and judgment. It was the difference between holding on and going under.

And through all of it, the radio stayed on.

Most days it was 95.5 WTVY out of Dothan, Alabama, carrying itself through the cab like a fourth passenger on a crowded bench seat.

Alan Jackson, Aaron Tippin, Shenandoah, Sammy Kershaw, John Michael Montgomery, Joe Diffie, Clay Walker, Brooks and Dunn, George Strait, Garth Brooks, Clint Black, The Tractors, John Anderson, Faith Hill, George Jones, and so many more.

Their voices rode with us through the dust, the heat shimmer, the sudden storms, and the long stretches where nobody had much to say.

That music became part of the work. It was stitched into the smell of hot metal and burning oil, the slap of weeds and briars against our legs, the red dirt on our shoes, and the strange quiet that falls over farmland in the middle of a searing afternoon.

We saw farms like ours, farms with better equipment and bigger money, and farms trying hard to look stronger than they were.

We learned the roads. We learned the fields. We learned how people tried to survive out there without saying too much about what it cost them.

Those were hard, hot days, but I remember them with fondness. Whatever trouble time laid over our family, those drives remain one of the good rooms in my memory. It was just the three of us, making a little extra money, burning up in that old truck, laughing when we could, and letting those songs carry us from one rain gauge to the next on our map.

That's what 90s country means to me.

It's not nostalgia without weight. It's not some easy longing for a past that never hurt.

It's the sound of being young in a place that was already teaching me about labor, weather, family, want, and endurance.

It's the sound of looking out over peanut rows and imagining that life might someday open wider than those backroads, even as the land was busy marking me in ways I wouldn't understand until much later in life.

“Farmer’s Daughter” grew out of these memories. It's a playful homage to the country music I came up on, but it carries the dust of those Saturdays underneath it.

It's for the radio in that old orange and white '79, for the highways and backroads we learned by heart, for the rain we measured, for the heat we endured, and for the boy I was back then, riding through the fields and dreaming about what the future might sound like.

Development Containers: Consistent Environments for Every Contributor

Development Containers logo

Some engineering teams hit the same friction eventually: A new developer clones the repository, follows the setup instructions, and spends the next two hours debugging why the toolchain that works on everyone else's machine doesn't work on theirs. The versions are slightly different. A system dependency is missing. The environment variable was never set. By the time they make their first commit, they've already burned half a day on tasks that had nothing to do with the code they were hired to write.

Development containers help solve this by codifying the development environment as a container image. Every contributor uses the same tools at the same versions on every machine. Onboarding becomes more a matter of opening the repository rather than following a tedious setup guide.

A development container, commonly called a "devcontainer", is a Docker container configured as a development environment. It holds the runtime, tools, extensions, and settings required to work on the project, and that configuration lives in the repository right next to the codebase.

The Development Containers specification defines the format that editor and platform integrations follow. VS Code, GitHub Codespaces, JetBrains Gateway, and the Dev Containers CLI all consume the same .devcontainer/ definition. A valid configuration should work across those entry points without forked setup docs.

Instead of a README.md that says "install x v1.24, y 1.11, and z v7.1.2," the repository includes a container image specification.

MCP Servers: A Practical Guide to Context on Demand

Model Context Protocol, or MCP, is how an AI client asks for context instead of waiting for you to shuttle it in. Clients can already expose open files, workspace context, and sometimes terminal output. Remote services, databases, CI runs, output from another shell, you still feed that in by hand. MCP gives the client a shared protocol to discover tools, read resources, and reach those systems directly. If the LLM is doing the reasoning, an MCP server is the part that lets it look around and, when you allow it, act on current state.

The model doesn't have to rely only on whatever you remembered to supply it. It can ask for current state, call a tool you exposed, and come back with something closer to evidence.

Less autocomplete. Less guessing. More "I checked x and y, and here is what I found."

MCP is an open protocol for connecting AI applications to the systems around them.

An MCP server can expose a few kinds of things:

  • Tools: actions the model can ask to run, such as read_logs, run_tests, or a query you wrapped instead of exposing raw shell.
  • Resources: readable context, such as repository metadata, service health, runbooks, or log files.
  • Prompts: reusable prompt templates that package common workflows.

An MCP client, such as VS Code or Cursor, connects to one or more servers and surfaces those capabilities to the model.

Old Workflow MCP Workflow
Paste a log into a chat The model calls a read_logs tool
Explain your repository The client provides workspace context
Paste command output The server returns structured tool output
Ask the model to guess state The model asks the system for current state
Manually perform every action The model proposes or invokes approved tool calls

MCP doesn't make a model omniscient. It gives the model a way to ask for context and use the actions you chose to expose. That matters more once you've lived through the manual context loop a few dozen times.

I'll Be Damned

Reflection

Some roads don't take you anywhere new; the ones that drag you back to what you can't forget.

For me, that’s the haunted stretch of State Road 39 that pulls away just north of the Miller County line, past my Grandaddy’s broken fences and the hard-baked clay ruts. It's where the world closes in around the pines and pastures, and you're just waiting to see what the afternoon is going to break next.

I never had the luxury of romanticizing this place. I was raised to be useful to a land that looks peaceful on a postcard but cuts deep when you’re trapped inside it. Where the stench of diesel and manure hangs heavy, and hardship settles onto your shoulders the same way the red dirt stains your boot seams.

These songs are how I drag those memories into the light.

They're written for the folks who carry the weight in total silence, only to get up tomorrow and do it all over again.

Lyrics

I'll Be Damned

Morning breaks like bad news
Over land that never speaks
Wind moves through the dead rows
Like it’s lookin’ for relief
Daddy’s boots by the back door
Dirt already on my hands
I was barely old enough
To know the weight of this damn land

     It’ll feed you, it’ll fight you
     Make you curse and make you pray
     And every year I tell myself
     This’ll be the year I walk away

     This is where the strong stay silent
     And the weak don’t last too long
     Where hope is just a habit
     And quittin’ feels too wrong
     Yeah, the roots go deep and the debt is steep
     And the devil holds my hand
     If I’m leavin’, I’ll be bleedin’
     If I’m stayin’, I’ll be damned

Iron breaks in the July heat
Right when the rain is runnin’ late
Banker’s callin’, diesel’s climbin’
Staring down an open gate
Old men nod across the fence line
Knowin’ what I just can’t say
You don’t choose this kind of livin’
It just bleeds into your veins

     Repeat Pre-Chorus

     Repeat Chorus

Red dust settled on the dash
Sweat dryin’ on my shirt
Yeah, every road around here
Starts and ends in dirt
You can tell yourself a story
But the ground don’t care at all
It just holds what we leave it
When our names quit gettin’ called

     Repeat Chorus

Writer: J. Ryan Johnson (BMI)
Copyright: © 2026 J. Ryan Johnson. All rights reserved.
Phone: +1 (407) 902-5419
Email: hello [at] tenthirtyam [dot] org

Songwriter Note

I am a songwriter and a musician, but I'm not confident that I have the voice that's needed to inhabit these verses.

My hope is that these words eventually find an artist and storyteller to help me breathe life into them. If that collaboration never comes, I may record them myself rather than let them settle into silence.

Until then, they remain as they were born: quiet reflections on the grit, grace, and shadows just north of the county line.

Generating a Python SDK from an OpenAPI Specification

If your API ships an OpenAPI spec, OpenAPI Generator can build the Python client from it. You don't have to hand-write models and endpoint wrappers.

Most tutorials stop at generate. After that you're still choosing generator options, picking a spec with enough surface area to matter, locking dependencies, running smoke tests, and keeping the repo sane when the spec changes again.

This walkthrough uses a compact OpenAPI 3.1 example. I use Astral uv for lock, sync, test, and build because that's what I run locally. The generator config and generate steps are the same with pip or Poetry.

Prerequisites

You need three things:

Start with a Useful Specification

Start with an API specification that'll actually exercise the generator: tags, pagination, request bodies, enums, timestamps, and a reusable error schema, not one of the generic "Pet Store" examples.

Here is a compact OpenAPI 3.1 example:

openapi: 3.1.0
info:
  title: Dispatch API
  version: 1.0.0
  summary: A compact task-tracking API for SDK generation examples.
  description: |
    This example stays small enough for a blog post while still looking like a real API.
    It includes pagination, reusable schemas, enums, timestamps, bearer auth, and a
    structured error response.
jsonSchemaDialect: https://json-schema.org/draft/2020-12/schema
servers:
  - url: https://api.example.com/v1
    description: Production
tags:
  - name: Tasks
    description: Manage tracked work items.
paths:
  /tasks:
    get:
      tags: [Tasks]
      operationId: listTasks
      summary: List tasks
      parameters:
        - name: status
          in: query
          required: false
          description: Filter tasks by workflow state.
          schema:
            $ref: "#/components/schemas/TaskStatus"
        - name: limit
          in: query
          required: false
          description: Maximum number of tasks to return.
          schema:
            type: integer
            minimum: 1
            maximum: 100
            default: 25
        - name: cursor
          in: query
          required: false
          description: Opaque pagination cursor from a previous response.
          schema:
            type: string
      responses:
        "200":
          description: A page of tasks.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/TaskListResponse"
        "401":
          description: Authentication failed.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    post:
      tags: [Tasks]
      operationId: createTask
      summary: Create a task
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/CreateTaskRequest"
      responses:
        "201":
          description: Task created.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "400":
          description: Invalid request body.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
  /tasks/{taskId}:
    get:
      tags: [Tasks]
      operationId: getTask
      summary: Get a task by ID
      parameters:
        - $ref: "#/components/parameters/TaskId"
      responses:
        "200":
          description: The requested task.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "404":
          description: Task not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
    patch:
      tags: [Tasks]
      operationId: updateTask
      summary: Update a task
      parameters:
        - $ref: "#/components/parameters/TaskId"
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/UpdateTaskRequest"
      responses:
        "200":
          description: Task updated.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Task"
        "404":
          description: Task not found.
          content:
            application/json:
              schema:
                $ref: "#/components/schemas/Error"
components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
  parameters:
    TaskId:
      name: taskId
      in: path
      required: true
      description: Stable task identifier.
      schema:
        type: string
        format: uuid
  schemas:
    TaskStatus:
      type: string
      enum:
        - queued
        - running
        - completed
        - failed
    Task:
      type: object
      required:
        - id
        - title
        - status
        - createdAt
        - updatedAt
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
          minLength: 1
          maxLength: 120
        description:
          type:
            - string
            - "null"
        status:
          $ref: "#/components/schemas/TaskStatus"
        labels:
          type: array
          items:
            type: string
        createdAt:
          type: string
          format: date-time
        updatedAt:
          type: string
          format: date-time
    CreateTaskRequest:
      type: object
      required:
        - title
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 120
        description:
          type:
            - string
            - "null"
        labels:
          type: array
          items:
            type: string
    UpdateTaskRequest:
      type: object
      properties:
        title:
          type: string
          minLength: 1
          maxLength: 120
        description:
          type:
            - string
            - "null"
        status:
          $ref: "#/components/schemas/TaskStatus"
        labels:
          type: array
          items:
            type: string
    TaskListResponse:
      type: object
      required:
        - items
      properties:
        items:
          type: array
          items:
            $ref: "#/components/schemas/Task"
        nextCursor:
          type:
            - string
            - "null"
    Error:
      type: object
      required:
        - code
        - message
      properties:
        code:
          type: string
        message:
          type: string
        requestId:
          type:
            - string
            - "null"
security:
  - bearerAuth: []

This example is small enough to walk through without pasting pages of YAML.

Configure the Python Generator

Create a generator configuration file and make the build system choice explicit:

{
  "packageName": "python_dispatch_sdk",
  "projectName": "pythn-dispatch-sdk",
  "packageVersion": "1.0.0",
  "packageUrl": "https://example.com/dispatch-sdk",
  "description": "Python SDK for the Dispatch API.",
  "author": "The Authors",
  "authorEmail": "[email protected]",
  "license": "MIT",
  "library": "httpx",
  "buildSystem": "hatchling",
  "hideGenerationTimestamp": true
}

In that configuration, these fields matter most:

  • packageName: the import name, so use snake_case
  • projectName: the distribution name, so use hyphens
  • library: httpx: the default I'd pick today unless you need urllib3 (see Requests vs. HTTPX)
  • buildSystem: hatchling: keeps the generated pyproject.toml aligned with current Python packaging practice
  • hideGenerationTimestamp: true: removes a useless source of churn from regenerated files

Don't flip disallowAdditionalPropertiesIfNotPresent without reading the docs first. OpenAPI Generator's Python generator docs call true the old behavior and false the spec-compliant one. It's a compatibility switch, not a free hardening flag.

Generate the SDK

With the spec and config in place, generate the client:

openapi-generator-cli generate \
  -i snippets/example-openapi-3.1-dispatch-api.yaml \
  -g python \
  -o ./client \
  -c snippets/openapi-generator-config-example.json

That produces a Python package, API classes, models, tests, and a pyproject.toml. The point is to treat that output as generated source, not something you hand-edit line by line.

When the upstream specification changes, rerun the same command and review the diff. If you're hand-editing generated files, you'll fight every regeneration.

Manage the Generated Package with uv

The generator hands you a pyproject.toml. Below I use uv to compile a lockfile, sync the environment, run tests, and build artifacts. Swap in pip or Poetry if that's what you already use.

Lock the generated dependencies:

cd client
uv pip compile pyproject.toml -o requirements.txt --upgrade

Create a local environment and sync it exactly to the lockfile:

uv venv
source .venv/bin/activate
uv pip sync requirements.txt

On Windows, activate with .venv\Scripts\activate instead.

Install the generated package plus test tools:

uv pip install -e . pytest pytest-cov
python -m pytest test -v

uv pip compile writes a lock you can review in requirements.txt. uv pip sync installs exactly that lock instead of whatever was already on the machine.

Build the Distribution

Once the smoke tests pass, build the package artifacts:

uv build --no-sources

That writes the source distribution and wheel into dist/.

Keep Your Repository Clean

Generated SDK repositories can collect a lot of junk unless you make the boundaries explicit.

At a minimum, ignore local environments, caches, build output, and generator metadata:

.venv/
__pycache__/
*.py[cod]
dist/
build/
.pytest_cache/
.coverage
htmlcov/
.openapi-generator/
git_push.sh

I'd also rewrite the generated README.md before publishing. The scaffold is fine for a first pass, but it's not documentation you should put your name on.

Optional: Template Overrides

If you need custom copyright or SPDX headers, use template overrides instead of editing generated files after the fact:

openapi-generator-cli author template -g python -o ./templates

From there, edit the copied Mustache templates and regenerate with -t ./templates. Keep the override directory small. The more templates you fork, the more work you'll inherit on generator upgrades.

Here is the generated workflow in one pass:

OpenAPI Python SDK Workflow Demo

Let the generator handle the scaffolding.

The specification quality, package boundaries, and how you ship releases are where the maintenance cost actually lives.

References

Making Sense of the Circle of Fifths

If the Circle of Fifths has always looked like one of those theory diagrams everyone swears is important but no one actually explains, you're not alone. The good news is that it's not really a chart about memorizing symbols. It's a map of how keys, chords, and musical motion relate to each other, and once you hear that on guitar, it starts feeling useful fast.

For a lot of players, the Circle of Fifths feels abstract because it gets introduced on paper instead of on the fretboard.

If my Decoding the Major and Minor Pentatonic Scales post was about understanding what's happening inside the pentatonic scale shapes, this one is about understanding why chords and keys pull the way they do around those scale shapes.

My goal here is to help you:

  • understand what the circle is actually showing you,
  • learn why moving by fifths sounds so strong in music,
  • connect nearby keys to nearby chord families, and
  • use the circle for progressions, songwriting, and practice.

Tip

The Circle of Fifths isn't mainly about memorizing key signatures. It's about seeing which keys are closely related, which chords want to lead to each other, and why certain progressions feel so natural.

What Is the Circle of Fifths?

At its core, the Circle of Fifths is just an ordered list of notes.

Start on C. Move up a perfect fifth and you get G. Move up another fifth and you get D.

Keep going and you get:

  • C
  • G
  • D
  • A
  • E
  • B
  • F#
  • Db
  • Ab
  • Eb
  • Bb
  • F
  • back to C

That's the circle.

If you move the other direction, you're moving by fourths, which lands on the same set of keys in reverse order.

"Why does that matter?"

Because music built from neighboring keys tends to share a lot of notes and chords. The closer two keys are on the circle, the more naturally they tend to connect.

Circle of Fifths Circle of Fifths

Guitar Translation

Think of the circle as a map of musical neighborhoods. Keys that sit next to each other are closely related. Keys across the circle feel more distant.

Why Fifths Sound So Strong

The interval of a fifth is one of the most stable and powerful sounds in music.

Guitar players already know this sound, even if they don't call it by name:

  • A basic power chord is built around the Root Note and the 5.
  • Power chords put the sound of the fifth under your fingers all over the neck.
  • A lot of strong chord movement in rock, blues, country, and pop follows fifth relationships!

The motion around the circle lines up with one of the most fundamental relationships in the instrument and in tonal music itself.

Tip

When you hear progressions like D - G - C or Em - Am - D - G, you're hearing the motion that lines up with the circle. And your ear recognizes that pull immediately.

Clockwise and Counterclockwise

One reason the circle matters is that it helps organize keys.

Moving clockwise:

  • goes up by fifths,
  • adds sharps, and
  • often feels like increasing harmonic brightness or tension

Moving counterclockwise:

  • goes up by fourths,
  • adds flats, and
  • often feels like a move toward warmer, flatter key areas

Here's a practical shorthand:

  • C has no sharps and no flats
  • one step clockwise, G, has 1 sharp
  • one more step, D, has 2 sharps
  • one step counterclockwise, F, has 1 flat
  • one more step, Bb, has 2 flats

Stop! You Don't Need to Memorize All of This at Once!

Start with just C, G, D, A on the sharp side and C, F, Bb on the flat side. That small map already explains a huge amount of real guitar music.

Relative Minor

The Circle of Fifths isn't only about major keys. Each major key has a Relative Minor that uses the same notes.

Examples:

  • C major and A minor
  • G major and E minor
  • D major and B minor

If that sounds familiar, it should. It's the same relationship we used in Decoding the Major and Minor Pentatonic Scales.

The circle helps you see those pairs as families:

  • major key on the outside
  • relative minor connected to the same note pool

That's useful when:

  • understanding songs that shift between major and minor moods,
  • finding related scales quickly, and
  • writing chord progressions that feel connected without feeling repetitive