Notes on The Unicorn Project by Gene Kim

The Unicorn Project Cover

Recently I listened to the audiobook of The Unicorn Project by Gene Kim. In the past, I’ve recommended its predecessor, The Phoenix Project, to multiple people, albeit reluctantly and tinged with embarrassed explanations about the format—the “business novel.” The author, Gene Kim, presents his thesis as a fictional narrative constructed solely to serve that end. The Ur-book in this genre is The Goal, which lives on more than one list of the best business books. (Both Phoenix and Unicorn specifically reference The Goal.)

I found The Unicorn Project to be slow to start but ultimately covering a lot of valuable ground. It’s most useful as a DevOps introduction for junior software engineers and for those who have only ever worked in dysfunctional organizations. It’s a great illustration of the power of continuous delivery and DevOps practices—continuous integration, developers working directly with the business, replicable builds, etc.—in a way that some people may find more powerful and digestible than a traditional exposition. Stories can be more powerful than facts.

But if you’re already aware of the concepts, you’ll spend a lot of time saying, “I know, I get it, dev environments need to be easy to set up and reproducible, I get it.” Some of the story sounds over the top—and it is—but having been on calls with big organizations trying to release software, it’s unfortunately very, very accurate.

The following are the concepts I found most useful:

  • The three horizons
  • The five ideals
  • Core and context
  • Complection

The three horizons

The three horizons is the concept that I find myself coming back to most. I am not sure how accurate it is as a top-level classification of businesses, but I have found it useful as a model.

Geoffery Moore created the three horizons to explain and categorize different types of businesses (or business units):

  • Horizon 1 is the legacy core business that generates the bulk of a company’s revenue and profitability. It’s predictable and is where process can and should be applied to drive incremental improvements and change. On the downside, it is where bureaucracy thrives and can resist needed change. It’s a gravity well that left unchecked will consume most of the company’s resources—after all, it’s where the bulk of revenue comes from. While you can reduce costs and maximize profitability in the short/medium term, any Horizon 1 business will eventually fade, since any profitable businesses invites competition that inevitably erodes profitability.
  • Horizon 2 is a smaller, emerging business that represents a possible future of the company. These businesses aren’t necessarily profitable—certainly not at first—but represent informed bets the company is making to find higher growth areas. The objective for a Horizon 2 business is to grow up into a Horizon 1 business.
  • Horizon 3 businesses are the true experiments. They require a culture of learning, and a willingness to prototype and iterate quickly to evaluate (1) market risk (does it solve a customer need), (2) technical risk (is it technically feasible), and (3) business model risk (is it a financially viable engine of growth). Many of these will fail. The most promising of these graduate into Horizion 2 efforts.

Horizon 2 and 3 are about learning-R&D, experiments, new markets, new customers, new businesses. Horizon 1 is about process and compliance. These will by nature clash.

I am by nature somewhat of a process person, so this model has been useful to remind me to try to ensure our team spends enough time in both Horizon 2 and Horizon 3. This requires (1) protecting people’s time, and (2) having a tolerance for certain teams not following established process for organizing our creation of software. But the risk of over-optimizing for the existing business is slow death.

The five ideals

You can argue the whole book is an elaborate mechanism for Kim to propose and illustrate his the five ideals for leading a technology business:

  • The First Ideal: Locality and Simplicity. In your software architecture, ideally introducing a new feature only requires changing a small amount of code in a single codebase, as opposed to many changes in many codebases. Complexity makes everything harder, and slows you down. The simpler your code, systems, and organization, the better off you will be. You don’t have locality and simplicity, making changes can be tied up in cascading, unknown dependencies. Or it requires the coordination of many different teams. This ideal directly leads to the goal of product-aligned, cross-functional teams that can deploy their own software to production.
  • The Second Ideal: Focus, Flow, and Joy. How does work feel? How it feels doesn’t matter for its own sake, but it is a leading indicator of the effectiveness and efficiency of how work gets done. Developers do their best work when they can focus on a single, difficult problem, and when their tools work with them rather than against them. Can developers work on one thing at a time, get fast feedback on their work, and quickly push their work to production? The way I think about this is that this ideal is about maximizing the amount of time a developer spends effectively thinking about and working on problems, and minimizing the amount of time lost on friction—waiting for others, waiting for builds, struggling against tooling, etc. This ideal also points to working in small batches—Theory of Constraints—both because it’s more efficient, and because it’s more enjoyable.
  • The Third Ideal: Improvement of Daily Work. In other words, continuous improvement. It’s essential to prioritize time to improve workflows and pay down technical debt, since incremental investments can pay off big over time.
  • The Fourth Ideal: Psychological Safety. Psychological safety is one of the top predictors of team performance. People need to feel safe to take risks and experiment and empowered to tell the boss something she doesn’t want to hear. A key example of this is a blameless postmortem. When there is a major customer-impacting outage, everyone should feel safe enough to raise their hand and say, “I might have caused this” and talk through tough issues with an eye for discovering the truth—not for assigning blame or ridicule. Without this, learning and improvement are impossible.
  • The Fifth Ideal: Customer Focus. Ensure that developers (and development teams) are focused outward towards the wider world, and not inward towards itself, turf, internal company concerns, or shiny technology for its own sake. A business needs to deliver value to customers to stay in business, and relentless focus on that leads to better decisions about technology and product. Ask yourself: does this actually matter to customers—i.e., are they willing to pay for it? One way to think about this is core vs. context, which I discuss below.

None of these ideas are novel to this book. But the categorization is useful for thinking about these concepts and seeing them in the world. That sounds like faint praise, but it’s not. These are essential concepts, succinctly captured.

Complection

Complect: To intertwine, or interweave, or to turn something simple into something complex.

The idea here is that software systems become complected over time, as different areas become more tightly coupled. It becomes harder (or impossible) to work on a single system because of unpredictable effects elsewhere. This is the inverse of encapsulation, although Kim uses the term “locality.”

… ‘Think of four strands of yarn that hang independently—that’s a simple system. Now take those same four strands of yarn and braid them together. Now you’ve complected them.’ Both configurations of yarn could fulfill the same engineering goal, but one is dramatically easier to change than the other. In the simple system, you can change one string independently without having to touch the others. Which is very good. However, in the complected system, when you want to make a change to one strand of yarn, you are forced to change the other three strands too. In fact, for many things you may want to do, you simply cannot, because everything is so knotted together.

Is this term useful? Maybe. I’ve always talked using words like “encapsulation” or “avoiding cross-dependencies” or “loosely-coupled” which has always gotten my points across.

Core and context

The purpose of a business is to produce a thing to sell to customers.

Core activities are those that customers are willing to pay for. Context is everything else, i.e., all of the activities that support the business but customers don’t care about and don’t create a competitive advantage—payroll systems, email, bug trackers, etc.

The point is to minimize the amount of money and effort going into context activities, and maximize the amount of time going into core. Your product should be world-class. Your email system simply needs to work. This is a great rule of thumb for deciding whether to build or buy.

One thing that The Unicorn Project does well is showing how, as time passes, what was once core can become context. Late in the book, the protagonists, forced to cut costs, struggle to decide what they should stop doing and outsource. Decades ago, they built a first-in-class ERP system, which proved a huge competitive advantage. But now there are off-the-shelf solutions that are just as good. It’s no longer a competitive advantage; it’s a distraction.

It’s important to periodically evaluate what you are working on to ensure you are spending the most time possible on investing in producing and improving the things that people actually want to buy.

Conclusion

The Unicorn Project is an interesting artifact of a book. The concepts are powerful and powerfully stated. But you have to wade through a lot of words to get to them, which I found frustrating at times. (I “read” it as an audiobook, which is probably the best way to tackle it). I think for many people, this is an ideal introduction to these DevOps concepts. The stories that a team tells about itself create a shared sense of purpose and reality—and this book can jumpstart that. You can steal these stories as a shorthand for the work that you need to do.

You sign up for things and get them done

You are about to violate a key leadership rule: “You sign up for things and get them done. Every single time.”

Leaders set the bar for what is and is not acceptable on their teams. They define this bar both overtly with the words they say, and more subtly with their actions. There are two scenarios that may play out when you’ve reached Meeting Blur: either you don’t change anything and do all of your work poorly, or you drop some of that work, which equates to a missed commitment. While the optics on both scenarios are bad, what is worse is that by choosing either course you signal to your team that these obvious bad outcomes are acceptable.

Seem harsh? Yeah, I’m a bit fired up because I think leaders vastly underestimate the impact of actions we rationalize as inconsequential. Let’s play it out once more. Thinking I am being responsible and helpful, I sign up for things. I do this repeatedly and sign up for too many things. Over time, I realize I’m overloaded, so I back out on some commitments. Where’s the flaw? Because I could not initially correctly assess how much work I could do, I’m signaling to my team that it’s okay to back out of commitments.

Michael Lopp, The Art of Leadership: Small Things, Done Well

Following through on personal commitments, 100%, all the time, is an aspect of leadership I try to hammer into any manager on my team. It’s also the one I’m most paranoid about falling short on myself.

Honoring and extracting reality

Another thing I learned along the way is how critical it is to have senior leadership support. And support in actions, not words. Senior leaders need to demonstrate their commitment to creating a learning organization. I will share the behaviors I try to model with my teams. I believe passionately in honoring and extracting reality. If I am a senior leader and my team doesn’t feel comfortable sharing risks, then I will never truly know reality. And, if I’m not genuinely curious and only show up when there’s a failure, then I am failing as a senior leader.

Courtney Kissler, VP Digital Platform Engineeering, Nike. From the forward of Accelerate: The Science of Lean Software and DevOps: Building and Scaling High Performing Technology Organizations.

What I’ve been reading

  • Emily St. John Mandel, The Glass Hotel. I haven’t read many novels yet this year, but this is my favorite. Subtle and engaging.
  • Nicole Forsgren and Jez Humble, Accelerate: The Science of Lean Software and DevOps: Building and Scaling High Performing Technology Organizations. I’m finding this to be really good. It’s famously hard to measure software developer productivity, but they make the best arguments I’ve seen for their measures.
  • Peter Ackroyd, Dominion: The History of England from the Battle of Waterloo to Victoria’s Diamond Jubiliee. The last in his series.
  • Diarmaid MacCulloch, Thomas Cranmer: A Life. This book is, frankly, a bit slow. But… I find this perfect reading for the end of the day. It’s a different world completely removed. It’s an activity that stretches out time, as opposed to an activity that accelerates time, like watching TV or even programming, where at the end the time has disappeared and I don’t know where it went. It’s also a reminder of just how much knowledge there is out there, and how you can keep digging down in an area and discovering more at seemingly the same amount of complexity and detail. This is a 600 page book, well-researched and well-written (taking years of work), about a historical figure that at the first approximation no one cares about or remembers today. I find this comforting and amazing.

A Markdown hyperlink TextExpander snippet that works in Ulysses

I write a lot of Markdown, and I’ve long had a few simple TextExpander snippets that easily let me insert a markdown link to the front-most window in either Safari or Chrome. They are very simple, for example:

tell application "Safari"
	set t to name of current tab of window 1
	set U to URL of current tab of window 1
end tell
"[" & t & "](" & U & ")"

But… I’ve been recently using Ulysses for writing, which is a Markdown editor, mostly and sort of. It turns out that you cannot simply cut and paste a markdown link into Ulysses and expect it to work. You have to use a special menu command “Paste from Markdown” instead, which will convert Markdown into its internal format.

This breaks TextExpander snippet expansion as well. The way snippet expansion usually works is that it places the expanded text in the clipboard, pastes, and then restores the old content. It’s that second step—pasting—that breaks in Ulysses:

Broken snippet expansion

I worked around this with the below:

-- Insert a markdown link for the active safari window

-- Get the info we need from Safari
tell application "Safari"
	set t to name of current tab of window 1
	set U to URL of current tab of window 1
end tell

set mLink to  "[" & t & "](" & U & ")"

-- Get the name of the fontmost active app
tell application "System Events"
	set appName to name of first application process whose frontmost is true
end tell

if appName is not equal to "UlyssesMac" then
	-- For most apps, just do the normal TextExpander thing and return the text to insert
	return mLink
else
	-- Ulysses doesn't like pasting in Markdown directly, and needs a special Ulysses command
	-- So instead, need to manipulate the clipboard and paste using that command
	set the clipboard to mLink

	delay .5 -- Delay is necessary for this to work
	tell application "System Events"
		-- Use the "Paste from Markdown" command
		keystroke "v" using {command down, option down}
	end tell
end if

The way this works is:

  1. It grabs the necessary information from the browser.
  2. Determines which application is active.
  3. If the current application is not Ulysses, do the normal thing and return the expanded link from the script. TextExpander takes it and pastes it in.
  4. If this is Ulysses, then we have to get creative and short-circuit the usual expansion process:
    1. Place the link text into the system clipboard
    2. Pause for half a second.
    3. Trigger the “Paste from Markdown” command in the menu.
    4. Return an empty string from the snippet itself.

So essentially, for Ulysses, the TextExpander snippet itself expands into nothing, but instead directly manipulates the clipboard and keyboard. It’s hacky but it works:

Working snippet expansion

I’ve shared my Markdown Links TextExpander group, which has a few more examples.

Feedback is information

I don’t like receiving feedback.

But… As an engineering manager I give people feedback all the time. I preach the gospel of feedback every day.

That doesn’t mean that I like it when feedback happens to me. It doesn’t mean that I don’t have an emotional reaction. Or that I don’t feel called out or suddenly vulnerable. Or that I always handle it as gracefully as I’d like.

What’s tough about feedback is that it targets what we are most invested in—ourselves—and for a brief moment it’s brutally clear how someone else sees us, and how it’s different than how we see ourselves and want to be seen. It feels uncomfortable. It feels personal. This is especially true for someone who cares deeply about their work and identifies strongly with it. Humans are not generally wired well to handle feedback gracefully.

But… feedback is important. There’s no way to know if you’re managing to keep your car on the road without the feedback of where the yellow lines are relative to you.

It’s best not to think of feedback as a mechanism for criticism, but as a mechanism of alignment and improvement and adjustment. Feedback is an opportunity to grow. Approach it with a growth mindset. You are receiving useful information that you would have not otherwise received. Deploy it to your benefit.

This is true even if you disagree with it, even if it is wrong, and even if it is rude or ill-intentioned! Any feedback is still a signal to how you are perceived, and new insight into the other person’s perspective. It always contains information.

I am coming to believe that a major differentiator between the good and the great is the ability to face brutal facts and see the world—and ourselves—as it is, and not how we wish it to be, all while maintaining an emotional equilibrium.

Our emotional reactions are often counterproductive to our ability to act and achieve in our own self interest. Feedback feels like an attack, so it’s natural to want to attack back, or withdraw inside ourselves. Neither of those are useful. It’s okay to be frustrated and okay to be angry. But take a breath, focus on your heart rate, and try to at least act calm and receive the feedback gracefully. Don’t push back, don’t be defensive. True calm will return, and then you can act.

So:

  1. Look at feedback as information, and as a mechanism for alignment and improvement. It is information that can be usefully deployed.
  2. Separate the emotional content of the feedback, and your emotional response from the informational content, even if it hurts, and even if you disagree.
  3. Act calm, become calm, act and grow.

Examining the structure of appearances

In ordinary life, we don’t spend very long looking at things or at the natural world or at people, but writers do. It is what literature has in common with painting, drawing, photography. You could say, following John Berger, that civilians merely see, while artists look. In an essay on drawing, Berger writes that ‘To draw is to look, examining the structure of experiences. A drawing of a tree shows, not a tree, but a tree being looked at. Whereas the sight of a tree is registered almost instantaneously, the examination of the sight of a tree (a tree being looked at) not only takes minutes or hours instead of a fraction of a second, it also involves, derives from, and refers back to, much previous experience of looking.’ Berger is saying two things, at least. First, that just as the artist takes pains – and many hours – to examine that tree, so the person who looks hard at the drawing, or reads a description of a tree on the page, learns how to take pains, too; learns how to change seeing into looking. Second, Berger seems to argue that every great drawing of a tree has a relation to every previous great drawing of a tree, since artists learn by both looking at the world and by looking at what other artists have done with the world. Our looking is always mediated by other representations of looking.

James Wood, Serious Noticing

Creating a zoom mic status icon in the menu bar

Like many people lately, I spend half my day in Zoom meetings. And also like many people, I’m working from house noisy from kids running around. So my usual mode of operation on a call is to mute myself except when actually speaking.

On the Mac, Zoom provides a built in global keyboard shortcut (Command-Shift-A) so I can toggle mute/unmute no matter what app I’m using. But that’s awkward to remember and type. And there’s no way to tell when your mic is hot if Zoom isn’t active in the foreground.

So I set out to fix these two things:

  1. A simpler way to toggle mute/unmute in zoom, from any application.
  2. A menu bar icon indicating my mic status, so I know if the mic is hot even if the zoom meeting window is in the background.

And I figured it out! It’s held together by shoestrings and good intentions, but it’s reliable. I’ve written up my notes here.

The steps I followed were:

  1. Set Zoom’s mute/unmute keyboard shortcut as a global keyboard shortcut.
  2. Map Map “eject” key to command-shift-A, command-f13 sequence using Karabiner Elements.
  3. Use Anybar to put a red/green dot in the menu bar indicating mic status, using an AppleScript to set its status.
  4. Write a script to update the menubar icon.

1. Set Zoom’s mute/unmute keyboard shortcut as a global keyboard shortcut.

This is done in the Zoom settings, seen here:

image alt Zoom preference screenshot

2. Map “eject” key to command-shift-A, command-f13 sequence using Karabiner Elements

Karabiner-Elements is a program that can change key mapping on the Mac. You can do really simple things, like map caps-lock to control, or more complex things, like what I’m about to describe.

I chose to map eject to two key sequences: command-shift-A, command-f13. The first simply executes the native Zoom keyboard shortcut for mute, as described above. The second one is more interesting, which I’ll describe in a later step. 

Complex modifications in Karabiner are defined via json.

{
  "title": "Zoom audio toggle modification",
  "rules": [
    {
        "description": "Eject key triggers two keyboard shortcuts",
        "manipulators": [
            {
                "from": {
                    "consumer_key_code": "eject"
                },
                "to": [
                    {
                        "key_code": "a",
                        "modifiers" : ["command", "shift"]
                    },
                    { 
                        "key_code": "f13",
                        "modifiers" : ["command"]
                    }
                ],
                "type": "basic"
            }
        ]
    }]
}

To enable the above:

  1. Place the json file in ~/.config/karabiner/assets/complex_modifications.
  2. Goto Karabiner -> Preferences -> Complex Modifications -> Add rule
  3. Enable your custom rule.

The documentation for complex modifications in Karabiner Elements is a bit hard to come by / nonexistent. A few resources I used:

(You can actually just add a simple modification in Karabiner elements if you don’t care about the status bar indicator, and stop here.)

3. Use Anybar to put a red/green dot in the menu bar indicating mic status, using an AppleScript to set its status.

Anybar is a simple Mac utility that puts a colored dot in the menu bar, which you can control using the command line or AppleScript. I installed it using Homebrew

4. Write a script to update the menubar icon

This is what the second keyboard shortcut sequence, command-f13 is for. When that is triggered, I use an Alfred hotkey to run the script, which reads the current state of audio in Zoom and updates the menu bar accordingly (There are different ways to do this, but I use Alfred to manage my keyboard shortcuts.):

image alt Alfred screenshot

image alt Alfred screenshot

-- Manipulate and get the status of Zoom mute/unmute, and show that status using the app AnyBar
-- Uses AnyBar to show zoom audio status (https://github.com/tonsky/AnyBar)
-- 
-- Takes one argument:
--   - toggle_zoom: Toggles the mute status and updates AnyBar red/green to reflect new status
--   - update_bar:  Grabs the current mute status and updates AnyBar
--
-- Anybar colors:
--   - Green: mic on
--   - Red: mic muted
--   - Hidden: No zoom meeting in progress
--

property btnTitleMute : "Mute audio"
property btnTitleUnMute : "Unmute audio"

on is_running(appName)
	tell application "System Events" to (name of processes) contains appName
end is_running

on set_indicator(indicatorColor)
	tell application "AnyBar" to set image name to indicatorColor
	-- display notification indicatorColor
end set_indicator

-- Return true if zoom meeting is active
on is_zoom_meeting_active()
	-- Is zoom even running?
	if not is_running("zoom.us") then
		return false
	end if
	
	tell application "System Events"
		tell application process "zoom.us"
			if exists (menu bar item "Meeting" of menu bar 1) then
				return true
			end if
			return false
		end tell
	end tell
end is_zoom_meeting_active

-- Return true/false if mic is active or not
on get_zoom_meeting_mic_on()
	if is_zoom_meeting_active() then
		tell application "System Events"
			tell application process "zoom.us"
				if exists (menu item btnTitleMute of menu 1 of menu bar item "Meeting" of menu bar 1) then
					return true
				end if
			end tell
		end tell
	end if
	return false
end get_zoom_meeting_mic_on


-- Update the status bar
on update_status_bar()
	if get_zoom_meeting_mic_on() then
		set_indicator("green")
	else
		set_indicator("red")
	end if
end update_status_bar

-- Toggle the audio state in zoom
on toggle_zoom_audio_state()
	if is_zoom_meeting_active() then
		tell application "System Events"
			tell application process "zoom.us"
				if my get_zoom_meeting_mic_on() then
					-- If unmuted, mute
					click menu item btnTitleMute of menu 1 of menu bar item "Meeting" of menu bar 1
					my set_indicator("red")
				else
					-- If unmuted, mute
					click menu item btnTitleUnMute of menu 1 of menu bar item "Meeting" of menu bar 1
					my set_indicator("green")
				end if
			end tell
		end tell
	end if
end toggle_zoom_audio_state

-- Entry point for script
on run argv
	-- If there is no zoom meeting, quit anybar (no indicator), and quit processing
	if not is_zoom_meeting_active() then
		tell application "AnyBar" to quit
		return
	end if
	
	if (count of argv) > 0 then
		set mode to item 1 of argv
		
		if mode is "toggle_zoom" then
			-- Change state of audio zoom (triggered from keybard shortcut)
			toggle_zoom_audio_state()
		else if mode is "update_bar" then
			-- Just update the menu bar (called via cron)
			update_status_bar()
		end if
	end if
end run

(Note that this script has the ability to set the mute status. I ended up not using that, because it’s significantly slower than doing it via native Zoom keyboard shortcut.)

This actually works really well. But what if I just click the “mute” button in the zoom app? This script won’t be run, and the status bar won’t be updated, and therefore wrong.

I use launchd to run the script every 10 seconds. This also serves to remove the status icon once a video chat has concluded.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>EnvironmentVariables</key>
	<dict>
		<key>PATH</key>
		<string>/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/usr/local/munki:/usr/local/sbin</string>
	</dict>
	<key>Label</key>
	<string>Periodically check zoom status</string>
	<key>LowPriorityIO</key>
	<true/>
	<key>ProgramArguments</key>
	<array>
		<string>/usr/bin/osascript</string>
		<string>/Users/james.sulak/src/zoom_toggle/zoom_audio.applescript</string>
		<string>update_bar</string>
	</array>
	<key>RunAtLoad</key>
	<true/>
	<key>StartInterval</key>
	<integer>10</integer>
</dict>
</plist>

Place this in ~/Library/LaunchAgents/.

The spirit of the staircase

My former writing teacher, the essayist and cartoonist Timothy Kreider, explained revision to me: “One of my favorite phrases is l’esprit d’escalier, ‘the spirit of the staircase’ — meaning that experience of realizing, too late, what the perfect thing to have said at the party, in a conversation or argument or flirtation would have been. Writing offers us one of the rare chances in life at a do-over: to get it right and say what we meant this time. To the extent writers are able to appear any smarter or wittier than readers, it’s only because they’ve cheated by taking so much time to think up what they meant to say and refining it over days or weeks or, yes, even years, until they’ve said it as clearly and elegantly as they can.”

It’s normal (and even desirable) that the structure of your work will change drastically between drafts; it’s a sign that you’re developing the piece as a whole, rather than just fixing the small problems.

From How to Edit Your Own Writing – The New York Times.

What I’ve been reading

I find myself more willing to buy/read physical books now that social distancing confines me to a small collection of rooms.