<?xml version="1.0" encoding="utf-8"?> 
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-CA">
    <generator uri="https://gohugo.io/" version="0.147.7">Hugo</generator><title type="html"><![CDATA[Jesse Builds Software]]></title>
    
        <subtitle type="html"><![CDATA[Jesse's thoughts on building quality software.]]></subtitle>
    
    
    
            <link href="https://jessemcdowell.ca/" rel="alternate" type="text/html" title="html" />
            <link href="https://jessemcdowell.ca/index.xml" rel="alternate" type="application/rss+xml" title="rss" />
            <link href="https://jessemcdowell.ca/atom.xml" rel="self" type="application/atom+xml" title="atom" />
    <updated>2026-05-02T22:58:40+00:00</updated>
    
    
    <author>
            <name>Jesse McDowell</name>
            </author>
    
        <id>https://jessemcdowell.ca/</id>
    
        
        <entry>
            <title type="html"><![CDATA[Using Restic for Backups on Windows]]></title>
            <link href="https://jessemcdowell.ca/2026/05/using-restic-for-backups-on-windows/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2025/04/My-Computer-Setup/?utm_source=atom_feed" rel="related" type="text/html" title="My Computer Setup" />
                <link href="https://jessemcdowell.ca/2025/03/Architectural-Decision-Using-Obsidian-For-Work-Notes/?utm_source=atom_feed" rel="related" type="text/html" title="Architectural Decision: Using Obsidian For Work Notes - 2025-01" />
                <link href="https://jessemcdowell.ca/2026/01/making-a-health-tracker-with-github-spark/?utm_source=atom_feed" rel="related" type="text/html" title="Making a Health Tracker With GitHub Spark" />
                <link href="https://jessemcdowell.ca/2026/01/copying-files-to-aws-ecs-fargate-instance-via-base64/?utm_source=atom_feed" rel="related" type="text/html" title="Copying Files to AWS ECS Fargate Instance via Base64" />
                <link href="https://jessemcdowell.ca/2025/11/Connect-to-Remote-Desktop-from-Outside-Your-Home-Securely-with-SSH/?utm_source=atom_feed" rel="related" type="text/html" title="Connect to Remote Desktop from Outside Your Home Securely with SSH" />
            
                <id>https://jessemcdowell.ca/2026/05/using-restic-for-backups-on-windows/</id>
            
            
            <published>2026-05-02T15:56:57-07:00</published>
            <updated>2026-05-02T15:57:30-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>I recently went about setting up a home server to replace a failed NAS appliance. This time, instead of proprietary software, I used Debian Linux and configured everything myself. Because it is storing a bunch of important files, an automated online backup system was an important part of the new build.</p>
<p>I had previously used <a href="https://www.arqbackup.com/">Arq</a> for backing up our home network. It is awfully powerful and easy to use. I especially appreciate that it lets you run backups to numerous cloud providers. Unfortunately, it only runs on Windows and Mac. I could keep using it by letting my main Windows computer back up the contents (by reading from a shared drive), but something running on the Linux server itself would be much better.</p>
<p>After a round of searching, I settled on <a href="https://restic.net/">Restic</a>. Restic is a lot like Arq, except that it runs on Windows, Mac, and Linux. It&rsquo;s also free and open source. Unfortunately, it is command-line only, so it takes a bit more work to set it up.</p>
<p>The Linux setup was easy, but it took a couple of tricks to get it working the way I wanted on Windows&hellip;</p>
<h2 id="setup-on-linux">Setup on Linux</h2>
<p>There is lots of good documentation for setting up Restic backups on Linux. This guide also shows setting up an S3 bucket in AWS: <a href="https://restic.readthedocs.io/en/stable/080_examples.html#setting-up-restic-with-amazon-s3">https://restic.readthedocs.io/en/stable/080_examples.html#setting-up-restic-with-amazon-s3</a></p>
<p>Because I use Debian for my home server, I used <a href="https://wiki.archlinux.org/title/Systemd/Timers">systemd Services and Timers</a> to run the backups on a schedule. It was easy to get going, and works great. I highly suggest using <a href="https://wiki.archlinux.org/title/Systemd-creds">systemd-creds</a> to keep your repository key encrypted in your systemd service files.</p>
<p>You should also make sure to run this command after installing Restic: <code>sudo restic self-update</code>. One of the weaknesses (and strengths) of Debian is that the package feeds are slow to take feature updates. The version <code>apt</code> installed still worked, but it was pretty old, and there were some important updates in that time.</p>
<h2 id="installing-on-windows">Installing on Windows</h2>
<p>Setting it up on Windows isn&rsquo;t very different from Linux. It might be intimidating for an average Windows user, but a power user should have no trouble. winget makes it particularly easy:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="n">winget</span> <span class="n">install</span> <span class="p">-</span><span class="o">-exact</span> <span class="p">-</span><span class="n">-id</span> <span class="n">restic</span><span class="p">.</span><span class="py">restic</span> <span class="p">-</span><span class="n">-scope</span> <span class="n">Machine</span> <span class="p">-</span><span class="n">-force</span>
</span></span></code></pre></div><h2 id="configuring-regular-backups-on-windows">Configuring Regular Backups on Windows</h2>
<p>I spent a bit of time experimenting and iterating before I settled on the following configuration. It&rsquo;s not a requirement for using Restic, but I like the way it works.</p>
<p>First of all, I like to keep the configuration and cache files in <code>ProgramData</code>. You can use these commands to create the directories required for the scripts below:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="c"># Create the directory structure:</span>
</span></span><span class="line"><span class="cl"><span class="nb">New-Item</span> <span class="n">-ItemType</span> <span class="n">Directory</span> <span class="n">C:</span><span class="p">\</span><span class="n">ProgramData</span><span class="p">\</span><span class="n">ResticBackup</span><span class="p">\</span><span class="n">Settings</span> <span class="n">-Force</span>
</span></span><span class="line"><span class="cl"><span class="nb">New-Item</span> <span class="n">-ItemType</span> <span class="n">Directory</span> <span class="n">C:</span><span class="p">\</span><span class="n">ProgramData</span><span class="p">\</span><span class="n">ResticBackup</span><span class="p">\</span><span class="n">Cache</span> <span class="n">-Force</span>
</span></span><span class="line"><span class="cl"><span class="nb">New-Item</span> <span class="n">-ItemType</span> <span class="n">Directory</span> <span class="n">C:</span><span class="p">\</span><span class="n">ProgramData</span><span class="p">\</span><span class="n">ResticBackup</span><span class="p">\</span><span class="n">Logs</span> <span class="n">-Force</span>
</span></span></code></pre></div><p>I also use text files for listing the includes and excludes. This way I can easily look the list up or adjust it without touching the scheduled task or PowerShell code.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="s2">&#34;C:\Users\</span><span class="nv">$Env:USERNAME</span><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">C:\ProgramData\ResticBackup\Settings
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;</span> <span class="p">|</span> <span class="nb">Out-File</span> <span class="n">C:</span><span class="p">\</span><span class="n">ProgramData</span><span class="p">\</span><span class="n">ResticBackup</span><span class="p">\</span><span class="n">Settings</span><span class="p">\</span><span class="n">Includes</span><span class="p">.</span><span class="py">txt</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="s2">&#34;# Code
</span></span></span><span class="line"><span class="cl"><span class="s2">node_modules
</span></span></span><span class="line"><span class="cl"><span class="s2">C:\Code\**\bin
</span></span></span><span class="line"><span class="cl"><span class="s2">C:\Code\**\obj
</span></span></span><span class="line"><span class="cl"><span class="s2">C:\Code\**\build
</span></span></span><span class="line"><span class="cl"><span class="s2">.vscode
</span></span></span><span class="line"><span class="cl"><span class="s2">.nuget\packages
</span></span></span><span class="line"><span class="cl"><span class="s2">.idea\libraries
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2"># User directories
</span></span></span><span class="line"><span class="cl"><span class="s2">C:\Users\**\Temp
</span></span></span><span class="line"><span class="cl"><span class="s2">C:\Users\**\Google\Chrome
</span></span></span><span class="line"><span class="cl"><span class="s2">C:\Users\**\INetCache
</span></span></span><span class="line"><span class="cl"><span class="s2">C:\Users\**\MicrosoftEdge\Cache
</span></span></span><span class="line"><span class="cl"><span class="s2">C:\Users\**\Mozilla\Firefox\**\cache*
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;</span> <span class="p">|</span> <span class="nb">Out-File</span> <span class="n">C:</span><span class="p">\</span><span class="n">ProgramData</span><span class="p">\</span><span class="n">ResticBackup</span><span class="p">\</span><span class="n">Settings</span><span class="p">\</span><span class="n">Excludes</span><span class="p">.</span><span class="py">txt</span>
</span></span></code></pre></div><p>This is the PowerShell script that runs the backup. Write the contents to <code>C:\ProgramData\ResticBackup\Settings\Invoke-ResticBackup.ps1</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="c">#</span><span class="k">Requires</span><span class="w"> </span><span class="kd">-Version</span><span class="na"> 7</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="k">param</span>
</span></span><span class="line"><span class="cl"><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="no">Switch</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$WhatIf</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">Set-StrictMode</span> <span class="n">-Version</span> <span class="mf">3.0</span>
</span></span><span class="line"><span class="cl"><span class="nv">$ErrorActionPreference</span> <span class="p">=</span> <span class="s1">&#39;Stop&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$LogPath</span> <span class="p">=</span> <span class="s2">&#34;C:\ProgramData\ResticBackup\Logs&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$LogFilePath</span> <span class="p">=</span> <span class="s2">&#34;</span><span class="nv">$LogPath</span><span class="s2">\</span><span class="p">$(</span><span class="nb">Get-Date</span> <span class="n">-Format</span> <span class="s1">&#39;yyyy-MM-dd_HH-mm-ss&#39;</span><span class="p">)</span><span class="s2">_Backup.txt&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">Start-Transcript</span> <span class="n">-Path</span> <span class="s2">&#34;</span><span class="nv">$logFilePath</span><span class="s2">&#34;</span> <span class="n">-UseMinimalHeader</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$ResticRepositoryKeyFile</span> <span class="p">=</span> <span class="s2">&#34;C:\ProgramData\ResticBackup\Settings\ResticRepositoryKey.</span><span class="nv">$Env:USERNAME</span><span class="s2">.txt&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="o">-not</span> <span class="p">(</span><span class="nb">Test-Path</span> <span class="nv">$ResticRepositoryKeyFile</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">throw</span> <span class="s2">&#34;repository key file not found for user: </span><span class="nv">$ResticRepositoryKeyFile</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$backupParams</span> <span class="p">=</span>
</span></span><span class="line"><span class="cl">    <span class="vm">$</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;--repo&#39;</span><span class="p">,</span> <span class="s1">&#39;s3:s3.amazonaws.com/&lt;your-bucket-name&gt;&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;-o&#39;</span><span class="p">,</span> <span class="s1">&#39;s3.region=&lt;your-region&gt;&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="s1">&#39;-o&#39;</span><span class="p">,</span> <span class="s1">&#39;s3.storage-class=ONEZONE_IA&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$Env:AWS_PROFILE</span> <span class="p">=</span> <span class="s1">&#39;restic&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$Env:AWS_SHARED_CREDENTIALS_FILE</span> <span class="p">=</span> <span class="s2">&#34;C:\Users\</span><span class="nv">$Env:USERNAME</span><span class="s2">\.aws\credentials&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$WhatIf</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$backupParams</span> <span class="p">+=</span> <span class="s1">&#39;--dry-run&#39;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$Env:RESTIC_PASSWORD_COMMAND</span> <span class="p">=</span> <span class="s2">&#34;pwsh -NoProfile -NonInteractive -Command &#39;(Get-Content </span><span class="se">`&#34;</span><span class="s2">C:\ProgramData\ResticBackup\Settings\ResticRepositoryKey.</span><span class="nv">$Env:USERNAME</span><span class="s2">.txt</span><span class="se">`&#34;</span><span class="s2"> -Raw).Trim() | ConvertTo-SecureString | ConvertFrom-SecureString -AsPlainText&#39;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$Env:RESTIC_PROGRESS_FPS</span> <span class="p">=</span> <span class="s1">&#39;0.016666&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">restic</span> <span class="n">backup</span> <span class="nv">@backupParams</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="p">-</span><span class="n">-files-from</span> <span class="s2">&#34;C:\ProgramData\ResticBackup\Settings\Includes.txt&#34;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="p">-</span><span class="n">-iexclude</span><span class="o">-file</span> <span class="s2">&#34;C:\ProgramData\ResticBackup\Settings\Excludes.txt&#34;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="p">-</span><span class="n">-cache-dir</span> <span class="s1">&#39;C:\ProgramData\ResticBackup\Cache&#39;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="p">-</span><span class="n">-use-fs-snapshot</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="p">-</span><span class="n">-no-scan</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="p">-</span><span class="n">-verbose</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="mf">2</span><span class="p">&gt;&amp;</span><span class="mf">1</span> <span class="p">|</span> <span class="nb">Out-String</span> <span class="n">-Stream</span> <span class="p">|</span> <span class="nb">Tee-Object</span> <span class="s2">&#34;</span><span class="nv">$LogPath</span><span class="s2">\restic-progress.txt&#34;</span>
</span></span></code></pre></div><p>You can use this command to schedule the task (running as user SYSTEM, twice daily):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$action</span> <span class="p">=</span> <span class="nb">New-ScheduledTaskAction</span> <span class="n">-Execute</span> <span class="s1">&#39;pwsh.exe&#39;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="n">-Argument</span> <span class="s2">&#34;-NonInteractive -NoProfile -File </span><span class="se">`&#34;</span><span class="s2">C:\ProgramData\ResticBackup\Settings\Invoke-ResticBackup.ps1</span><span class="se">`&#34;</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$trigger1</span> <span class="p">=</span> <span class="nb">New-ScheduledTaskTrigger</span> <span class="n">-Daily</span> <span class="n">-At</span> <span class="mf">7</span><span class="err">:</span><span class="n">10am</span>
</span></span><span class="line"><span class="cl"><span class="nv">$trigger2</span> <span class="p">=</span> <span class="nb">New-ScheduledTaskTrigger</span> <span class="n">-Daily</span> <span class="n">-At</span> <span class="mf">7</span><span class="err">:</span><span class="n">10pm</span>
</span></span><span class="line"><span class="cl"><span class="nv">$settings</span> <span class="p">=</span> <span class="nb">New-ScheduledTaskSettingsSet</span> <span class="n">-ThrottleLimit</span> <span class="mf">1</span> <span class="n">-MultipleInstances</span> <span class="n">IgnoreNew</span> <span class="n">-DontStopIfGoingOnBatteries</span>
</span></span><span class="line"><span class="cl"><span class="nv">$principal</span> <span class="p">=</span> <span class="nb">New-ScheduledTaskPrincipal</span> <span class="n">-UserId</span> <span class="s2">&#34;SYSTEM&#34;</span> <span class="n">-LogonType</span> <span class="n">ServiceAccount</span> <span class="n">-RunLevel</span> <span class="n">Highest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$task</span> <span class="p">=</span> <span class="nb">New-ScheduledTask</span> <span class="n">-Action</span> <span class="nv">$action</span> <span class="n">-Trigger</span> <span class="nv">$trigger1</span><span class="p">,</span><span class="nv">$trigger2</span> <span class="n">-Settings</span> <span class="nv">$settings</span> <span class="n">-Principal</span> <span class="nv">$principal</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">Register-ScheduledTask</span> <span class="n">-TaskPath</span> <span class="s1">&#39;Backup&#39;</span> <span class="n">-TaskName</span> <span class="s1">&#39;Restic S3 Backup&#39;</span> <span class="n">-InputObject</span> <span class="nv">$task</span> <span class="n">-Force</span>
</span></span></code></pre></div><h2 id="storing-passwords-securely-on-windows">Storing Passwords Securely on Windows</h2>
<p>If you were paying close attention above, you may have noticed the username variable in the repository key file. That&rsquo;s because the key is stored in an encrypted format (via <a href="https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.security/convertfrom-securestring"><code>ConvertFrom-SecureString</code></a>), and the encryption key it uses is different for each user of the computer.</p>
<p><code>ConvertFrom-SecureString</code> has a <code>-Key</code> parameter so that you can use the same encrypted string for multiple users. The problem with this approach is that you then have to protect the key used to encrypt the secret, and end up with the same problem again.</p>
<p>This mechanism is a little annoying, but it keeps the key safe. It would be a lot simpler to store your unencrypted repository key in a text file and let Restic use this instead. I don&rsquo;t recommend this, however, because anyone who can access that file could then read your backups.</p>
<p>I use this script to write my own copy of the encrypted repository key:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$ResticRepositoryKey</span> <span class="p">=</span> <span class="nb">Read-Host</span> <span class="n">-AsSecureString</span> <span class="n">-Prompt</span> <span class="s1">&#39;Restic Repository Key&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$ResticRepositoryKey</span> <span class="p">|</span> <span class="nb">ConvertFrom-SecureString</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="p">|</span> <span class="nb">Out-File</span> <span class="s2">&#34;C:\ProgramData\ResticBackup\Settings\ResticRepositoryKey.</span><span class="nv">$Env:USERNAME</span><span class="s2">.txt&#34;</span>
</span></span></code></pre></div><p>Saving the key so that the system user can decrypt it is a little more challenging. There is no way (that I&rsquo;ve found) to encrypt a secret for another user with <code>ConvertFrom-SecureString</code>, so you have to run the code that encrypts it as that user. Because you can&rsquo;t log in as SYSTEM directly, I use a scheduled task. The running task needs access to the key, however, so I temporarily write the unencrypted key to a file.</p>
<p>Write the following to <code>Save-EncryptedResticRepositoryKeyForCurrentUser.ps1</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="c">#</span><span class="k">Requires</span><span class="w"> </span><span class="kd">-Version</span><span class="na"> 7</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nb">Set-StrictMode</span> <span class="n">-Version</span> <span class="mf">3.0</span>
</span></span><span class="line"><span class="cl"><span class="nv">$ErrorActionPreference</span> <span class="p">=</span> <span class="s1">&#39;Stop&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$LogPath</span> <span class="p">=</span> <span class="s1">&#39;C:\ProgramData\ResticBackup\Logs&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$LogFilePath</span> <span class="p">=</span> <span class="s2">&#34;</span><span class="nv">$LogPath</span><span class="s2">\</span><span class="p">$(</span><span class="nb">Get-Date</span> <span class="n">-Format</span> <span class="s1">&#39;yyyy-MM-dd_HH-mm-ss&#39;</span><span class="p">)</span><span class="s2">_SaveEncryptedKey_</span><span class="nv">$Env:USERNAME</span><span class="s2">.txt&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">Start-Transcript</span> <span class="n">-Path</span> <span class="s2">&#34;</span><span class="nv">$logFilePath</span><span class="s2">&#34;</span> <span class="n">-UseMinimalHeader</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$ResticRepositoryKey</span> <span class="p">=</span> <span class="p">(</span><span class="nb">Get-Content</span> <span class="s1">&#39;C:\ProgramData\ResticBackup\Settings\UnencryptedRepositoryKey.txt&#39;</span> <span class="n">-Raw</span><span class="p">).</span><span class="py">Trim</span><span class="p">()</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="p">|</span> <span class="nb">ConvertTo-SecureString</span> <span class="n">-AsPlainText</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$ResticRepositoryKey</span> <span class="p">|</span> <span class="nb">ConvertFrom-SecureString</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="p">|</span> <span class="nb">Out-File</span> <span class="s2">&#34;C:\ProgramData\ResticBackup\Settings\ResticRepositoryKey.</span><span class="nv">$Env:USERNAME</span><span class="s2">.txt&#34;</span>
</span></span></code></pre></div><p>This script writes the unencrypted repository key to a file, then configures a scheduled task to encrypt it as the SYSTEM user. It then runs the scheduled task, waits a few seconds for it to finish, and (crucially) deletes the unencrypted key file.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$ResticRepositoryKey</span> <span class="p">=</span> <span class="nb">Read-Host</span> <span class="n">-AsSecureString</span> <span class="n">-Prompt</span> <span class="s1">&#39;Restic Repository Key&#39;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$ResticRepositoryKey</span> <span class="p">|</span> <span class="nb">ConvertFrom-SecureString</span> <span class="n">-AsPlainText</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="p">|</span> <span class="nb">Out-File</span> <span class="s1">&#39;C:\ProgramData\ResticBackup\Settings\UnencryptedRepositoryKey.txt&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$scriptPath</span> <span class="p">=</span> <span class="s2">&#34;C:\ProgramData\ResticBackup\Settings\Save-EncryptedResticRepositoryKeyForCurrentUser.ps1&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$action</span> <span class="p">=</span> <span class="nb">New-ScheduledTaskAction</span> <span class="n">-Execute</span> <span class="s1">&#39;pwsh.exe&#39;</span> <span class="p">`</span>
</span></span><span class="line"><span class="cl">    <span class="n">-Argument</span> <span class="s2">&#34;-NonInteractive -NoProfile -File </span><span class="se">`&#34;</span><span class="nv">$scriptPath</span><span class="se">`&#34;</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">$settings</span> <span class="p">=</span> <span class="nb">New-ScheduledTaskSettingsSet</span> <span class="n">-ThrottleLimit</span> <span class="mf">1</span> <span class="n">-MultipleInstances</span> <span class="n">IgnoreNew</span> <span class="n">-DontStopIfGoingOnBatteries</span>
</span></span><span class="line"><span class="cl"><span class="nv">$principal</span> <span class="p">=</span> <span class="nb">New-ScheduledTaskPrincipal</span> <span class="n">-UserId</span> <span class="s2">&#34;SYSTEM&#34;</span> <span class="n">-LogonType</span> <span class="n">ServiceAccount</span> <span class="n">-RunLevel</span> <span class="n">Highest</span>
</span></span><span class="line"><span class="cl"><span class="nv">$task</span> <span class="p">=</span> <span class="nb">New-ScheduledTask</span> <span class="n">-Action</span> <span class="nv">$action</span> <span class="n">-Settings</span> <span class="nv">$settings</span> <span class="n">-Principal</span> <span class="nv">$principal</span>
</span></span><span class="line"><span class="cl"><span class="nb">Register-ScheduledTask</span> <span class="n">-TaskPath</span> <span class="s1">&#39;Backup&#39;</span> <span class="n">-TaskName</span> <span class="s1">&#39;Encrypt Restic Repository Key for SYSTEM User&#39;</span> <span class="n">-InputObject</span> <span class="nv">$task</span> <span class="n">-Force</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">Start-ScheduledTask</span> <span class="n">-TaskPath</span> <span class="s1">&#39;Backup&#39;</span> <span class="n">-TaskName</span> <span class="s1">&#39;Encrypt Restic Repository Key for SYSTEM User&#39;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">Start-Sleep</span> <span class="n">-Seconds</span> <span class="mf">5</span>
</span></span><span class="line"><span class="cl"><span class="nb">Remove-Item</span> <span class="s1">&#39;C:\ProgramData\ResticBackup\Settings\UnencryptedRepositoryKey.txt&#39;</span>
</span></span></code></pre></div><h2 id="running-restic-manually">Running Restic Manually</h2>
<p>If you want to run any manual Restic commands (viewing backups, retrieving files, pruning history, etc.), and you don&rsquo;t feel like pasting your repository key every time, you can use this helpful one-liner:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-powershell" data-lang="powershell"><span class="line"><span class="cl"><span class="nv">$Env:RESTIC_PASSWORD_COMMAND</span> <span class="p">=</span> <span class="s2">&#34;pwsh -NoProfile -NonInteractive -Command &#39;(Get-Content </span><span class="se">`&#34;</span><span class="s2">C:\ProgramData\ResticBackup\Settings\ResticRepositoryKey.</span><span class="se">`$</span><span class="s2">Env:USERNAME.txt</span><span class="se">`&#34;</span><span class="s2"> -Raw).Trim() | ConvertTo-SecureString | ConvertFrom-SecureString -AsPlainText&#39;&#34;</span>
</span></span></code></pre></div><p>It allows Restic to decrypt and use your encrypted repository key every time it needs it. This is the same mechanism used in the backup script.</p>
<h2 id="regarding-s3-credentials">Regarding S3 credentials</h2>
<p>I am storing my backups in AWS S3, and using the AWS CLI&rsquo;s own configuration mechanism to store the AWS API key for me. It doesn&rsquo;t seem to be encrypted, which is not my favourite, but I&rsquo;ve decided that this is okay for me. The key is associated with a user that has only the permissions needed to write backups into the one S3 bucket I use for them.</p>
<p>If you want to be extra cautious, you could use a trick like I use for the repository key.</p>
<h2 id="other-considerations">Other Considerations</h2>
<p>For a fully-functional, self-sustaining backup system, there are a few more things you need to set up:</p>
<ul>
<li>regular pruning of old/unnecessary data</li>
<li>some kind of monitoring to tell you if the backups stop running</li>
<li>a routine/process to periodically check that you can actually pull files from your backup</li>
<li>if the backup destination supports it, some mechanism to lock/version files for a set amount of time will ensure that your backups can&rsquo;t be destroyed secretly (a common trick used in ransomware attacks)</li>
</ul>
<p>Would anyone be interested in me writing about these things? Comment below with any particular questions you have.</p>
<h2 id="results">Results</h2>
<p>I&rsquo;ve been using this to back up my personal laptop for over a year now, and it&rsquo;s been working great. It&rsquo;s fast and effective. It works even when I&rsquo;m travelling. And I don&rsquo;t notice a performance hit if it runs while I&rsquo;m working.</p>
<p>Arq was good to me, but I&rsquo;m glad I switched.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/practices" term="practices" label="practices" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/devops" term="devops" label="devops" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/reviews" term="reviews" label="reviews" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/powershell" term="powershell" label="powershell" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[User Experience for Background Processes]]></title>
            <link href="https://jessemcdowell.ca/2026/03/user-experience-for-background-processes/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2025/05/Disambiguating-Scalability/?utm_source=atom_feed" rel="related" type="text/html" title="Disambiguating Scalability" />
                <link href="https://jessemcdowell.ca/2024/11/Abandoning-Household-Organization/?utm_source=atom_feed" rel="related" type="text/html" title="Abandoning Household Organization" />
                <link href="https://jessemcdowell.ca/2024/08/Rocketbook-vs-Whiteboard/?utm_source=atom_feed" rel="related" type="text/html" title="Rocketbook vs Whiteboard" />
                <link href="https://jessemcdowell.ca/2024/06/Pragmatic-Potato-Tech-Stack/?utm_source=atom_feed" rel="related" type="text/html" title="The Pragmatic Potato Tech Stack" />
                <link href="https://jessemcdowell.ca/2024/05/Cross-Cutting-Concerns/?utm_source=atom_feed" rel="related" type="text/html" title="Cross-Cutting Concerns - Ten Approaches" />
            
                <id>https://jessemcdowell.ca/2026/03/user-experience-for-background-processes/</id>
            
            
            <published>2026-03-07T10:59:00-08:00</published>
            <updated>2026-05-02T15:57:30-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>Done well, background processes are a fantastic way to save time, and more importantly, save focus. Handing off monotonous chores so you can do something more challenging can be a true relief. Unfortunately, background processing isn&rsquo;t always implemented well. When it&rsquo;s done poorly, it can undermine trust in the system.</p>
<p>A lack of trust can have negative effects beyond just making people miserable. It can discourage exploration and discovery. It can force painful and inefficient workarounds. It can also cause data to be duplicated, forgotten, corrupted, or lost entirely.</p>
<p>In the age of autonomous agents this problem is bound to get worse, so let&rsquo;s talk about how to do it better.</p>
<div class="table-of-contents">
    <nav id="TableOfContents">
  <ul>
    <li><a href="#quick-version">Quick version</a></li>
    <li><a href="#long-version">Long Version</a></li>
    <li><a href="#1-be-transparent">1) Be Transparent</a>
      <ul>
        <li><a href="#11-it-should-be-obvious-when-starting-an-asynchronous-task">1.1) It should be obvious when starting an asynchronous task</a></li>
        <li><a href="#12-the-user-should-know-what-theyre-going-to-get-andor-it-should-be-easy-to-undo">1.2) The user should know what they&rsquo;re going to get, and/or it should be easy to undo</a></li>
      </ul>
    </li>
    <li><a href="#2-be-dependable">2) Be Dependable</a>
      <ul>
        <li><a href="#21-dont-drop-the-ball">2.1) Don&rsquo;t drop the ball</a></li>
        <li><a href="#22-fail-quickly-when-possible">2.2) Fail quickly when possible</a></li>
        <li><a href="#23-let-users-check-up-on-long-running-tasks">2.3) Let users check-up on long-running tasks</a></li>
      </ul>
    </li>
    <li><a href="#3-handle-issues-with-civility">3) Handle Issues with Civility</a>
      <ul>
        <li><a href="#31-the-intrusiveness-of-a-notification-should-be-proportional-to-its-urgency">3.1) The intrusiveness of a notification should be proportional to its urgency</a></li>
        <li><a href="#32-make-the-current-state-of-things-clear">3.2) Make the current state of things clear</a></li>
        <li><a href="#33-let-the-user-manage-failure-notifications">3.3) Let the user manage failure notifications</a></li>
        <li><a href="#4-be-considerate-of-multi-user-impacts">4) Be Considerate of Multi-user Impacts</a></li>
      </ul>
    </li>
    <li><a href="#5-be-autonomous-with-guard-rails">5) Be Autonomous with guard rails</a>
      <ul>
        <li><a href="#51-make-the-obvious-correction-when-possible">5.1) Make the obvious correction when possible</a></li>
        <li><a href="#52-allow-the-user-to-override-bad-judgements">5.2) Allow the user to override bad judgements</a></li>
        <li><a href="#53-guard-against-infinite-loops">5.3) Guard against infinite loops</a></li>
      </ul>
    </li>
    <li><a href="#conclusion">Conclusion</a></li>
  </ul>
</nav>
</div>

<h2 id="quick-version">Quick version</h2>
<p>An easy way to test your background interactions is to imagine that you are the background process, and your significant other asked you to do the task. Assuming you want to please your significant other, what would be the best service you could provide?</p>
<p>If my wife asked me to buy milk at the store, I would buy the milk, and put it in the fridge. I wouldn&rsquo;t interrupt her in the middle of something important to tell her the milk is in the fridge. I&rsquo;d just put it there, and she would expect it to be there later.</p>
<p>If the store was out of milk, however, I&rsquo;d probably let her know. Similarly, I wouldn&rsquo;t call her at work to tell her about it on the spot. I&rsquo;d save it until the next convenient opportunity to bring it up.</p>
<p>If there was some problem selecting the milk, like I wasn&rsquo;t sure what kind she wanted, or they were out of the kind she wanted, I might call her. I might just make a choice. I might skip it and wait until the next time I&rsquo;m at the store too. This will depend on the urgency of the request, and the cost of getting it wrong. Buying the wrong kind of milk is a bit wasteful, but if we were talking about ordering plane tickets, buying tickets to the wrong city would be a serious problem.</p>
<p>To bring this back to a software example, take a look at email clients. Email is asynchronous by nature, and a lot of effort has gone into refining the experience over the decades.</p>
<p>Aside: You may not think email applies to your situation, but according to <a href="https://en.wikipedia.org/wiki/Jamie_Zawinski#Zawinski's_Law">Zawinski&rsquo;s Law</a>, you&rsquo;ll be adding email to your application eventually.</p>
<p>If I push send on a new email, it&rsquo;s nice to get some immediate acknowledgement that the email will be sent. If everything goes normally, this is enough.</p>
<p>If the email system can&rsquo;t deliver the email, it will usually give some kind of feedback. It usually isn&rsquo;t an in-your-face warning. A little red dot on the outbox is probably enough. Some indication of what it means if a user hovers or clicks on it helps with discoverability. If the client can eventually send the email, the dot can silently disappear. A very busy person might never notice, and that&rsquo;s okay.</p>
<p>If something was wrong with the email such that it could never be delivered (ex: a non-existent email address), something a little harder to ignore makes sense. An in-your-face error message is still not great. A lot of email services send an email back to you explaining the situation. This is great because you&rsquo;re (likely) already checking your emails, and you can choose when you deal with it.</p>
<p>There are numerous failure modes in email, and if you haven&rsquo;t worked on email processing yourself, you probably aren&rsquo;t aware of most of how fraught a system it really is. This is a sign of a mature user experience. You haven&rsquo;t hit many edges because you&rsquo;ve been steered away from them subtly. When problems do occur, though, the feedback feels natural and proportionate.</p>
<h2 id="long-version">Long Version</h2>
<p>I now submit to you: my attempt at a list of suggestions to consider when designing background processes.</p>
<h2 id="1-be-transparent">1) Be Transparent</h2>
<p>Background process can be awfully opaque, even in some well established applications. When a user is requesting some work, it&rsquo;s important that they understand what they are doing. If they mess something up because the system did something they didn&rsquo;t anticipate, they will lose trust in the system.</p>
<h3 id="11-it-should-be-obvious-when-starting-an-asynchronous-task">1.1) It should be obvious when starting an asynchronous task</h3>
<p>Users should have some indication that the thing they&rsquo;re requesting will not be instantaneous. If it will be a long time before it gets done, this should be obvious too. There are a lot of great, subtle ways to hint at this. This should be enough for most cases.</p>
<h3 id="12-the-user-should-know-what-theyre-going-to-get-andor-it-should-be-easy-to-undo">1.2) The user should know what they&rsquo;re going to get, and/or it should be easy to undo</h3>
<p>If a button in an application is a &ldquo;heavy&rdquo; button, meaning that it does a bunch of stuff, or can&rsquo;t be easily reversed, I naturally feel nervous. Messing things up for myself is annoying, but messing them up for my team can be a lot more scary. This is especially true when I&rsquo;m not familiar with the system I&rsquo;m using. For any changes that aren&rsquo;t obvious, it&rsquo;s helpful to get a breakdown of exactly what will happen.</p>
<p>Alternatively, if the operation is quick and easily reversible, that might be good enough. It&rsquo;s sometimes a lot easier to show the result than to describe it. However, the longer the process takes, the harder it will be for the user to notice what changed. Try-and-undo works best for changes that are easily discoverable.</p>
<h2 id="2-be-dependable">2) Be Dependable</h2>
<p>A big part of handing work over to a computer is so that you don&rsquo;t have to keep track of it any more. This kind of delegation can be very freeing, but it only works if the application delivers reliable results.</p>
<h3 id="21-dont-drop-the-ball">2.1) Don&rsquo;t drop the ball</h3>
<p>When a user requests something, make sure you do it, or if you can&rsquo;t, make sure you tell them that you couldn&rsquo;t do it. This could take a lot of different forms, and some tuning to get it right, but it&rsquo;s an essential part of being useful.</p>
<h3 id="22-fail-quickly-when-possible">2.2) Fail quickly when possible</h3>
<p>If something is requested that will obviously never work, you should try to let the user know as soon as possible. Even better if you can notify them before accepting the task. If a user is notified right away, they&rsquo;ll still have the context in their head to fix it. Or if they are delegating a more important task, they will know they might need to change their plans. A great example of this is when sending an email with no recipients.</p>
<h3 id="23-let-users-check-up-on-long-running-tasks">2.3) Let users check-up on long-running tasks</h3>
<p>Allowing users to check on the things they requested can also improve trust. This is especially important for tasks that can take a long time, or are of critical importance. Sometimes people want to check up on what&rsquo;s going on, or check to see if something is broken.</p>
<h4 id="231-finding-completed-tasks">2.3.1) Finding completed tasks</h4>
<p>Similarly, being able to find work that has completed successfully can also be helpful. The sent folder in most email clients is a great example of this. If there is ever a question about something being done, looking it up is awfully convenient.</p>
<h4 id="232-allow-cancelling-work-that-hasnt-completed">2.3.2) Allow cancelling work that hasn&rsquo;t completed</h4>
<p>If you do have a way to see queued or incomplete work, and it&rsquo;s possible, allowing someone to cancel it is also nice. If they realize they forgot something, or made a mistake, being able to stop it and fix it early allows them to do better work.</p>
<p>This assumes that cancelling a task is possible. If it&rsquo;s not possible, then obviously you shouldn&rsquo;t make that option available.</p>
<h2 id="3-handle-issues-with-civility">3) Handle Issues with Civility</h2>
<p>Error handling is always tricky in software design. It&rsquo;s especially tricky with processes that don&rsquo;t have constant attention from the user. Getting this right is another essential part of maintaining trust.</p>
<h3 id="31-the-intrusiveness-of-a-notification-should-be-proportional-to-its-urgency">3.1) The intrusiveness of a notification should be proportional to its urgency</h3>
<p>If a user has had enough time to move on to another task, you should only interrupt them for feedback if it&rsquo;s likely they&rsquo;ll want to drop what they&rsquo;re doing to deal with it. Email is an example where it can probably wait. If someone is hailing a ride, and something has changed the driver&rsquo;s arrival time significantly, you should probably let them know urgently, and give them some choices about what happens next (such as cancelling or rescheduling).</p>
<h3 id="32-make-the-current-state-of-things-clear">3.2) Make the current state of things clear</h3>
<p>When a user is notified of a problem, they&rsquo;re going to have to decide what to do about it. If work was half-done, things were partially sent, or something needs to be manually cleaned up, they should know this. Expecting them to know, or to hunt around and figure it out themselves isn&rsquo;t very generous. If some kind of retry or automatic correction is going to happen asynchronously, it&rsquo;s essential that this be communicated as well.</p>
<p>Making atomic changes is nice when it&rsquo;s possible, because not doing anything is usually better than doing something half-way. Of course, it&rsquo;s not always feasible. If you do have this behaviour, make sure your users know about it. It will give them more confidence with the system.</p>
<p>Alternatively, it can be helpful to give the user an easy way to find the affected data / records. If they have to do manual cleanup, this will at least help them find it all.</p>
<h3 id="33-let-the-user-manage-failure-notifications">3.3) Let the user manage failure notifications</h3>
<p>Providing asynchronous failure notifications with some bit of visual feedback can be a great way to go for low-urgency issues. When you do, however, make sure that there is a way for the user to clear the notification. If you want a user to respond when they see the red dot, make sure you take it away when there is nothing to do. Otherwise, if they get accustomed to seeing the dot when there is nothing to fix, they may start to ignore it when it there really is something to do.</p>
<p>Similarly, for tasks that can be important but may not necessarily be urgent, once they&rsquo;ve been notified of a failure, they may want to keep it in a list somewhere so they are reminded to deal with it later.</p>
<p>Email is a great example of this done well. When a message can&rsquo;t be delivered, popping it into their inbox lets them keep it as long as they want. They can delete it if they respond to it right away, forward it to someone if they need to ask for help, or just leave it there forever.</p>
<h3 id="4-be-considerate-of-multi-user-impacts">4) Be Considerate of Multi-user Impacts</h3>
<p>It&rsquo;s always important to consider the impacts of actions in multi-user systems, but this is especially true with background processes.</p>
<p>If it&rsquo;s normal for tasks to run for a very long time (hours, days, etc), you might need a system that multiple users can use to monitor the work and respond to any failures. You don&rsquo;t want urgent issues to get ignored when someone has taken a lunch break or a vacation. If immediate action isn&rsquo;t necessary, you could also have a staged notification: only notify the broader team if the person who started the job hasn&rsquo;t dealt with it in some amount of time. This can get pretty complicated though, so use it carefully, and test it a lot.</p>
<p>If you do allow multiple users to respond to a failure, you might also need some way for them to coordinate who is responding to the issue.</p>
<p>When tasks are very expensive to run, or there are scarce resources to run them, you may also need some sort of prioritization or rate limiting system. However, these kinds of mechanisms can cause unpleasant side effects when systems are run beyond their capacity, so consider them carefully before adding them in.</p>
<h2 id="5-be-autonomous-with-guard-rails">5) Be Autonomous with guard rails</h2>
<p>A big benefit of running work asynchronously is that it frees up the user to do other things. The more you require interaction to get the task done, the less valuable this becomes. That being said, doing something undesirable in failure scenarios is often worse than doing nothing. So it&rsquo;s important to only implement autonomous error handling when the desired outcome is certain.</p>
<h3 id="51-make-the-obvious-correction-when-possible">5.1) Make the obvious correction when possible</h3>
<p>Sometimes the way to resolve an issue is obvious. An easy example is when someone is trying to delete something that no longer exists. Maybe you treat that as a success, or a success with a warning. The user requested that the thing no longer exist, and now it doesn&rsquo;t.</p>
<p>This assumes, of course, that there isn&rsquo;t some other implication to the delete being already requested by someone else. An inventory management system is an example where it might be a more serious problem.</p>
<h3 id="52-allow-the-user-to-override-bad-judgements">5.2) Allow the user to override bad judgements</h3>
<p>If you do make corrections on behalf of the user that could be questionable, make sure there is a way for them to find them and fix them if they&rsquo;re wrong. Replication conflicts is a common example of this. It&rsquo;s great when you can auto-merge, but sometimes it&rsquo;s wrong, and if there is no way for the user to find the conflicting changes, it may be hard for them to make it correct.</p>
<h3 id="53-guard-against-infinite-loops">5.3) Guard against infinite loops</h3>
<p>Automatic retry is a great solution to some kinds of problems, but you should always make sure there is some finite limit to how much you&rsquo;ll retry. Just like queue base systems need dead-letter queues, background tasks need some way to say that it&rsquo;s unlikely that a task will ever complete if the user doesn&rsquo;t intervene.</p>
<p>Also make sure that you don&rsquo;t retry immediately over and over again. If some system down the line is having a temporary hiccup, this just increases the stress on it when it may already be overwhelmed. Incremental back-off and circuit-breaker mechanisms can make a components drastically better citizens of their systems.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Background processes may be mostly invisible, but they become quite obvious when they behave poorly. Design your background interactions mindfully, because delighting users is what we should all be striving to do.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/design" term="design" label="design" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/user-experience" term="user-experience" label="user experience" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Making a Health Tracker With GitHub Spark]]></title>
            <link href="https://jessemcdowell.ca/2026/01/making-a-health-tracker-with-github-spark/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/11/This-Blog-Hexo-generated-static-site-hosted-on-GitHub-Pages/?utm_source=atom_feed" rel="related" type="text/html" title="This Blog: Hexo-generated static site hosted on GitHub Pages" />
                <link href="https://jessemcdowell.ca/2025/03/Architectural-Decision-Using-Obsidian-For-Work-Notes/?utm_source=atom_feed" rel="related" type="text/html" title="Architectural Decision: Using Obsidian For Work Notes - 2025-01" />
                <link href="https://jessemcdowell.ca/2025/01/Household-Organization-Hallway-Whiteboard/?utm_source=atom_feed" rel="related" type="text/html" title="Household Organization: Hallway Whiteboard" />
                <link href="https://jessemcdowell.ca/2024/08/Rocketbook-vs-Whiteboard/?utm_source=atom_feed" rel="related" type="text/html" title="Rocketbook vs Whiteboard" />
                <link href="https://jessemcdowell.ca/2024/06/Pragmatic-Potato-Tech-Stack/?utm_source=atom_feed" rel="related" type="text/html" title="The Pragmatic Potato Tech Stack" />
            
                <id>https://jessemcdowell.ca/2026/01/making-a-health-tracker-with-github-spark/</id>
            
            
            <published>2026-01-31T12:08:00-08:00</published>
            <updated>2026-05-02T15:57:30-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>Over the holiday break I decided to give <a href="https://github.com/features/spark">GitHub Spark</a> a try.</p>
<p>GitHub Spark is a new feature in GitHub Copilot that helps you build web applications quickly using AI. The Spark platform is based on React with Tailwind CSS. It also provides user authetication (via GitHub account), and user-specific data storage (via Azure).</p>
<p>At the time of this writing it seems like it&rsquo;s still an early version of the platform, so depending on when you&rsquo;re reading this, you may need to do your own research.</p>
<h2 id="the-project">The Project</h2>
<p>I&rsquo;ve been looking for a good health tracking app for years, but so far been disappointed with everything I&rsquo;ve found. They are either too simple, too limited, or too expensive. There are some free ones, but I haven&rsquo;t found one I like. Even if I did, the free ones are often making their money selling your health data. So I tried making my own with Spark.</p>
<p>The kind of app I&rsquo;m looking for will provide reminders for medications, track how much I&rsquo;ve taken and when, and record a few symptoms of interest. With this kind of data available, I can take hard numbers and graphs to my doctor appointments. It&rsquo;s definitely on the nerdy end of the data collection fad, but I&rsquo;ve found it helpful and insightful.</p>
<h2 id="short-version">Short Version</h2>
<p>It took a couple of four-hour sessions, but by the end of it, I had an app that I&rsquo;m still using today. I like it better than any product I&rsquo;ve seen. It has a few annoying bugs, some visual inconsistencies, and the layout could be a lot better. Honestly though, it looks and performs better than many cheap mobile apps I&rsquo;ve seen in the store.</p>
<p>The most interesting part for me is the Spark platform, which is both a strength and a weakness. It&rsquo;s comforting to have built-in authentication and storage mechanisms instead of depending on AI slop for these core functions. It also takes care of auto-update notifications. It makes it possible to get many simple apps off the ground quickly.</p>
<p>The problem, though, is that once you&rsquo;re in the platform, you&rsquo;re in the platform. Spark only makes web apps built on the Spark platform, and you don&rsquo;t have any other choices for where the data goes or how it gets synced. It also doesn&rsquo;t make mobile apps. It works fine as a pinned link on my phone, but this limits it&rsquo;s ability to be used for ad-supported consumer apps.</p>
<p><img src="healthtrack-mobile-screenshot.jpeg" alt="A screenshot of the health tracking mobile application this post is about."></p>
<h2 id="notifications">Notifications</h2>
<p>I&rsquo;ve not yet seen a single notification from my app, even though Spark has assured me that it works. It&rsquo;s also assured me that it&rsquo;s fixed it, twice now. Each time, it&rsquo;s told me that it knows for sure why it hasn&rsquo;t been working. So this hasn&rsquo;t inspired a lot of confidence.</p>
<p>This could be a limitation of the platform, or the platform it&rsquo;s running in, but it is a glaring defect. Even though it can&rsquo;t do one of the main things I want from the app, I still prefer it over others I&rsquo;ve used.</p>
<p>To be clear, I haven&rsquo;t spent much time debugging or diagnosing the issue myself. I might be able to get it to work if it was a significant issue for me, but so far, it hasn&rsquo;t made the top of the list.</p>
<h2 id="design-experience">Design Experience</h2>
<p>Spark generated a working app that met most of the requirements after just a few minutes. It wasn&rsquo;t great, but it was usable. Unfortunately, my experience went downhill from there.</p>
<p>The app had a few tabs for the different areas of functionality, but they all worked differently. To make them consistent, I had to keep asking Spark to change things. I found myself getting more and more explicit because I was developing a preference, and I wanted to make sure it took the best approach. This is not what it&rsquo;s like working with great developers, it&rsquo;s more like how the relationship goes when I work with sloppy ones.</p>
<p>It produced a clean, modern looking app, but it had a bunch of usability issues. Information density was terribly low, and several iterations trying to improve it resulted in little improvement. I ended up accepting it as it was and moving on to more important things.</p>
<p>One part of the experience I liked was the way the app is constantly running in a test mode while Spark is working on it, so you can keep playing around and testing. Unfortunately, the changes were often so slow that I found myself needed to document all the things I wanted to fix somewhere else. I couldn&rsquo;t just walk through all the little visual and usability issues and move on, I had to enter them one at a time and wait while it figured out what to change.</p>
<p>The long time between changes quickly became a real drag on the experience. Even simple changes like, &ldquo;Make the save buttons on every tab have the same padding and style&rdquo; could take 5 minutes. That&rsquo;s not long enough to do anything else, but way too long to maintain a state of flow.</p>
<h2 id="rapid-prototyping--evaluating-requirements">Rapid Prototyping &amp; Evaluating Requirements</h2>
<p>The work was ultimately very different from normal programming. I wasn&rsquo;t building an app as much as I was designing a product. It was easy to try things out, to get my hands on it, and see what works. But I had arms-length control in a way that I&rsquo;m not used to.</p>
<p>To be clear, Spark makes all the code available to you, and editable, but I&rsquo;m not sure how well it handles it if you start changing code directly. Except for very minor tweaks to wording, I didn&rsquo;t want to risk losing the AI experience to continue making more fundamental changes.</p>
<p>I think this project turned out well for me because of a few factors:</p>
<ul>
<li>I was building something I wanted to use myself. I have a very strong understanding of the problem and requirements. Every reaction I had was a real user reaction. This wouldn&rsquo;t be true if I was trying to make software for an industry I&rsquo;m not a part of.</li>
<li>I had already tested several similar applications, so I had seen a few different approaches to the problem. This allowed me to articulate what I wanted up front, taking the best parts from everything I&rsquo;d used.</li>
<li>I&rsquo;ve been designing and building user interfaces for a long time, so I know the common patterns and have a specific sense of what I like. Also, like any developer, I am particularly well practiced at describing what I don&rsquo;t like. In this case, it&rsquo;s actually a benefit.</li>
</ul>
<p>I don&rsquo;t think you need to be able to write code to build an app using Spark, but I think it would be hard to build a good one without practice studying them. Spark wasn&rsquo;t able to suggest approaches, or push back when I asked for things it couldn&rsquo;t do well, or do at all. I had to give it meticulously detailed descriptions and then inspect everything thoroughly.</p>
<p>That being said, the ability to build, test, and iterate helped me to figure out what worked and what didn&rsquo;t in an amazingly short amount of time.</p>
<p>Another non-trivial factor was how tiring it was to work with Spark. Between waiting and making decisions (which is surprisingly tiring), I would get tired really fast. I found it hard to motivate myself to go back for the second round to finish the project.</p>
<p>Looking at it from the outside, building a functional-enough app in just 8 hours of labour is an amazing feat. But even as someone who loves using software to help people, I don&rsquo;t feel any excitement about using it again.</p>
<p>Since those first two sessions I&rsquo;ve since gone back and tweaked some features and fixed some bugs. I think I made a pretty big improvement with just 30 minutes, and I didn&rsquo;t feel worn out afterward. Perhaps more short stints spaced out would be a better way to work with this sort of tool.</p>
<h2 id="next-steps">Next Steps</h2>
<p>It was an interesting experience, and helped me prove to myself that a home-made tracking app is something I would actually use. I don&rsquo;t think I could turn this one into a product though. I&rsquo;m not even comfortable releasing it freely as open source. It might be possible to hack my code out of Spark since it&rsquo;s mostly powered by React. It might even be possible to cram in a shim to change where the data goes. My developer instincts tell me that it wouldn&rsquo;t be worth the trouble it could cause.</p>
<p>I will probably make another attempt at building a similar app with AI, but I won&rsquo;t use the Spark platform. I&rsquo;d like to have a bit more control (or any at all) over how the data is stored, how users log in, and to be able to make real mobile applications with platform-native features.</p>
<p>I might use Spark again if I want to try some ideas out quickly, or show someone a prototype. It might be okay for some in-house tools too, but the user-centric data storage would be a dealbreaker for many common scenarios.</p>
<p>It&rsquo;ll be interesting to see how this technology evolves. I&rsquo;m sure the AIs will get faster and better over time. The Spark platform could add a native mobile runtime environment and expanded features for managing data. Until it does improve, though, I can&rsquo;t see myself using it for any serious projects.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/reviews" term="reviews" label="reviews" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/github" term="github" label="github" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Copying Files to AWS ECS Fargate Instance via Base64]]></title>
            <link href="https://jessemcdowell.ca/2026/01/copying-files-to-aws-ecs-fargate-instance-via-base64/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2025/11/Connect-to-Remote-Desktop-from-Outside-Your-Home-Securely-with-SSH/?utm_source=atom_feed" rel="related" type="text/html" title="Connect to Remote Desktop from Outside Your Home Securely with SSH" />
                <link href="https://jessemcdowell.ca/2025/05/Disambiguating-Scalability/?utm_source=atom_feed" rel="related" type="text/html" title="Disambiguating Scalability" />
                <link href="https://jessemcdowell.ca/2025/04/My-Computer-Setup/?utm_source=atom_feed" rel="related" type="text/html" title="My Computer Setup" />
                <link href="https://jessemcdowell.ca/2024/09/Using-WeeChat-on-Windows-11/?utm_source=atom_feed" rel="related" type="text/html" title="Using WeeChat and weechat-android on Windows 11" />
                <link href="https://jessemcdowell.ca/2024/03/The-True-Cost-of-Dependencies/?utm_source=atom_feed" rel="related" type="text/html" title="The True Cost of Dependencies" />
            
                <id>https://jessemcdowell.ca/2026/01/copying-files-to-aws-ecs-fargate-instance-via-base64/</id>
            
            
            <published>2026-01-04T19:57:57-07:00</published>
            <updated>2026-05-02T15:57:30-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>I recently needed to copy a few small text files into a container running in AWS Fargate. I assumed it would be an easy matter of copying it through the terminal session, but that is, unfortunately, not the case.</p>
<p>I was accessing the containers via the <code>aws ecs execute-command --interactive --comand &quot;bash&quot;</code> mechanism. If you&rsquo;re not familiar, this is a quick way to get a terminal inside an ECS container running in any cluster. It&rsquo;s especially helpful when you have containers that do not receive incoming traffic from the internet. It is almost like SSH, but it is not exactly SSH. And one of the limitations is that there is no equivalent to <code>scp</code>. There is also no ability to forward ports, so you can&rsquo;t use it to access SSH either.</p>
<p>You could assign a public IP to every container you run so that it&rsquo;s accessible via SSH. But, why would you? Not only does it cost more (though only a little bit), it is increasing the attack surface of your system.</p>
<p>The easiest way to get a couple of text files onto an ECS container is to copy and paste their contents into a file through the terminal window. This only works with plain text files, but for scripts this is usually sufficient. For example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat &gt;myscript.sql <span class="s">&lt;&lt;EOF
</span></span></span><span class="line"><span class="cl"><span class="s">paste your contents here
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span></code></pre></div><p>I tried this at first, but about 5 files in I started to wonder if I was still pasting the right contents into the right file. There is a kind of mindless haze the sets in when doing repetitive operations, and it invites mistakes. I&rsquo;ve also got a nurtured sense of laziness that pushes me to find new ways to solve problems.</p>
<p>After a bit of fiddling, I discovered that it&rsquo;s possible to compress a bunch of files into a giant base64 blob, which can easily be pasted into the terminal session, and unpacked inside it. This means you&rsquo;re only doing one paste operation, and binary files can be transmitted too! Here is how to do it:</p>
<p>First, you should connect to the destination machine. You can use a command like this with the AWS CLI. Note that you need to be authenticated and configured to use the correct AWS region first. You&rsquo;ll also need to look up the task id, which can also be done from the command line, but I find the AWS console faster.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">aws ecs execute-command --cluster mycluster --interactive --command <span class="s2">&#34;bash&#34;</span> --container mydatabase --task 6932e628a2ca4a15b53d9e25d9bd3193
</span></span></code></pre></div><p>The main reason to do this first is so that you only have to copy and paste the base64 blob once. Guess how many times I&rsquo;ve copied the blob, only to realize I need to use the clipboard for the task id, and then have to select and copy the blob again afterwards?</p>
<p>Next, from a different terminal window on the source machine, you use tar and base64 to convert the desired files into a blob of base64:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">cd</span> location/of/files
</span></span><span class="line"><span class="cl">tar -czf - *.sql <span class="p">|</span> base64
</span></span></code></pre></div><p>This will dump all the content to the terminal. Depending on your environment, you may be able to send it directly to a clipboard. I can&rsquo;t figure out how to do it in my WSL environment, so I just select it and copy it manually. It may help to use a <code>clear</code> command first so that you can find the start of the blob more easily.</p>
<p>Back on the destination machine, stick the base64 data into a temporary file:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat &gt;pasted.b64 <span class="s">&lt;&lt;EOF
</span></span></span><span class="line"><span class="cl"><span class="s">paste contents here
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span></code></pre></div><p>Now you can unpack the contents into the current directory with this command, and continue with your task.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">base64 -d pasted.b64 <span class="p">|</span> tar -xzf -
</span></span></code></pre></div><p>You could also use the same technique in reverse to move a bunch of text files from an ECS container back to your local machine. Or even between two different ECS containers.</p>
<p>Or maybe some nice person will write an <code>aws-ecs-scp</code> command to do all the hard work for us.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/devops" term="devops" label="devops" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/howto" term="howto" label="howto" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/linux" term="linux" label="linux" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Buying Robux for Kids (Roblox)]]></title>
            <link href="https://jessemcdowell.ca/2025/11/Buying-Robux-for-Kids/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/resources/game-credits-for-kids/?utm_source=atom_feed" rel="related" type="text/html" title="Game Credits for Kids" />
                <link href="https://jessemcdowell.ca/2025/09/Minecraft-Store-for-Kids-on-Playstation/?utm_source=atom_feed" rel="related" type="text/html" title="Minecraft Store for Kids on Playstation" />
                <link href="https://jessemcdowell.ca/2023/09/Case-of-the-Slow-Matchmaking-Routine/?utm_source=atom_feed" rel="related" type="text/html" title="Case of the Slow Matchmaking Routine" />
                <link href="https://jessemcdowell.ca/2023/05/Is-the-Bug-Fun/?utm_source=atom_feed" rel="related" type="text/html" title="Is the Bug Fun?" />
            
                <id>https://jessemcdowell.ca/2025/11/Buying-Robux-for-Kids/</id>
            
            
            <published>2025-11-27T09:06:48-08:00</published>
            <updated>2025-11-27T09:06:48-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>I recently wrote about <a href="/2025/09/Minecraft-Store-for-Kids-on-Playstation/">the system I use to purchase Minecoins for my son</a>. Not long after I got this figured out, my son informed me that all his friends were playing Roblox, and he would like to start using that now too. Fortunately, the process for Robux is a lot simpler than Minecoins and Minecraft Tokens.</p>
<p>Robux are a type of token that you can use to buy stuff in the various Roblox worlds. There are links to buy things everywhere, in your face, all around you, always. This is because Robux are a way for creators in the game to earn money. It&rsquo;s unfortunately difficult for younger children to understand exactly what the big numbers beside cool things mean, or what the impact of buying them is, so I suggest considering how you want to approach this as a parent before dumping your kids into it.</p>
<p>The default mechanism for buying Robux in a kids account is for you to put your credit card number into their account. Any time they buy Robux, they have to promise that they are, in fact, the adult owner of the card. I am skeptical that a wall of text will effectively dissuade an eager child from impulse-buying something cool, so I don&rsquo;t recommend using this method.</p>
<p>You can, however, easily purchase Robux gift cards in stores and online. I get mine on Amazon. There are a lot more choices for amounts to spend, and the price seems to be a touch better than buying them on the various gaming platforms.</p>
<p>I suggest the &ldquo;Roblox Digital Gift Code&rdquo;, which is fulfilled immediately via email. All you have to do is hand the code to your kid. Once they enter the code in their account, they can use the Robux immediately.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/parenting" term="parenting" label="parenting" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/video-games" term="video-games" label="video games" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Connect to Remote Desktop from Outside Your Home Securely with SSH]]></title>
            <link href="https://jessemcdowell.ca/2025/11/Connect-to-Remote-Desktop-from-Outside-Your-Home-Securely-with-SSH/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2025/05/Disambiguating-Scalability/?utm_source=atom_feed" rel="related" type="text/html" title="Disambiguating Scalability" />
                <link href="https://jessemcdowell.ca/2025/04/My-Computer-Setup/?utm_source=atom_feed" rel="related" type="text/html" title="My Computer Setup" />
                <link href="https://jessemcdowell.ca/2024/09/Using-WeeChat-on-Windows-11/?utm_source=atom_feed" rel="related" type="text/html" title="Using WeeChat and weechat-android on Windows 11" />
                <link href="https://jessemcdowell.ca/2024/03/The-True-Cost-of-Dependencies/?utm_source=atom_feed" rel="related" type="text/html" title="The True Cost of Dependencies" />
                <link href="https://jessemcdowell.ca/2023/12/Switching-from-Google-Podcasts-to-Spotify/?utm_source=atom_feed" rel="related" type="text/html" title="Switching from Google Podcasts to Spotify" />
            
                <id>https://jessemcdowell.ca/2025/11/Connect-to-Remote-Desktop-from-Outside-Your-Home-Securely-with-SSH/</id>
            
            
            <published>2025-11-02T09:52:15-08:00</published>
            <updated>2025-11-02T09:52:15-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>I have a Windows workstation sitting under my desk at home. This is where I do all of my professional development. I like this setup because it keeps my work completely separate from my personal stuff. If I&rsquo;m honest, though, my main reason for buying it was because I poured a glass of water over my development laptop. But now that I have it, I highly recommend it.</p>
<p>When I&rsquo;m working at home, I connect to this workstation using Remote Desktop (RDP). It&rsquo;s a fantastic protocol for accessing a Windows machine across a network. It&rsquo;s built in to the operating system, and it&rsquo;s generally pretty smooth. Most times I can&rsquo;t even tell that I&rsquo;m using it.</p>
<p>When I&rsquo;m working remotely, however, accessing that workstation under the desk is a lot harder.</p>
<p>You could share the RDP port on the workstation through your router, but I don&rsquo;t recommend this. Windows isn&rsquo;t really hardened to be on the open internet. I suggest instead exposing an SSH server, authenticating with a certificate (and not allowing any other methods), and using that to tunnel RDP through to your Windows workstation nestled safely within your firewall.</p>
<p>In this post I&rsquo;ll walk you through setting that up.</p>
<div class="table-of-contents">
    <nav id="TableOfContents">
  <ul>
    <li><a href="#quick-version">Quick version</a></li>
    <li><a href="#how-it-works">How it works</a></li>
    <li><a href="#setting-up-ssh">Setting up SSH</a></li>
    <li><a href="#creating-a-certificate-and-adding-it-to-a-user">Creating a certificate and adding it to a user</a></li>
    <li><a href="#exposing-ssh-to-the-internet">Exposing SSH to the Internet</a></li>
    <li><a href="#set-up-dynamic-dns-for-your-router">Set up dynamic DNS for your router</a></li>
    <li><a href="#connecting-to-ssh">Connecting to SSH</a></li>
    <li><a href="#enable-rdp-on-your-workstation">Enable RDP on your workstation</a></li>
    <li><a href="#connecting-rdp-to-localhost">Connecting RDP to localhost</a></li>
    <li><a href="#putting-it-all-together">Putting it all together</a></li>
  </ul>
</nav>
</div>

<h2 id="quick-version">Quick version</h2>
<p>You can use this command to set up the SSH connection with RDP being routed through it:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh user@fully_qualified_ddns_name -p ssh_port -i ~/.ssh/home_server -L 127.0.0.2:3389:windows_private_ip:3389
</span></span></code></pre></div><p>Substitute the values:</p>
<ul>
<li><code>user</code> - your username on your home server (where SSH is running)</li>
<li><code>fully_qualified_ddns_name</code> - the full ddns name of yours router (ex: <code>myhome.duckdns.org</code>)</li>
<li><code>ssh_port</code> - the public port you use to expose SSH to the internet (ex: 2299)</li>
<li><code>home_server</code> - the certificate file you set up for SSH authentication</li>
<li><code>windows_private_ip</code> - the private network ip (from the point of view of your home server) of the Windows workstation you want to connect to</li>
</ul>
<p>Once the connection is established, open the standard Remote Desktop client, and connect to: <code>127.0.0.2</code>. Then log in to the workstation with your normal credentials.</p>
<h2 id="how-it-works">How it works</h2>
<p><img src="ssh_tunnel.svg" alt="A diagram showing RDP being tunneled via SSH through to the workstation behind the router."></p>
<h2 id="setting-up-ssh">Setting up SSH</h2>
<p>I&rsquo;m running sshd (the service that hosts SSH) on a little home server I use to run a number of services in my home. I&rsquo;m using Debian, so I was able to install sshd easily using the built-in package manager. There are SSH servers for all the platforms, so you don&rsquo;t need to use Debian if you don&rsquo;t want to.</p>
<p>If you do use Debian however, this is how I&rsquo;ve got mine configured:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo groupadd server-access
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">CURRENT_USER</span><span class="o">=</span><span class="k">$(</span>whoami<span class="k">)</span>
</span></span><span class="line"><span class="cl">sudo usermod -aG server-access <span class="s2">&#34;</span><span class="nv">$CURRENT_USER</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">sudo <span class="nb">echo</span> <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">PermitRootLogin no
</span></span></span><span class="line"><span class="cl"><span class="s1">PubkeyAuthentication yes
</span></span></span><span class="line"><span class="cl"><span class="s1">PasswordAuthentication no
</span></span></span><span class="line"><span class="cl"><span class="s1">PermitEmptyPasswords no
</span></span></span><span class="line"><span class="cl"><span class="s1">MaxAuthTries 4
</span></span></span><span class="line"><span class="cl"><span class="s1">
</span></span></span><span class="line"><span class="cl"><span class="s1">AllowGroups server-access
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span> &gt; /etc/ssh/sshd_config.d/secure.conf
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">sudo service ssh restart
</span></span></code></pre></div><p>This creates a group called <code>server-access</code>, and adds the current user to it. Later on, we&rsquo;re allowing anyone in this group to access the sshd server. We are also requiring all users to authenticate with a certificate. You can add as many users to this group as you&rsquo;d like.</p>
<h2 id="creating-a-certificate-and-adding-it-to-a-user">Creating a certificate and adding it to a user</h2>
<p>Run this command to generate a certificate on the computer you&rsquo;re connecting from. Note that it should work in both Linux and modern versions of Windows. Probably on Macs too, but I haven&rsquo;t tried.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh-keygen -t rsa -f ~/.ssh/home_server
</span></span></code></pre></div><p>You can use any filename you like for the certificate, but remember what you choose because you&rsquo;ll need it later. If you like, you can also add a comment inside the file with <code>-C &quot;home server&quot;</code>. This can help keep them straight if you have multiple certificates.</p>
<p>This command generates two files: the private key (the name you chose), and the public key (the name you chose with <code>.pub</code> added to the end). Open the public key file and copy its contents. You can use <code>cat ~/.ssh/home_server.pub</code> to read the file in a console.</p>
<p>If you&rsquo;re using a Debian machine for your Home Server, you can find (or create) a file at <code>~/.ssh/authorized_keys</code>, and paste the whole contents of the <code>.pub</code> file as a new line. If you&rsquo;re setting this up for multiple users, make sure to create one of these files for each home directory. I suggest different keys for each user.</p>
<p>If you have multiple computers to connecting to your home network all for the same user, I also suggest using a different key pair for each connecting computer. This way, if one of your computers is lost or stolen, you can delete its key while continuing to use the others. If you&rsquo;re doing this, I also suggest adding comments to the end of public keys in the <code>authorized_keys</code> file so you can tell them all apart.</p>
<p>Now you can test your SSH authentication. Running this inside your home network is fine.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh user@home_server_ip -i ~/.ssh/home_server
</span></span></code></pre></div><p>If it works, you should be logged in to the home server.</p>
<h2 id="exposing-ssh-to-the-internet">Exposing SSH to the Internet</h2>
<p>You have to set up port forwarding on your router/firewall to allow SSH on your home server to be accessible from outside your home. Look to the documentation for your particular router if you need help with this.</p>
<p>The default port for SSH is 22. You should be forwarding to port 22 at the local ip address of your home server. I don&rsquo;t recommend, however, using port 22 as the public port on the outside of your router. There are a lot of robots crawling every IP address on the internet. Since port 22 is a commonly used port, it could be a target for automated attacks.</p>
<p>It&rsquo;s ideal to choose a port about 2000 as this is well past the range of the most commonly used protocols on the internet. You can refer to a listing of commonly used port numbers (such as <a href="https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers">https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers</a>) to make extra sure it&rsquo;s not something standard.</p>
<h2 id="set-up-dynamic-dns-for-your-router">Set up dynamic DNS for your router</h2>
<p>Depending on your Internet Provider (ISP), your home network may get an IP address that never changes, or you might have one that changes every day. Even if it happens infrequently, Dynamic DNS can make connecting to your home environment a lot simpler.</p>
<p>Many consumer routers have a Dynamic DNS (DDNS) option built in. If your router does, this will likely be the easiest method. If not, you can set up up a service like <a href="https://github.com/qdm12/ddns-updater">ddns-updater</a> on your home server. I personally run it inside a Podman container.</p>
<p>Once this is working, you&rsquo;ll now have a convenient name to reach your home network instead of needing to look up its public ip every time you want to connect.</p>
<h2 id="connecting-to-ssh">Connecting to SSH</h2>
<p>With all of the above configured, you should now be able to connect to SSH from outside your network. Depending on your router, you may not be able to test this from inside your home network.</p>
<p>When I want to test external connections, I enable hotspot mode on my phone and connect the client computer I&rsquo;m using to its wifi. If you don&rsquo;t want to do this, it could also be a good excuse to visit your local coffee shop.</p>
<p>You can use this command to connect to the SSH server:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh user@external_ip_address -p ssh_port -i ~/.ssh/home_server
</span></span></code></pre></div><h2 id="enable-rdp-on-your-workstation">Enable RDP on your workstation</h2>
<p>You need to enable Remote Desktop on the Windows workstation before you can use it. If you haven&rsquo;t done this already, open Settings and search for Remote Desktop.</p>
<p>You&rsquo;ll also need to know your username and password to log into Windows.</p>
<h2 id="connecting-rdp-to-localhost">Connecting RDP to localhost</h2>
<p>If your client computer happens to be a Windows computer, you have to use a trick to get around a weird limitation in the Microsoft Remote Desktop Client.</p>
<p>If you try to connect it to 127.0.0.1, you get this error:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Your computer could not connect to another console session on the remote computer because you already have a console session in progress.
</span></span></code></pre></div><p>Unfortunately, you really want to open sharing on localhost (as opposed to the wifi ip) so that only RDP connections from on your computer will be able to reach the SSH tunnel. This is especially important when using public networks like a coffee shop wifi.</p>
<p>One little-known trick is that all the IP addresses in the <code>127.0.0.1/8</code> block (anything starting with <code>127.</code>) are all valid localhost IP addresses. The check in the Microsoft RDP client only looks for <code>127.0.0.1</code>, so you can use <code>127.0.0.2</code> and get around it.</p>
<p>To forward the port through the SSH connection, you add this to the SSH command: <code>-L 127.0.0.2:3389:windows_private_ip:3389</code></p>
<p>The first 3389 is the port that&rsquo;s opened up on your local computer. You don&rsquo;t have to use 3389 here, but it&rsquo;s convenient. If you had multiple workstations inside your home network you can add <code>-L</code> multiple times with a different local port for each one.</p>
<p>The second 3389 is the port on the computer you&rsquo;re connecting to. This will pretty much always be 3389, so don&rsquo;t try to change it.</p>
<h2 id="putting-it-all-together">Putting it all together</h2>
<p>Refer to <a href="#Quick-version">Quick version</a> near the top of this post for all the steps together.</p>
<p>With everything set up, you should now be able to work remotely with ease and peace of mind. Maybe it&rsquo;s time to finally do some real work on your laptop from that coffee shop?</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/devops" term="devops" label="devops" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/howto" term="howto" label="howto" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Every Drink on the Phillips LatteGo 5500]]></title>
            <link href="https://jessemcdowell.ca/2025/10/Every-Drink-on-the-Phillips-LatteGo-5500/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2018/06/brewing-your-own-iced-tea/?utm_source=atom_feed" rel="related" type="text/html" title="Brewing Your Own Iced Tea" />
                <link href="https://jessemcdowell.ca/2024/08/Rocketbook-vs-Whiteboard/?utm_source=atom_feed" rel="related" type="text/html" title="Rocketbook vs Whiteboard" />
                <link href="https://jessemcdowell.ca/2024/01/My-Experience-with-the-Dvorak-Keyboard-Layout/?utm_source=atom_feed" rel="related" type="text/html" title="My Experience with the Dvorak Keyboard Layout" />
                <link href="https://jessemcdowell.ca/2014/06/why-i-only-drink-loose-tea/?utm_source=atom_feed" rel="related" type="text/html" title="Why I Only Drink Loose Tea" />
                <link href="https://jessemcdowell.ca/2011/01/working-together-and-having-fun/?utm_source=atom_feed" rel="related" type="text/html" title="Working Together and Having Fun" />
            
                <id>https://jessemcdowell.ca/2025/10/Every-Drink-on-the-Phillips-LatteGo-5500/</id>
            
            
            <published>2025-10-05T20:55:55-07:00</published>
            <updated>2025-10-05T20:55:55-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>At the end of last year we got ourselves a new coffee maker: the Philips LatteGo 5500. This is a lovely machine. It&rsquo;s a super-automatic espresso maker that grinds beans, pulls espresso shots, and steams milk all on its own. If you fill it with good beans, it can make delicious coffee drinks at home with little effort.</p>
<p>The 5500 can make a lot of drinks. I couldn&rsquo;t find a description of what the differences are, so I made my own list. It took months for me to try every drink. This was partially because I prefer iced drinks in hot weather, but also because I didn&rsquo;t want to waste good coffee.</p>
<h3 id="front-panel">Front Panel</h3>
<table>
  <thead>
      <tr>
          <th>Drink</th>
          <th>Description</th>
          <th>Espresso</th>
          <th>Steamed Milk</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Espresso</td>
          <td>Just espresso</td>
          <td>Normal</td>
          <td></td>
      </tr>
      <tr>
          <td>Coffee</td>
          <td>A long pour</td>
          <td>Long</td>
          <td></td>
      </tr>
      <tr>
          <td>Americano</td>
          <td>Espresso and hot water</td>
          <td>Normal</td>
          <td></td>
      </tr>
      <tr>
          <td>Cappuccino</td>
          <td>Steam milk followed by espresso. It might just by my imagination, but it seems that the milk is being steamed to be more airy than for than the Latte</td>
          <td>Normal</td>
          <td>Before Espresso</td>
      </tr>
      <tr>
          <td>Latte Macchiato</td>
          <td>Steamed milk followed by espresso</td>
          <td>Normal</td>
          <td>Before Espresso</td>
      </tr>
      <tr>
          <td>Iced Coffee</td>
          <td>A long pour, poured slower at a lower temperature</td>
          <td>Normal</td>
          <td></td>
      </tr>
      <tr>
          <td>Iced Latte</td>
          <td>Steamed milk into ice followed by espresso</td>
          <td>Normal</td>
          <td>Before Espresso</td>
      </tr>
  </tbody>
</table>
<h3 id="more-drinks--hot">More Drinks → Hot</h3>
<table>
  <thead>
      <tr>
          <th>Drink</th>
          <th>Description</th>
          <th>Espresso</th>
          <th>Steamed Milk</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Caffè Crema</td>
          <td>This seems the same as the Americano</td>
          <td>Normal</td>
          <td></td>
      </tr>
      <tr>
          <td>Ristretto</td>
          <td>A shorter (stronger) shot of espresso. It doesn&rsquo;t seem that much smaller than a normal espresso shot&hellip;</td>
          <td>Short</td>
          <td></td>
      </tr>
      <tr>
          <td>Espresso Lungo</td>
          <td>Slightly bigger than a normal espresso shot</td>
          <td>Long</td>
          <td></td>
      </tr>
      <tr>
          <td>Café au Lait</td>
          <td>A long pour followed by a small bit of steamed milk</td>
          <td>Long</td>
          <td>After Espresso</td>
      </tr>
      <tr>
          <td>Caffè Latte</td>
          <td>Espresso followed by steamed milk</td>
          <td>Normal</td>
          <td>After Espresso</td>
      </tr>
      <tr>
          <td>Flat White</td>
          <td>A little bit of steamed milk followed by two espresso shots</td>
          <td>Normal</td>
          <td>Before Espresso</td>
      </tr>
      <tr>
          <td>Travel Mug</td>
          <td>Two long pours (or more), with an adjustable temperature</td>
          <td>Long</td>
          <td></td>
      </tr>
      <tr>
          <td>Milk Froth</td>
          <td>Just steamed milk</td>
          <td>None</td>
          <td>Yes</td>
      </tr>
      <tr>
          <td>Hot Water</td>
          <td>Just hot water</td>
          <td>None</td>
          <td></td>
      </tr>
  </tbody>
</table>
<h3 id="more-drinks--iced">More Drinks → Iced</h3>
<table>
  <thead>
      <tr>
          <th>Drink</th>
          <th>Description</th>
          <th>Espresso</th>
          <th>Steamed Milk</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Iced Espresso</td>
          <td>Espresso, poured slower at a lower temperature</td>
          <td>Normal</td>
          <td></td>
      </tr>
      <tr>
          <td>Iced Americano</td>
          <td>Espresso and hot water</td>
          <td>Normal</td>
          <td></td>
      </tr>
      <tr>
          <td>Iced Caffe Crema</td>
          <td>Long pour, poured slower at a lower temperature</td>
          <td>Long</td>
          <td></td>
      </tr>
      <tr>
          <td>Iced Cappuccino</td>
          <td>Steamed milk followed by espresso</td>
          <td>Normal</td>
          <td>Before Espresso</td>
      </tr>
      <tr>
          <td>Iced Cafe au Lait</td>
          <td>Long pour followed by steamed milk</td>
          <td>Long</td>
          <td>After Espresso</td>
      </tr>
      <tr>
          <td>Iced Cafe Latte</td>
          <td>Espresso followed by a bit of steamed milk</td>
          <td>Normal</td>
          <td>After Espresso</td>
      </tr>
  </tbody>
</table>
<p>All of the drinks with Iced in the name ask you to put ice in the cup before you can adjust the settings and press start.</p>
<p>One of the most significant differences between some of the drink variations is when milk is steamed, either before or after the espresso is pulled. All of the drinks which steam milk after the espresso are in the More Drinks menu. This is a bit annoying for me, since I prefer the Caffè Latte to the Macchiato.</p>
<p>(I realize complaining about the order of espresso and milk is pretty snobby, even for me.)</p>
<p>It was a fun experiment. I was most surprised by the Iced Latte setting. I thought it would be weird steaming milk into ice, but it worked great. The foam was nice and cold by the time the espresso was done, but it still kept a fair bit of its froth. This is now my favourite iced drink.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/coffee" term="coffee" label="coffee" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/food" term="food" label="food" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/gadgets" term="gadgets" label="gadgets" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/recipes" term="recipes" label="recipes" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Minecraft Store for Kids on Playstation]]></title>
            <link href="https://jessemcdowell.ca/2025/09/Minecraft-Store-for-Kids-on-Playstation/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/09/Case-of-the-Slow-Matchmaking-Routine/?utm_source=atom_feed" rel="related" type="text/html" title="Case of the Slow Matchmaking Routine" />
                <link href="https://jessemcdowell.ca/2023/05/Is-the-Bug-Fun/?utm_source=atom_feed" rel="related" type="text/html" title="Is the Bug Fun?" />
            
                <id>https://jessemcdowell.ca/2025/09/Minecraft-Store-for-Kids-on-Playstation/</id>
            
            
            <published>2025-09-06T13:54:26-07:00</published>
            <updated>2025-09-06T13:54:26-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>My son is getting really into Minecraft right now. It&rsquo;s a game that exercises design, creativity, exploration, and even teaches some basic programming skills. The stock game has quite a lot to do and explore, but at some point it&rsquo;s natural to want more.</p>
<p>Minecraft has a built-in store with a wide assortment of extension, skins, and even whole worlds. Some of them are free, but most require some form of credit.</p>
<p>I spent a lot of time figuring out how to buy our first addon&hellip; It was far from straightforward, and I didn&rsquo;t find any good resources that explained the process from beginning to end. So that&rsquo;s what I&rsquo;m going to share with you here.</p>
<p>My son uses Minecraft Bedrock Edition, running on a PS5. It probably isn&rsquo;t so difficult if you use any other platform, but on the PlayStation, in particular, purchasing credits was a challenge.</p>
<h2 id="minecoins-and-minecraft-tokens">Minecoins and Minecraft Tokens</h2>
<p>Most platforms use Minecoins for purchases in the store. On PlayStation, however, you need to buy Minecraft Tokens. You can&rsquo;t mix-and-match, or use one on the other. A lot of games are like this. It can be pretty confusing the first time you run into this, and they don&rsquo;t go out of their way to tell you the difference when they ask for your credit card number.</p>
<p>As long as you buy credits from within whichever platform you&rsquo;re using, you may not even notice this oddity. But in the case of PlayStation, since it was impossible to buy Minecoins for my son&rsquo;s account, we had to find another solution.</p>
<h2 id="limitations-of-child-accounts-on-playstation">Limitations of Child Accounts on PlayStation</h2>
<p>All the gaming platforms have some kind of child mode protections, and PlayStation is no exception. This applies to launching games, but it especially applies to purchasing things in the PlayStation store.</p>
<p>Most of the things we purchase in the PlayStation store are straightforward. I purchase them with my adult account, and they become available for my son who uses the same console.</p>
<p>Any Minecraft Tokens I purchase, however, won&rsquo;t be available to him. The only way for him to use them is to buy them with his own account. Unfortunately, because the game is rated for 10 and older, and my son is not yet 10, he isn&rsquo;t able to access any Minecraft items in the PlayStation store.</p>
<p>For most things on the PlayStation, such as games and other content, I can control the level to which my son is restricted, and I can make exceptions for specific games. The store, however, appears to use his birthdate. There is no way that I&rsquo;ve found to get around it.</p>
<p>The error message is incredibly unhelpful too. It took a lot of searching just to figure out what was happening. This is the error message he gets:</p>
<blockquote>
<p>Content not found
This content can&rsquo;t be found.</p></blockquote>
<p>Some people online suggested setting up fake adult accounts for kids to use, but this is messy. I don&rsquo;t like to fool around with fake identities where money is involved. If there is ever a problem, it might be impossible to get help.</p>
<h2 id="sharing-addons-between-platforms">Sharing AddOns Between Platforms</h2>
<p>You can log in to a Microsoft account from Minecraft on any platform. This turns out to be very helpful for kids accounts. As long as the add-on is available, it will be in your library on any platform you happen to be using.</p>
<p>This means that you can purchase Minecoins and Add-ons on a different device (such as a phone), and then use them on the PlayStation. The store on Android is much easier for my son to use. He clicks purchase, a message pops up on my phone, and I can approve or decline it.</p>
<p>The downside to this approach is that you need to have two copies of Minecraft, and since it isn&rsquo;t free on either platform, this costs extra money. It was worth it for us. If you&rsquo;d rather not purchase a second copy, I suggest you stick to a non-PlayStation version of the game where it&rsquo;s easier to purchase Minecoins.</p>
<p>It took me hours and hours to purchase the first add-on for my son, but now that we&rsquo;ve figured this trick out, it only takes seconds.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/parenting" term="parenting" label="parenting" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/video-games" term="video-games" label="video games" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Household Organization: Home Assistant on Wall-Mounted Tablet]]></title>
            <link href="https://jessemcdowell.ca/2025/07/Household-Organization-Home-Assistant-on-Wall-Mounted-Tablet/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2025/01/Household-Organization-Hallway-Whiteboard/?utm_source=atom_feed" rel="related" type="text/html" title="Household Organization: Hallway Whiteboard" />
                <link href="https://jessemcdowell.ca/2024/11/Abandoning-Household-Organization/?utm_source=atom_feed" rel="related" type="text/html" title="Abandoning Household Organization" />
            
                <id>https://jessemcdowell.ca/2025/07/Household-Organization-Home-Assistant-on-Wall-Mounted-Tablet/</id>
            
            
            <published>2025-07-26T19:38:41-07:00</published>
            <updated>2025-07-26T19:38:41-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>I&rsquo;ve written before about <a href="https://jessemcdowell.ca/2025/01/Household-Organization-Hallway-Whiteboard/">our hallway whiteboard</a>. It&rsquo;s been helpful, and we still use it, but we are now transitioning to a digital display mounted on our refrigerator.</p>
<p><img src="kitchen-panel.png" alt="A picture of a tablet mounted on a refrigerator showing the weather, a calendar, and some other information."></p>
<p>The digital display has two major advantages over the whiteboard. We can access it from anywhere, even outside the house. And it can include dynamic elements that are updated automatically, such as the weather, our calendars, and various bits of information from our home automation system.</p>
<p>The most useful and transformative feature has been the addition of a digital shopping list. I can quickly add an item from the kitchen panel, or from my phone, and my wife can do the same. When either of us is at the store, we can see the list and check off items as we buy them. I can&rsquo;t tell you how many times we used to be missing a key ingredient because the paper list got lost or left at home. That doesn&rsquo;t happen any more.</p>
<p>We&rsquo;re using an older Android tablet for the display, which I don&rsquo;t recommend. The tablet is connected permanently to a USB charging cable. On <a href="https://youtu.be/tb9Qe4fjt24?t=360">the recommendation of Everything Smart Home</a>, I have it connected to a smart plug that toggles the power on and off to protect the battery. I&rsquo;m not sure how necessary this is with the tablet.</p>
<p>I&rsquo;m using the popular commercial <a href="https://www.fully-kiosk.com/">Fully Kiosk Browser</a> application on the tablet. It seems to be the best of class, and recommended my numerous people trying to do the same thing. I have found it to be a bit glitchy, but the application also reports that many features are not available because of the older Android version my tablet is running.</p>
<p>The dashboard itself is generated with <a href="https://www.home-assistant.io/">Home Assistant</a>, a free open-source application for home automation. I originally ran it on a Raspberry Pi I won in a contest, but I now it&rsquo;s running in a VM on a Proxmox server that runs a bunch of household services. It worked fine on the Pi, but the SD card got worn down. Home Assistant is notoriously hard on its storage medium.</p>
<p>My dashboard includes:</p>
<ul>
<li>Four family calendars in a tabular layout (one calendar for each of us, and a shared one for combined obligations), and a couple of web calendars for public holidays and school events</li>
<li>A link to the shopping list (with a count of items currently in it)</li>
<li>The weather: current conditions and forecasts for the next few days</li>
<li>A plot of indoor air temperatures over the last few hours</li>
<li>An air quality reading from our neighbourhood</li>
</ul>
<p>I&rsquo;d like to put more information on the panel, and adjust how some of it is displayed. Home Assistant is extensible, but I&rsquo;ve so far avoided writing my own extensions. Maybe one of these days I&rsquo;ll take a crack at it. The information we have now is already useful for a bunch of situations.</p>
<h2 id="previous-attempts">Previous Attempts</h2>
<p>Before Home Assistant, I used a home automation platform called <a href="https://hubitat.com/">Hubitat</a>. This was a great platform for home automation, though it is both closed source, and has fewer integrations available. I did find it to be more reliable for automation. But also, the dashboard, at the time that I tried it, was hard to configure and looked pretty terrible.</p>
<p>On the tablet itself I also tried a free open source application called WallPanel. It was not very reliable. I&rsquo;m not sure how much of this is because of the older Android version on my tablet.</p>
<h2 id="summary">Summary</h2>
<p>Having a panel mounted on the wall is a great way to keep the family organized, but it takes a fair bit of work to set it up. It is absolutely useful, but as of right now, I wouldn&rsquo;t recommend it for anyone but the most technically capable, or those with a lot of free time.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/home-assistant" term="home-assistant" label="home assistant" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/home-automation" term="home-automation" label="home automation" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/household-organization" term="household-organization" label="household organization" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[My Favourite Japanese Word]]></title>
            <link href="https://jessemcdowell.ca/2025/06/My-Favourite-Japanese-Word/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
            
                <id>https://jessemcdowell.ca/2025/06/My-Favourite-Japanese-Word/</id>
            
            
            <published>2025-06-28T23:38:08-07:00</published>
            <updated>2025-06-28T23:38:08-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>I spent a few years studying Japanese with my wife. It was challenging, interesting, and fun. We met a lot of cool people in our classes, some of whom are still good friends. I found it so interesting, in fact, that I have also developed a love of talking about it. Fun for me, and a nuisance for anyone around me.</p>
<p>There are a lot of things I could write about, but I&rsquo;m just going to share one Japanese word. It is my favourite word. If I could wave a magic wand and import it into English, I would do so with little thought.</p>
<p>The word is: Ganbaru</p>
<p>In Japanese, it can be spelled either as 頑張る, or phonetically as がんばる. You can look at <a href="https://jisho.org/search/ganbaru">the definition in Jisho.org</a> for more info or the pronunciation, but it should be pretty easy for an English speaker to pronounce it well enough from its romaji.</p>
<p>Ganbaru is a verb that has a bunch of meanings all bundled together:</p>
<ul>
<li>Good luck</li>
<li>You can do it</li>
<li>I believe in you</li>
<li>Don&rsquo;t give up</li>
<li>Hang in there</li>
<li>Keep going</li>
<li>Give it everything you have</li>
</ul>
<p>It is normally used in its imperative form: 頑張って (ganbatte). You can add a suffix: 頑張ってください (ganbatte kudasai), which is like adding please in English.</p>
<p>The phrase can be very positive and friendly, as you would expect, but it can also be considerably more snarky, depending on context. If someone asks you to help with something crazy, it can just as easily (and still fairly politely) tell them that they are on their own. Kind of like &ldquo;good luck with that&rdquo; in English.</p>
<p>Now that you know my favourite word, and my hopeless wish to bring it into English, you have the perfect response to give me&hellip;</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/japanese" term="japanese" label="japanese" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Disambiguating Scalability]]></title>
            <link href="https://jessemcdowell.ca/2025/05/Disambiguating-Scalability/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2024/03/The-True-Cost-of-Dependencies/?utm_source=atom_feed" rel="related" type="text/html" title="The True Cost of Dependencies" />
                <link href="https://jessemcdowell.ca/2013/06/reading-server-graphs-connected-users/?utm_source=atom_feed" rel="related" type="text/html" title="Reading Server Graphs: Connected Users" />
                <link href="https://jessemcdowell.ca/2024/06/Pragmatic-Potato-Tech-Stack/?utm_source=atom_feed" rel="related" type="text/html" title="The Pragmatic Potato Tech Stack" />
                <link href="https://jessemcdowell.ca/2024/05/Cross-Cutting-Concerns/?utm_source=atom_feed" rel="related" type="text/html" title="Cross-Cutting Concerns - Ten Approaches" />
                <link href="https://jessemcdowell.ca/2023/11/Using-Architectural-Decision-Records/?utm_source=atom_feed" rel="related" type="text/html" title="Using Architectural Decision Records" />
            
                <id>https://jessemcdowell.ca/2025/05/Disambiguating-Scalability/</id>
            
            
            <published>2025-05-09T19:33:22-07:00</published>
            <updated>2025-05-09T19:33:22-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>Although it was years ago, I still remember the conversation vividly. I was having my regular check-in with the CTO. It was also sunny outside, and the view from his office was particularly nice that day.</p>
<p>He asked me, &ldquo;Is our app scalable?&rdquo; After a moment of thought, I answered, &ldquo;Yes.&rdquo;</p>
<p>I spent the next few years remembering that conversation, wondering what would have happened if I had asked what he meant by scalable.</p>
<p>We used a cloud-based architecture with automated deployments. We could go a lot further scaling horizontally, and if that failed, we could easily generate new environments. If a lot of customers were to show up with cash in hand, it would be no problem for us to put them on the system.</p>
<p>That was, unfortunately, not what he meant. He was involved in a possible deal with a single giant customer, and he wanted to know if we could support them. That was a much harder thing for us to achieve.</p>
<p>He couldn&rsquo;t tell me why he was asking. I also don&rsquo;t think that one conversation caused any significant issues or miscalculations&hellip; but since then I am reminded of that moment any time someone uses the world scalability.</p>
<p>For that reason, I&rsquo;ve been cataloging the different ways that the word is being used. Here are the meanings I&rsquo;ve found:</p>
<h2 id="lots-of-users">Lots of users</h2>
<p>I think this is what most people mean when they say scalable. In the land of B2C (Business to Consumer), this is certainly a problem that businesses want to have.</p>
<p>Cloud architectures can handle this kind of growth pretty easily. You need a database that&rsquo;s twice as big? Sure, let me hit this button here and it&rsquo;ll be provisioned in a minutes. Need a fourth server? No problem, it&rsquo;ll be up in 5 minutes.</p>
<p>Compared to the olden times of dedicated rack space, this is magical. You can skip a lot of testing and planning when resources can be brought online in minutes instead of weeks.</p>
<p>It should be noted that the cloud providers charge a premium for this convenience, but if your income is doubling, it usually isn&rsquo;t a problem to double your infrastructure costs as well.</p>
<h2 id="rapid-addition-of-users">Rapid addition of users</h2>
<p>This is similar to lots of users, and similarly, cloud architectures can make it easier to handle, but there is a limit to how far you can push it.</p>
<p>For example, imagine working on a video game. It&rsquo;s only been out for a week, so your player counts are still in four-digits. Now imagine your company has placed an ad in the Superbowl, and it is a success. Within a few hours you are approaching 6-digit user counts, and more people are trying to sign up.</p>
<p>At a small company, this would be a make-or-break moment. Your cloud architecture could help, but adding 100x capacity is maybe not something you can do with a couple of button clicks in the cloud console.</p>
<p>You could (and probably should) provision some extra servers before running a major advertising campaign, but you can&rsquo;t go overboard there either. You have to pay for the resources you&rsquo;ve activated even if the paying customers don&rsquo;t show up.</p>
<p>This is a much more complicated problem to deal with, but there are a lot of things you can do. Here are a few off the top of my head:</p>
<ul>
<li>use serverless back-end technologies that scale automatically, such as document databases and cloud functions</li>
<li>put as much of the logic into the client as you can. You only need to scale the components in the back-end, so the smaller they are, the less work you have to do</li>
<li>lean on established third-parties for critical and sensitive stuff like payment processing and user authentication. Even if they take a bigger cut than roll-your-own alternatives, they can also reduce your risks.</li>
<li>use more static content and less dynamic content. CDNs cost less than custom servers, deliver bits faster for a global audience, and are designed to handle traffic spikes</li>
<li>use a smaller-scale test campaign before the big one to get a better idea of the potential interest in a larger campaign</li>
<li>build in mechanisms that allow you to temporarily lower the server load if things get bad. For example, you could use a configuration service to increase client polling intervals, or temporarily disable more computation-intensive features</li>
<li>add a waiting list at the starting page in case everything else fails</li>
</ul>
<h2 id="saving-money-when-traffic-drops">Saving money when traffic drops</h2>
<p>A lot of B2B (Business to Business) software experiences the significant majority of its load between 8am and 5pm from Monday to Friday. B2C stuff can be spread out a bit more, but it tends to have natural rhythms too.</p>
<p>If you&rsquo;re a fresh start-up, scaling up quickly is the priority. A few years into the journey, however, accountants start to ask about the cost per customer, and shrinking that number becomes increasingly important. Running idle servers in the middle of the night is wasteful, and you pay for it out of your profits.</p>
<p>Serverless cloud technologies can help here, as can environment orchestration technologies. Kubernetes is excellent at handling this, but it requires a significant investment of time and resources to use it.</p>
<p>Autoscaling is a simple concept, but there are lots of challenges to using it successfully. Orchestration code can have bugs like any software. Now imagine deploying a bug that adds an extra server every minute even though it&rsquo;s unnecessary. Now imagine deploying that bug on Friday afternoon before a long weekend. How much money will you have spent before you check the operations dashboard on Tuesday morning?</p>
<p>These systems need checks, alerts, testing, and staff available to deal with problems. If you only need two servers for your entire customer base, you shouldn&rsquo;t be worrying about this. If you&rsquo;re spending millions-per-year on cloud resources, it&rsquo;s probably worth hiring a team of IT experts to cut that down%.</p>
<h2 id="very-big-customers">Very big customers</h2>
<p>As I mentioned in the introduction, I discovered this particular meaning in a memorable way. This is more of a risk in the B2B space.</p>
<p>It can feel really wonderful when a huge company shows up and offers to drop some serious cash on the table. If things are tight, it can be hard for executives to refuse this kind of deal. It&rsquo;s hard even when times are good.</p>
<p>Unfortunately, large customers can be very different from small customers, and it can affect a lot more than your server architecture. For example:</p>
<ul>
<li>any entity in your data model that scales linearly with the customer size needs a UI/UX that supports it, such as:
<ul>
<li>drop-down lists with searching / filtering / paging capabilities</li>
<li>list pages with good filtering / paging capabilities</li>
<li>dashboard pages that operate smoothly when the quantity of data behind them jumps massively</li>
</ul>
</li>
<li>the queries powering routine reports might hit the database a lot harder when your biggest customer runs them</li>
<li>if the customer can&rsquo;t be naturally divided into pieces (ex, state, region, store), you need to make sure one environment can handle everything. If it can be split, you need to make sure some admin features work across multiple environments</li>
<li>large customers often have more complicated requirements around user permissions and security controls</li>
<li>if a customer makes up more than 50% of your company revenue, they can make it really difficult to refuse feature requests, even absurd ones</li>
</ul>
<p>It&rsquo;s hard to make an excellent product for a single audience, but it&rsquo;s considerably harder to make an excellent product for two. When you support a bunch of small customers and one big one, you effectively have two audiences. This will strain your product managers, designers, developers, testers, trainers, and so on.</p>
<p>If you&rsquo;re the kind of person who&rsquo;s reading my blog, however, I assume this kind of decision won&rsquo;t be yours to make&hellip; so if you end up in this position, you&rsquo;ll have to find a way to make it work anyway. Good luck. :)</p>
<h2 id="sustainable-practices">Sustainable practices</h2>
<p>As a technically focused person, this one rubbed me the wrong way at first. I&rsquo;ve had to accept that some people use the word scalable a bit more poetically.</p>
<p>That being said, it is still important to remember that the impacts of heavy utilization can be personal as much as they are technical. It doesn&rsquo;t matter how much money the shareholders are making, at some point your employees are going to start quitting when they can&rsquo;t sleep at night or take weekends off.</p>
<h2 id="summary">Summary</h2>
<p>Shared vocabulary is a powerful way to communicate big ideas efficiently, but it always has the danger of communicating different ideas just as quickly. This is why we should use examples, use case statements, and diagrams to make sure we are understood clearly. If the difference between one meaning and the other can be measured in person-years of labour, it&rsquo;s worth your time to make sure you&rsquo;re talking about the same thing!</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/architecture" term="architecture" label="architecture" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/design" term="design" label="design" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/devops" term="devops" label="devops" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/performance" term="performance" label="performance" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/quality" term="quality" label="quality" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[My Computer Setup]]></title>
            <link href="https://jessemcdowell.ca/2025/04/My-Computer-Setup/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2025/03/Architectural-Decision-Using-Obsidian-For-Work-Notes/?utm_source=atom_feed" rel="related" type="text/html" title="Architectural Decision: Using Obsidian For Work Notes - 2025-01" />
                <link href="https://jessemcdowell.ca/2024/09/Using-WeeChat-on-Windows-11/?utm_source=atom_feed" rel="related" type="text/html" title="Using WeeChat and weechat-android on Windows 11" />
                <link href="https://jessemcdowell.ca/2024/04/Writing-Code-for-Better-Reviews/?utm_source=atom_feed" rel="related" type="text/html" title="Writing Code for Better Reviews" />
                <link href="https://jessemcdowell.ca/2024/02/Being-Honest-and-Positive-at-Work/?utm_source=atom_feed" rel="related" type="text/html" title="Being Honest and Positive at Work" />
                <link href="https://jessemcdowell.ca/2024/01/Optimal-Code-Reviews/?utm_source=atom_feed" rel="related" type="text/html" title="Optimal Code Reviews" />
            
                <id>https://jessemcdowell.ca/2025/04/My-Computer-Setup/</id>
            
            
            <published>2025-04-05T17:42:45-07:00</published>
            <updated>2025-04-05T19:43:43-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>For various reasons, I&rsquo;ve had to set up new computers a few times lately. This is still a time-consuming task that I try to avoid, but as I&rsquo;ve been automating more and more of the process, it&rsquo;s getting faster and more consistent. This post is essentially a note to my future self, or any colleagues that want to use this as their baseline for their own setups.</p>
<p>Many of the tools I use are free and open source, but some of them are not. Make sure to review and purchase licences where you need to. I&rsquo;ve used most of these things fairly heavily, and am comfortable recommending all of them. It&rsquo;s also easy to remove anything you don&rsquo;t want to use.</p>
<p>I start with these commands because it makes the rest of the script easier to run. Windows Terminal is more comfortable for running lots of scripts, and <a href="https://bitwarden.com/">BitWarden</a> is my password manager of choice. Obsidian is the app where I keep my copy of these instructions, and Notepad++ is always helpful. Run this as administrator in whatever terminal application is available:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">winget install --id chocolatey.chocolatey --source winget
</span></span><span class="line"><span class="cl">winget install --id Microsoft.Powershell --source winget
</span></span><span class="line"><span class="cl">choco install -y microsoft-windows-terminal bitwarden obsidian notepadplusplus
</span></span></code></pre></div><p>My next step is to config Windows Terminal and pin it to the taskbar. I make PowerShell (the newer Core version) the default. I also make a clone that includes the &ldquo;Run as Administrator&rdquo; option. This makes it a little easier to run administrative scripts. I also make these the first two options in the list, but it currently requires editing the settings file.</p>
<p>After this, I can do most of the setup with a few commands. I&rsquo;ve used a group of PowerShell variables and expansion operators to group the apps I install. It&rsquo;s better to run one choco command with everything than to run a bunch of separate ones (less fussing while it does its business). It should be straigtforward to understand how these work if you want to add/remove items or entire sections</p>
<p>Next, and now in Windows Terminal, PowerShell, as administrator, I run:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-PowerShell" data-lang="PowerShell"><span class="line"><span class="cl"><span class="n">winget</span> <span class="n">install</span> <span class="n">-e</span> <span class="p">-</span><span class="n">-id</span> <span class="n">Google</span><span class="p">.</span><span class="py">Chrome</span>
</span></span><span class="line"><span class="cl"><span class="n">winget</span> <span class="n">install</span> <span class="n">-e</span> <span class="p">-</span><span class="n">-id</span> <span class="n">WinDirStat</span><span class="p">.</span><span class="py">WinDirStat</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">$hardware</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="s2">&#34;razer-synapse-3&#34;</span><span class="p">,</span> <span class="s2">&#34;logitech-options&#34;</span><span class="p">,</span> <span class="s2">&#34;samsung-magician&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">$utilities</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="s2">&#34;Firefox&#34;</span><span class="p">,</span> <span class="s2">&#34;restic&#34;</span><span class="p">,</span> <span class="s2">&#34;7zip&#34;</span><span class="p">,</span> <span class="s2">&#34;irfanview&#34;</span><span class="p">,</span> <span class="s2">&#34;irfanviewplugins&#34;</span><span class="p">,</span> <span class="s2">&#34;vlc&#34;</span><span class="p">,</span> <span class="s2">&#34;winscp&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">$editors</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="s2">&#34;scrivener&#34;</span><span class="p">,</span> <span class="s2">&#34;paint.net&#34;</span><span class="p">,</span> <span class="s2">&#34;audacity&#34;</span><span class="p">,</span> <span class="s2">&#34;inkscape&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">$entertainment</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="s2">&#34;spotify&#34;</span><span class="p">,</span> <span class="s2">&#34;steam&#34;</span><span class="p">,</span> <span class="s2">&#34;epicgameslauncher&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">$development</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="s2">&#34;git&#34;</span><span class="p">,</span> <span class="s2">&#34;git-lfs&#34;</span><span class="p">,</span> <span class="s2">&#34;dotnet-sdk-9&#34;</span><span class="p">,</span> <span class="s2">&#34;podman-cli&#34;</span><span class="p">,</span> <span class="s2">&#34;jetbrains-rider&#34;</span><span class="p">,</span> <span class="s2">&#34;webstorm&#34;</span><span class="p">,</span> <span class="s2">&#34;Python&#34;</span><span class="p">,</span> <span class="s2">&#34;dbeaver&#34;</span><span class="p">,</span> <span class="s2">&#34;beyondcompare&#34;</span><span class="p">,</span> <span class="s2">&#34;nvm&#34;</span><span class="p">,</span> <span class="s2">&#34;awscli&#34;</span><span class="p">,</span> <span class="s2">&#34;jq&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="nv">$misc</span> <span class="p">=</span> <span class="vm">@</span><span class="p">(</span><span class="s2">&#34;zoom&#34;</span><span class="p">,</span> <span class="s2">&#34;veracrypt&#34;</span><span class="p">,</span> <span class="s2">&#34;calibre&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">choco</span> <span class="n">install</span> <span class="n">-y</span> <span class="nv">@hardware</span> <span class="nv">@utilities</span> <span class="nv">@editors</span> <span class="nv">@entertainment</span> <span class="nv">@development</span> <span class="nv">@misc</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># show hidden files and file extensions</span>
</span></span><span class="line"><span class="cl"><span class="nb">Set-ItemProperty</span> <span class="n">-Path</span> <span class="s2">&#34;HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced&#34;</span> <span class="n">-Name</span> <span class="s2">&#34;Hidden&#34;</span> <span class="n">-Value</span> <span class="mf">1</span>
</span></span><span class="line"><span class="cl"><span class="nb">Set-ItemProperty</span> <span class="n">-Path</span> <span class="s2">&#34;HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced&#34;</span> <span class="n">-Name</span> <span class="s2">&#34;HideFileExt&#34;</span> <span class="n">-Value</span> <span class="mf">0</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># This disables the incredibly annoying Ctrl+Shift and Left Alt + Shift input language hotkeys</span>
</span></span><span class="line"><span class="cl"><span class="nb">New-ItemProperty</span> <span class="s2">&#34;HKCU:\Keyboard Layout\Toggle&#34;</span> <span class="n">-Name</span> <span class="s1">&#39;Language Hotkey&#39;</span> <span class="n">-Value</span> <span class="mf">3</span> <span class="n">-PropertyType</span> <span class="n">String</span> <span class="n">-Force</span>
</span></span><span class="line"><span class="cl"><span class="nb">New-ItemProperty</span> <span class="s2">&#34;HKCU:\Keyboard Layout\Toggle&#34;</span> <span class="n">-Name</span> <span class="s1">&#39;Hotkey&#39;</span> <span class="n">-Value</span> <span class="mf">3</span> <span class="n">-PropertyType</span> <span class="n">String</span> <span class="n">-Force</span>
</span></span><span class="line"><span class="cl"><span class="nb">New-ItemProperty</span> <span class="s2">&#34;HKCU:\Keyboard Layout\Toggle&#34;</span> <span class="n">-Name</span> <span class="s1">&#39;Layout Hotkey&#39;</span> <span class="n">-Value</span> <span class="mf">3</span> <span class="n">-PropertyType</span> <span class="n">String</span> <span class="n">-Force</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># Add Roaming binary folder (in OneDrive) to my Path variable. I keep a few utilities in this directory such as Procmon64, and a couple of PowerShell scripts I like to keep handy</span>
</span></span><span class="line"><span class="cl"><span class="nv">$UserPath</span> <span class="p">=</span> <span class="p">[</span><span class="no">System.Environment</span><span class="p">]::</span><span class="n">GetEnvironmentVariable</span><span class="p">(</span><span class="s1">&#39;PATH&#39;</span><span class="p">,</span> <span class="p">[</span><span class="no">System.EnvironmentVariableTarget</span><span class="p">]::</span><span class="n">User</span><span class="p">)</span> <span class="p">|</span> <span class="nb">Out-String</span> <span class="n">-NoNewline</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="p">(</span><span class="nv">$UserPath</span> <span class="o">-notmatch</span> <span class="s1">&#39;OneDrive\\Roaming\\WindowsBin&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="nv">$UserPath</span> <span class="o">-notmatch</span> <span class="s1">&#39;;&#39;</span><span class="p">)</span> <span class="p">{</span> <span class="nv">$UserPath</span> <span class="p">+=</span> <span class="s1">&#39;;&#39;</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="nv">$UserPath</span> <span class="p">+=</span> <span class="s1">&#39;%USERPROFILE%\OneDrive\Roaming\WindowsBin;&#39;</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span><span class="no">System.Environment</span><span class="p">]::</span><span class="n">SetEnvironmentVariable</span><span class="p">(</span><span class="s1">&#39;PATH&#39;</span><span class="p">,</span> <span class="nv">$UserPath</span><span class="p">,</span> <span class="p">[</span><span class="no">System.EnvironmentVariableTarget</span><span class="p">]::</span><span class="n">User</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># More Development stuff</span>
</span></span><span class="line"><span class="cl"><span class="n">wsl</span> <span class="p">-</span><span class="n">-install</span>
</span></span><span class="line"><span class="cl"><span class="n">dotnet</span> <span class="nb">dev-certs</span> <span class="n">https</span> <span class="p">-</span><span class="n">-trust</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">Install-Module</span> <span class="n">-Force</span> <span class="nb">posh-git</span>
</span></span><span class="line"><span class="cl"><span class="nb">Install-Module</span> <span class="n">-Name</span> <span class="n">AWS</span><span class="p">.</span><span class="py">Tools</span><span class="p">.</span><span class="py">Installer</span>
</span></span><span class="line"><span class="cl"><span class="nb">Install-AWSToolsModule</span> <span class="n">AWS</span><span class="p">.</span><span class="py">Tools</span><span class="p">.</span><span class="n">EC2</span><span class="p">,</span><span class="n">AWS</span><span class="p">.</span><span class="py">Tools</span><span class="p">.</span><span class="py">S3</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">git</span> <span class="n">config</span> <span class="p">-</span><span class="n">-global</span> <span class="n">user</span><span class="p">.</span><span class="py">email</span> <span class="n">REDACTED</span><span class="nv">@users</span><span class="p">.</span><span class="py">noreply</span><span class="p">.</span><span class="py">github</span><span class="p">.</span><span class="py">com</span>
</span></span><span class="line"><span class="cl"><span class="n">git</span> <span class="n">config</span> <span class="p">-</span><span class="n">-global</span> <span class="n">user</span><span class="p">.</span><span class="py">name</span> <span class="n">Jesse</span>
</span></span><span class="line"><span class="cl"><span class="n">git</span> <span class="n">config</span> <span class="p">-</span><span class="n">-global</span> <span class="n">diff</span><span class="p">.</span><span class="py">tool</span> <span class="n">bc</span>
</span></span><span class="line"><span class="cl"><span class="n">git</span> <span class="n">config</span> <span class="p">-</span><span class="n">-global</span> <span class="n">difftool</span><span class="p">.</span><span class="py">bc</span><span class="p">.</span><span class="py">path</span> <span class="s1">&#39;c:/Program Files/Beyond Compare 5/bcomp.exe&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">git</span> <span class="n">config</span> <span class="p">-</span><span class="n">-global</span> <span class="n">merge</span><span class="p">.</span><span class="py">tool</span> <span class="n">bc</span>
</span></span><span class="line"><span class="cl"><span class="n">git</span> <span class="n">config</span> <span class="p">-</span><span class="n">-global</span> <span class="n">mergetool</span><span class="p">.</span><span class="py">bc</span><span class="p">.</span><span class="py">path</span> <span class="s1">&#39;c:/Program Files/Beyond Compare 5/bcomp.exe&#39;</span>
</span></span><span class="line"><span class="cl"><span class="n">git</span> <span class="n">config</span> <span class="p">-</span><span class="n">-global</span> <span class="n">core</span><span class="p">.</span><span class="py">editor</span> <span class="s2">&#34;&#39;C:/Program Files/Notepad++/notepad++.exe&#39; -multiInst -notabbar -nosession -noPlugin&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c"># Add some aliases and adjust some settings in my PowerShell profile</span>
</span></span><span class="line"><span class="cl"><span class="nb">New-Item</span> <span class="n">-Type</span> <span class="n">Directory</span> <span class="n">-Path</span> <span class="p">(</span><span class="nb">Split-Path</span> <span class="nv">$PROFILE</span><span class="p">)</span> <span class="n">-Force</span>
</span></span><span class="line"><span class="cl"><span class="s2">&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">New-Alias npp &#39;C:\Program Files\Notepad++\notepad++.exe&#39;
</span></span></span><span class="line"><span class="cl"><span class="s2">New-Alias bc &#39;C:\Program Files\Beyond Compare 5\BComp.exe&#39;
</span></span></span><span class="line"><span class="cl"><span class="s2">New-Alias ll Get-ChildItem
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Import-Module posh-git
</span></span></span><span class="line"><span class="cl"><span class="s2"></span><span class="se">`$</span><span class="s2">GitPromptSettings.EnableFileStatus = </span><span class="se">`$</span><span class="s2">false
</span></span></span><span class="line"><span class="cl"><span class="s2">
</span></span></span><span class="line"><span class="cl"><span class="s2">Import-Module PSReadLine&#34;</span> <span class="p">|</span> <span class="nb">Out-File</span> <span class="nv">$PROFILE</span> <span class="n">-NoClobber</span>
</span></span></code></pre></div><p>After this, there are still a few manual steps I take. I could probably automate these, but I haven&rsquo;t yet:</p>
<ul>
<li>Ensure BitLocker is enabled, and back up the recovery keys in my password manager.</li>
<li>Set OneDrive to download all files locally</li>
<li>Adjust keyboard layout settings as I like them</li>
<li>Put a shortcut for <a href="https://www.taenarum.com/software/Borderline.html">Borderline</a> on the Task Bar. It is a little utility that moves and/or resizes any windows that are outside or across monitor boundaries. This is unfortunately common when running an external display and multiple scale factors.</li>
<li>Adjust Power &amp; battery settings:
<ul>
<li>Turn my screen off after: 10 minutes</li>
<li>When plugged in, make my device sleep: Never</li>
</ul>
</li>
<li>Restore the following files and directories:
<ul>
<li><code>$HOME\.ssh</code></li>
<li><code>$HOME\Downloads</code></li>
<li><code>$HOME\AppData\Roaming\Notepad++\shortcuts.xml</code></li>
<li><code>C:\Windows\System32\drivers\etc\hosts</code></li>
</ul>
</li>
<li>Set up my backup system and test that it&rsquo;s working</li>
<li>Set a reminder to clean up backups for any retired systems after a couple of months</li>
</ul>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/practices" term="practices" label="practices" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/resources" term="resources" label="resources" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/howto" term="howto" label="howto" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/powershell" term="powershell" label="powershell" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Architectural Decision: Using Obsidian For Work Notes - 2025-01]]></title>
            <link href="https://jessemcdowell.ca/2025/03/Architectural-Decision-Using-Obsidian-For-Work-Notes/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2024/06/Pragmatic-Potato-Tech-Stack/?utm_source=atom_feed" rel="related" type="text/html" title="The Pragmatic Potato Tech Stack" />
                <link href="https://jessemcdowell.ca/2023/11/Using-Architectural-Decision-Records/?utm_source=atom_feed" rel="related" type="text/html" title="Using Architectural Decision Records" />
                <link href="https://jessemcdowell.ca/2023/11/This-Blog-Hexo-generated-static-site-hosted-on-GitHub-Pages/?utm_source=atom_feed" rel="related" type="text/html" title="This Blog: Hexo-generated static site hosted on GitHub Pages" />
                <link href="https://jessemcdowell.ca/2023/08/Regarding-Test-Coverage-Targets/?utm_source=atom_feed" rel="related" type="text/html" title="Regarding Test Coverage Targets" />
                <link href="https://jessemcdowell.ca/2023/07/Horizontal-One-on-Ones-and-Talking-Practice/?utm_source=atom_feed" rel="related" type="text/html" title="Horizontal One-on-Ones and Talking Practice" />
            
                <id>https://jessemcdowell.ca/2025/03/Architectural-Decision-Using-Obsidian-For-Work-Notes/</id>
            
            
            <published>2025-03-02T13:59:08-08:00</published>
            <updated>2025-03-02T13:59:08-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>I have been feeling frustrated with my personal organization of late. I had a bit of time before starting a new project, so I put some time into improving my process. The post below is a simplified version of the notes I took while I looked for alternatives. I&rsquo;m sharing this as an example of how I do research, and as a more realistic example of <a href="/resources/architectural-report-template/">the architectural report structure I typically use</a>, which <a href="/2023/11/Using-Architectural-Decision-Records/">I&rsquo;ve written about before</a>.</p>
<p>According to my records, I spent 28 hours working on the decision. This breaks down to about 22 hours testing the eight finalists, 4 hours researching options, at a summary level, and 2 hours of planning. Of the time spent testing, I spent between 30 and 240 minutes each for 7 of the 8 finalists. I spent 8 hours testing Obsidian, though most of that was testing and improving the processes I ended up using.</p>
<p>I started this task thinking purely about about improving my working notes and personal task list, but as I dug further in, it became apparent that I could centralize many of the different organizational tools I use. That process is still under way as of this writing, but I must say, it feels pretty nice every time I delete another productivity app.</p>
<h2 id="purpose">Purpose</h2>
<p>This document is a decision record explaining my choice for replacing my organizational system. Theoretically, it is intended for myself as a way to guide my thinking, but in reality, I am only writing it out in this form to share on my blog. The real document I used as the first draft for this post is similar, but a lot rougher, and with other unimportant details jumbled in.</p>
<p><em>Some of this text is inserted purely as commentary for the blog post, and would not normally be included in a decision record document. I have marked such text with italics, this paragraph included.</em></p>
<h2 id="context">Context</h2>
<ul>
<li>My personal backlog is spread across two email accounts, Trello, Remember The Milk, a few Evernote notes, and occasionally project management systems used by my customers. I would like to have one place where I can see all of my priorities so that I can easily find the next most important thing to work on, and to ensure important tasks don&rsquo;t get dropped.</li>
<li>I am spending a fair bit of time fiddling with cards in Trello, trying to update them, reset them, and change the dates on them. I don&rsquo;t feel like I ever have a good view of my priorities there. I am also worried that important tasks are getting dropped because of it&rsquo;s limitations.</li>
<li>Remember The Milk is great for managing recurring tasks, but it doesn&rsquo;t work for anything more complicated than simple chores. Some of my projects have a lot of research notes, open questions, and subtasks that are best kept all together. Trello can handle more complexity in a card, but I still find it limiting for a project that is more than a couple days of work.</li>
<li>Keeping a backlog spread between notes in Evernote and Trello lists causes unnecessary work reconciling the two. I also end up searching both when I want to find notes from something I&rsquo;ve done in the past.</li>
</ul>
<h2 id="decision">Decision</h2>
<ul>
<li>Try using Obsidian
<ul>
<li>Use a minimal number of vaults</li>
<li>Set up the Tasks plugin</li>
<li>Create a daily note template that includes incomplete tasks scheduled for that day and a view of my calendar</li>
</ul>
</li>
<li>Migrate one type of information at a time. Migrate new types as I get more accustomed to Obsidian.</li>
</ul>
<h2 id="options-evaluated">Options Evaluated</h2>
<h3 id="finalists">Finalists</h3>
<p>I tested each of these applications before choosing Obsidian.</p>
<p><em>The table below is what I call a decision matrix. I put options across the top, and questions down the side. I don&rsquo;t particularly like tables for this sort of data, but so far, they&rsquo;re the best solution I&rsquo;ve found. A nice feature of this format is that I can keep expanding the list of questions as I learn more about the available options and the problem I&rsquo;m trying to solve.</em></p>
<p><em>I don&rsquo;t go out of my way to answer every question for every option. When an option stacks up enough limitations or blockers, or I just have a feeling that it&rsquo;s not what I&rsquo;m looking for, I focus my attention on something else. The tabular format makes it easy to see when I&rsquo;ve done this, but also makes it possible to pick up where I left off if all the other options prove to be worse.</em></p>
<p><em>I like to use background shading in the cells (green, yellow, and red) to mark answers that are notable strengths, weaknesses, or blockers. For the sake of this blog post, I&rsquo;ve added emoji to the cells instead.</em></p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>Obsidian</th>
          <th>Logseq</th>
          <th>Amplenote</th>
          <th>Tana</th>
          <th>Joplin</th>
          <th>Eidos</th>
          <th>Columns</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Decision</td>
          <td>✅<br>Powerful for note management. Tons of plugins. Healthy community. Local storage and markdown notes make it easy to extend or migrate away.<br><br>Not sure if it will be powerful enough for  my chaotic brainstorming.<br></td>
          <td>⚠️<br>Looks pretty powerful for brainstorming and task management. Might not be ideal for general notes, though. Probably not ideal for sharing with wife.</td>
          <td>Looks promising and very usable, but it doesn&rsquo;t seem to be as promising as Obsidian.<br>Tasks and calendars are cool, but I don&rsquo;t feel like they are as powerful.<br>Vendor lock-in is also a significant deterrant.</td>
          <td>⛔<br>Very cool UI, great features, but there is vendor lock-in, and it&rsquo;s not extensible.</td>
          <td>⛔<br>Useability is too poor. I don&rsquo;t think I could handle using it. My wife would find it worse.</td>
          <td>⛔<br>Cool idea, but it doesn&rsquo;t seem mature enough.</td>
          <td>⛔<br>Useful way to manage lists, but not powerful enough for anything else.</td>
      </tr>
      <tr>
          <td>Strengths</td>
          <td>Lots of plugins<br>A bit easier for my wife to use</td>
          <td>Great for storing knowledge and maybe brainstorming and task management</td>
          <td>Excellent for scheduling tasks.</td>
          <td>Included Meeting agent (though it&rsquo;s not integrated with Zoho)<br>Inbox concept is cool<br>Smooth UI<br>Can attach notes to stuff in the calendar<br>Integrated AI</td>
          <td>Collaboration features</td>
          <td>Very Extensible</td>
          <td>Really good at lists</td>
      </tr>
      <tr>
          <td>Drawbacks</td>
          <td>Switching between edit and view mode is annoying, but plugins might be able to help with this</td>
          <td>Would be difficult to share things with my wife<br>Harder to learn and use</td>
          <td>Todos are first-class, but don&rsquo;t really fit into the notes. They&rsquo;re more like reminders than artifacts.<br>Less powerful features with drawings and code. No executable code.</td>
          <td></td>
          <td>Useability is pretty poor</td>
          <td>Weird activation key<br>Doesn&rsquo;t seem finished yet</td>
          <td>⛔<br>Not good at anything except checklists</td>
      </tr>
      <tr>
          <td>Performance</td>
          <td>✅<br>Really fast</td>
          <td>⚠️<br>A bit sluggish, but might improve with database version.</td>
          <td>Seems pretty fast</td>
          <td></td>
          <td></td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Price</td>
          <td>Sync: $4/m/user USD <br>Commercial: $50/y USD (two or more employees)</td>
          <td>✅<br>Free, Open Source</td>
          <td>⚠️<br>Free Forever (which makes me nervous about longevity) Commercial: $6/m (+more sync)</td>
          <td>$90/yr</td>
          <td>Free / $80/yr</td>
          <td>Free, Open Source</td>
          <td></td>
      </tr>
      <tr>
          <td>Syncing</td>
          <td>Obsidian Sync is supposed to be good.<br>Other syncing solutions are available.</td>
          <td><a href="https://docs.logseq.com/#/page/logseq%20sync">Logseq Sync</a> (Paid Feature) Or: sync files yourself or: store them in git!</td>
          <td>Naturally online.</td>
          <td>Naturally online.</td>
          <td>Do it yourself, or use their cloud. OneDrive supported also.</td>
          <td>Self-hosted API: <a href="https://github.com/mayneyao/eidos-api-agent-node">https://github.com/mayneyao/eidos-api-agent-node</a></td>
          <td>Cloud-based</td>
      </tr>
      <tr>
          <td>Test: Make a daily Plan with projects and chores</td>
          <td>Very powerful tasks plugin queries tasks from multiple notes. Takes a bit of work to set up.</td>
          <td>Can use todo items across any area of notes. Can then write queries to surface these in the daily journal.</td>
          <td>Really good at this. Can easy drop items into the calendar. Daily jots for notes and tasks. Buttons for bringing incomplete tasks over from previous day. Cannot view work and personal calendars together though. In the instructions they say that people don&rsquo;t really want this&hellip; hrmm</td>
          <td>✅<br>Can make anything a todo item, and it&rsquo;s automatically put into appropriate lists.</td>
          <td>⛔<br>Terrible ux</td>
          <td>Can build my own list&hellip;</td>
          <td>Easy</td>
      </tr>
      <tr>
          <td>Test: Recreate Mule project notes</td>
          <td>✅<br>Easily with hierarchal tasks, and notes, and anything else I want</td>
          <td>✅<br>Hierarchy, namespaces, todo, with optional schedules. Fantastic!</td>
          <td>⚠️<br>It&rsquo;s easy to make pages, and to make tasks that are pages, and tasks that are nested&hellip; but it doesn&rsquo;t feel like a first class thing.</td>
          <td>Fine</td>
          <td>⚠️<br>Kind of like evernote, but worse experience</td>
          <td></td>
          <td>⛔<br>Too complicated<br><br>Has chat-style notes, but they&rsquo;re too limiting</td>
      </tr>
      <tr>
          <td>Handling Tasks</td>
          <td>✅<br>With tasks plugin <a href="https://github.com/obsidian-tasks-group/obsidian-tasks">https://github.com/obsidian-tasks-group/obsidian-tasks</a></td>
          <td>✅<br>Relatively easy with TODO. Has multiple statuses such as complete and cancelled</td>
          <td>Easy to add tasks to notes or other things Very easy to schedule tasks.<br><br>Tasks that are completed are removed from notes.</td>
          <td><code>Ctrl+Enter</code><br>Limited statuses<br>Can create multiple similar supertags with different fields</td>
          <td>It has some functionality, but it&rsquo;s not pleasant to use</td>
          <td><code>Ctrl + Enter</code></td>
          <td>Easy, efficient</td>
      </tr>
      <tr>
          <td>Handling Subtasks</td>
          <td>✅<br>Very easy with nesting</td>
          <td>✅<br>Very easy with nesting</td>
          <td>⚠️<br>You can nest tasks, but they exist somewhat independently of their notes, and the relationships don&rsquo;t get maintained. If you complete the parent, the children get completed.</td>
          <td>Easy</td>
          <td>Not capable natively</td>
          <td>Infinite hierarchy</td>
          <td>Only one level</td>
      </tr>
      <tr>
          <td>Handling recurring tasks</td>
          <td>✅<br>Yes</td>
          <td>✅<br>Yes</td>
          <td>✅<br>Yes. And easy to do.</td>
          <td>⚠️<br>Nope, but there are workarounds</td>
          <td>⛔<br>Supposed to be possible, but can&rsquo;t figure it out</td>
          <td></td>
          <td>⛔<br>Can&rsquo;t handle it</td>
      </tr>
      <tr>
          <td>Handling drawings</td>
          <td>✅<br>Excalidraw and Canvas pages.<br><br>Ink plugin looks interesting.</td>
          <td>✅<br>Yes, and infinite canvas boards <br><code>/draw</code></td>
          <td>Excalidraw</td>
          <td>No</td>
          <td>Can draw freehand and use text. rearranging objects, but no built-in shapes.</td>
          <td></td>
          <td>⛔<br>pictures, not notes</td>
      </tr>
      <tr>
          <td>Handling Code</td>
          <td>✅<br>Yes. And it can run it too!</td>
          <td>Yes</td>
          <td>Yes</td>
          <td>Yes, but it seems a bit buggy.</td>
          <td>Yes, but the UI got buggy when it was added.</td>
          <td></td>
          <td>Markdown code blocks are supported</td>
      </tr>
      <tr>
          <td>Inline executable code?</td>
          <td>✅<br>Math plugin<br>Any code I want with Execute Code plugin</td>
          <td>✅<br>math, with variables and functions</td>
          <td>No</td>
          <td>No</td>
          <td>⚠️<br>Yes. Math via plugins.</td>
          <td></td>
          <td>Don&rsquo;t think so</td>
      </tr>
      <tr>
          <td>Handling Diagrams</td>
          <td>✅<br>Canvas: Nice block-and-arrow diagrams Excalidraw and Mermaid support!</td>
          <td>✅<br>Embedded mermaid!</td>
          <td>⚠️<br>There&rsquo;s a plugin, but it doesn&rsquo;t work very well</td>
          <td>No</td>
          <td>Can embed mermaid drawings directly</td>
          <td></td>
          <td>Nope</td>
      </tr>
      <tr>
          <td>Test: Reading List</td>
          <td>It&rsquo;s possible with dataview or folder table plugins, but it&rsquo;ll take some work to set it up. Kanban board may work too.</td>
          <td>Should be fine</td>
          <td>Not good at lists</td>
          <td>Yes. With fields and sorting</td>
          <td></td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Test: Development Recommendations</td>
          <td>Pretty easy, but may want to customize it a bit.</td>
          <td>✅<br>Should be excellent</td>
          <td></td>
          <td>Should be okay. Linking across content seems harder.</td>
          <td></td>
          <td></td>
          <td>Nope</td>
      </tr>
      <tr>
          <td>Test: Brainstorming</td>
          <td>Should be fine. Canvas is cool for brainstorming. Easy to dump notes on board.</td>
          <td>✅<br>Should be great for this</td>
          <td></td>
          <td>No</td>
          <td></td>
          <td></td>
          <td>Nope</td>
      </tr>
      <tr>
          <td>Test: Storing Receipts</td>
          <td>⛔<br>It can do it, but with synching mechanism, they have to be downloaded to all devices that access them, even phones.</td>
          <td>⛔<br>Probably not good at this</td>
          <td></td>
          <td>No</td>
          <td></td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Handles Tables</td>
          <td>Markdown tables. Some plugins to make editing them more tolerable.</td>
          <td></td>
          <td></td>
          <td>Not markup ones</td>
          <td></td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Tabulation?</td>
          <td><a href="https://github.com/blacksmithgu/obsidian-dataview">obsidian-dataview plugin</a></td>
          <td></td>
          <td>No</td>
          <td>Yes, with fields</td>
          <td></td>
          <td></td>
          <td>Nope</td>
      </tr>
      <tr>
          <td>Grouping / sorting data</td>
          <td>Yes, via dataview plugin.</td>
          <td>Advanced queries are possible, but the syntax is a bit crazy looking.</td>
          <td></td>
          <td>Some pretty powerful built-in queries</td>
          <td></td>
          <td></td>
          <td>Nope</td>
      </tr>
      <tr>
          <td>Data Organization</td>
          <td>Vaults (separated), Folders, Files. Daily Journal</td>
          <td>Pages, Namespaces, Tags, Daily Journal</td>
          <td></td>
          <td>Similar to Logseq</td>
          <td></td>
          <td>Folders (recursive) Documents Lists</td>
          <td></td>
      </tr>
      <tr>
          <td>searching</td>
          <td>✅<br>Embedded queries. Plugins for more advanced cases.</td>
          <td>Embedded queries. Language is bizarrely complicated.</td>
          <td></td>
          <td>✅<br>Built-in queries.</td>
          <td></td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Links to other applications</td>
          <td>Supports links</td>
          <td>Supports links</td>
          <td></td>
          <td>Yes</td>
          <td></td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Integrate live data from other applications</td>
          <td>Not built in</td>
          <td>iframes with inline html</td>
          <td></td>
          <td>No</td>
          <td>✅<br>Markdown: <a href="https://joplinapp.org/plugins/plugin/github.hegerdes.joplin_remote_note_pull/">Remote-Note-Pull</a></td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Scan from Rocketbook</td>
          <td>⚠️<br>Not built in, but maybe possible with integrations</td>
          <td></td>
          <td>Mail-to-note feature</td>
          <td>Not built in</td>
          <td>Email -&gt; Note with Joplin Cloud</td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Mobile Usability</td>
          <td>✅<br>Excellent</td>
          <td></td>
          <td></td>
          <td>Nice inbox for dumping things in.<br>Can access notes via web, but editing looks tedious</td>
          <td></td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Can my wife use it?</td>
          <td>⚠️<br>Obsidian Sync can be used for sharing a vault with multiple people. Can only share at the vault level, though.</td>
          <td>⛔<br>Sharing between users is not officially supported. It would be hard for her to use.</td>
          <td>Probably. It supports sharing via tags</td>
          <td>She probably wouldn&rsquo;t like it</td>
          <td>Too difficult to use</td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Automation / integration</td>
          <td>✅<br>Plugin: Local REST Api<br>Local markdown files.</td>
          <td>✅<br>It&rsquo;s all just markdown files! Also an API</td>
          <td>There is an API</td>
          <td>API for Pro account</td>
          <td>Local API</td>
          <td>Local API for integrations</td>
          <td></td>
      </tr>
      <tr>
          <td>Client Type</td>
          <td>Installed app</td>
          <td>Application or Web Page</td>
          <td>PWA, or Installed App</td>
          <td>Web page</td>
          <td>Executable, Mobile</td>
          <td>Local Web App Can be self-hosted</td>
          <td></td>
      </tr>
      <tr>
          <td>License</td>
          <td>Proprietary</td>
          <td>AGPL-3.0</td>
          <td>Proprietary</td>
          <td>Proprietary</td>
          <td>AGPL-3.0</td>
          <td>AGPL-3.0</td>
          <td></td>
      </tr>
      <tr>
          <td>What is it written in?</td>
          <td>NA</td>
          <td>Clojure &amp; TypeScript</td>
          <td>NA</td>
          <td>NA</td>
          <td>TypeScript &amp; Rust</td>
          <td>TypeScript</td>
          <td></td>
      </tr>
      <tr>
          <td>Plugins?</td>
          <td>✅<br>A lot of them!</td>
          <td>✅<br>Yes. There are a LOT of them around.</td>
          <td>⚠️<br>Yes, but doesn&rsquo;t seem like a healthy ecosystem</td>
          <td>⛔<br>Not supported</td>
          <td>Yes. Seems to be a healthy ecosystem.</td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Plugin languages supported?</td>
          <td>TypeScript, JavaScript</td>
          <td>TypeScript</td>
          <td></td>
          <td></td>
          <td>TypeScript</td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Where is data hosted</td>
          <td>Locally, wherever you want, or Cloud: USA</td>
          <td>Locally or wherever you want</td>
          <td>Their cloud</td>
          <td>Google Cloud</td>
          <td>Local or Numerous Cloud Options</td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Offline Mode</td>
          <td>Yes</td>
          <td>Yes</td>
          <td></td>
          <td>⛔<br>No</td>
          <td>Yes</td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Data Encryption</td>
          <td>⚠️<br>None locally. <br>E2E encryption in their sync option.</td>
          <td>⚠️<br>None locally. Transmit as you want.</td>
          <td></td>
          <td>⛔<br>&ldquo;data is encrypted both at rest and in transit&rdquo;</td>
          <td>E2E Encryption is possible, but not enabled by default.</td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Export capability</td>
          <td>✅<br>Local storage is markdown. Lots of plugins for exporting.</td>
          <td>✅<br>It&rsquo;s just mardown files PDF Export<br>Slide generator</td>
          <td></td>
          <td>JSON or Markdown</td>
          <td>JEX format (Joplin Export file) - meant for backup PDF<br>Markdown</td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Import from Evernote</td>
          <td>Supported <a href="https://help.obsidian.md/import/evernote">https://help.obsidian.md/import/evernote</a></td>
          <td></td>
          <td></td>
          <td>⛔<br>Not supported</td>
          <td>Supports ENEX files Links may not work where titles differ</td>
          <td></td>
          <td></td>
      </tr>
      <tr>
          <td>Company Health</td>
          <td>Based in Oakville, Ontario Active Development Appears to be private, small company.</td>
          <td></td>
          <td></td>
          <td></td>
          <td></td>
          <td></td>
          <td></td>
      </tr>
  </tbody>
</table>
<h3 id="obsidian-alternatives-evaluated">Obsidian Alternatives Evaluated</h3>
<p>Obsidian was a bit different from most of the other options that made it to the finalist stage. Once it began to stand out as a frontrunner, I did a quick search for apps similar to Obsidian, and tested the following two as a result:</p>
<ul>
<li><strong>Amplenote</strong> - included in matrix above</li>
<li><strong>Affine.pro</strong> - disqualified pretty quickly because there was no mobile option and had a very small library of plugins</li>
</ul>
<h3 id="initial-scan-for-options">Initial Scan for Options</h3>
<p>The following were discovered and briefly researched to decide if they should be evaluated further.</p>
<p><em>I only evaluated these options on a few criteria that were easy to look up. This allowed me to narrow the field quickly, as opposed to testing each one in detail. I went in interested in network-style tools, and excited by the prospect of something open source, extensible, and based on a transparent data format.</em></p>
<p><em>Because of the limitations of tables, and because I usually have a lot more options that questions in this phase, I&rsquo;ve swapped the table axes from my usual for a decision matrix.</em></p>
<table>
  <thead>
      <tr>
          <th></th>
          <th>Price</th>
          <th>Handling tasks</th>
          <th>Handling Subtasks</th>
          <th>Mixed Content Support</th>
          <th>Notes</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>✅<br>Tana</td>
          <td>$90/yr</td>
          <td>Okay</td>
          <td>⚠️<br>Seems possible with tags, but complicated.</td>
          <td>✅<br>Good</td>
          <td>Seems like a popular new thing. Powerful note features with fields and supertags.</td>
      </tr>
      <tr>
          <td>✅<br>Columns</td>
          <td></td>
          <td>It&rsquo;s all lists</td>
          <td></td>
          <td></td>
          <td>Lots and lots of lists.</td>
      </tr>
      <tr>
          <td>Routine</td>
          <td>Free</td>
          <td></td>
          <td></td>
          <td></td>
          <td>⛔<br>Lots of incomplete features</td>
      </tr>
      <tr>
          <td>boostnote.io</td>
          <td>$8/user/month</td>
          <td>Maybe</td>
          <td>Can&rsquo;t tell</td>
          <td>Code formatting. Diagrams. Links to other apps.</td>
          <td>✅<br>Open source client</td>
      </tr>
      <tr>
          <td>✅<br>Eidos</td>
          <td>✅<br>Free. Open Source. Locally Hosted.<br><br></td>
          <td></td>
          <td></td>
          <td>Supports Extensions. Can execute code. Supports spreadsheets</td>
          <td>Locally hosted, offline. Has local API. Offline LLM support.</td>
      </tr>
      <tr>
          <td>✅<br>Joplin</td>
          <td>Pro: $80/yr Free if you host your own data</td>
          <td>Can insert tasks</td>
          <td></td>
          <td>Plugins. Rich Text and markdown support. Supports extensions. Code blocks. OCR!!</td>
          <td>Open data. You can store it yourself or use their cloud option. Seems like a better, free Evernote</td>
      </tr>
      <tr>
          <td>Milanote</td>
          <td>$12/yr</td>
          <td>Tasks</td>
          <td>⛔<br>Doesn&rsquo;t appear to</td>
          <td>Notes. Todo lists. Photos in a visual canvas.</td>
          <td>Very visual. Good for brainstorming.</td>
      </tr>
      <tr>
          <td>✅<br>Logseq</td>
          <td>✅<br>Open Source.</td>
          <td>✅<br>Tasks can be inter-mixed in notes. Use queries to access them later. Grouped by blocks too.</td>
          <td>✅<br>Seems like it&rsquo;s possible to build pages with piles of tasks embedded in them.</td>
          <td></td>
          <td>Networked outliner. Use tags to link things together.</td>
      </tr>
      <tr>
          <td>✅<br>Obsidian</td>
          <td>Sync $4/month</td>
          <td>✅<br>Kanban plugins.</td>
          <td></td>
          <td>✅<br>Canvas. Supports plugins. Tasks. Indenting. Code blocks.</td>
          <td>✅<br>A popular choice</td>
      </tr>
  </tbody>
</table>
<h2 id="summary">Summary</h2>
<p><em>It was a bit of a process to do the research, but I&rsquo;m glad that I did. Moving all my organization systems has the potential to be time consuming. The greater the cost of making a mistake, the more up-front effort is justified. This also gave me an opportunity to explore various options and styles and get better informed on how I could organize everything.</em></p>
<p><em>Now that I&rsquo;m a few weeks into the switch, I&rsquo;m still loving Obsidian. I have a few more things to migrate, but generally it has been better than my expectation in every regard.</em></p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/architecture" term="architecture" label="architecture" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/architectural-decision" term="architectural-decision" label="architectural decision" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/practices" term="practices" label="practices" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/reviews" term="reviews" label="reviews" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Case of the Premature Power Outage]]></title>
            <link href="https://jessemcdowell.ca/2025/02/Case-of-the-Premature-Power-Outage/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2024/06/Pragmatic-Potato-Tech-Stack/?utm_source=atom_feed" rel="related" type="text/html" title="The Pragmatic Potato Tech Stack" />
                <link href="https://jessemcdowell.ca/2024/05/Cross-Cutting-Concerns/?utm_source=atom_feed" rel="related" type="text/html" title="Cross-Cutting Concerns - Ten Approaches" />
                <link href="https://jessemcdowell.ca/2024/03/The-True-Cost-of-Dependencies/?utm_source=atom_feed" rel="related" type="text/html" title="The True Cost of Dependencies" />
                <link href="https://jessemcdowell.ca/2023/11/Using-Architectural-Decision-Records/?utm_source=atom_feed" rel="related" type="text/html" title="Using Architectural Decision Records" />
                <link href="https://jessemcdowell.ca/2023/11/This-Blog-Hexo-generated-static-site-hosted-on-GitHub-Pages/?utm_source=atom_feed" rel="related" type="text/html" title="This Blog: Hexo-generated static site hosted on GitHub Pages" />
            
                <id>https://jessemcdowell.ca/2025/02/Case-of-the-Premature-Power-Outage/</id>
            
            
            <published>2025-02-03T19:40:52-08:00</published>
            <updated>2025-02-03T19:40:52-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>Many years ago I worked on a large system with a number of interconnected services that had grown organically over the years. As new services were added, they became dependent on each other in ways that nobody on the team fully understood. When our hosting provider announced a planned outage, we had to figure out how to safely shut the system off, and more importantly, how to safely and quickly start it back up again afterward. We had never done either of these things before, so we put a lot of effort in to make sure it would go smoothly.</p>
<p>I&rsquo;ll never forget the day. About a week before the planned outage, we were in a big meeting of all the teams. We were talking through our shutdown script, moving things around and making sure everyone agreed on the order. It seemed like we could shut down smoothly if we followed the steps, and one false move could break everything. About 30 minutes into the meeting, someone came running in to announce that the whole system had just gone down.</p>
<p>We rushed back to our desks and started checking. It wasn&rsquo;t all down, but a bunch of things had gone down at once, and one of those things was our main firewall appliance.</p>
<p>We reached out to the data centre operators to get more information. The planned outage was for an upgrade to one of the two power feeds, and they had electricians preparing the new lines in advance. One of those electricians had accidentally hit a live wire with his drill, knocking out one of the feeds.</p>
<p>Normally this wouldn&rsquo;t be a problem because of the second, redundant feed. Unfortunately, this was when we learned that the data centre operators had not always connected each device to the separate power sources correctly. Our firewall, for example, had both of its plugs connected to the affected feed. Our database servers and NAS were powered with redundancy, though, so some parts of the system were able to keep working.</p>
<p>We immediately asked for the firewall to be fixed, and as we figured out each remaining server that was down, we asked for them to be corrected as well. Many customers in the data centre had similar issues, so the operators were overwhelmed, and responses got slower and slower. Things were almost working for us again when suddenly everything went down again.</p>
<p>Fifteen minutes later, we got an email (sent half an hour earlier) that the data centre had to be completely shut down. The building&rsquo;s air conditioning did not have a redundant power supply, and the building had reached 60°C (140°F). Cutting the building power was the only choice they had; the operators weren&rsquo;t able to safely enter the building anymore.</p>
<p>Several hours later, the data centre was restored. We didn&rsquo;t know what state things were in, and the operators didn&rsquo;t have a lot of time to help us, so they just turned everything on, and we took it from there.</p>
<p>It was a late night, but we had everything going by the end of the same day. Thanks to diligent administration, redundant storage, and our design to support regular incremental upgrades, we had no meaningful data loss, and most systems worked correctly on startup. The worst of it was a couple of behind-the-scenes services with interdependencies that had to be restarted a few times.</p>
<p>It was a sobering experience. We had a happy ending, but it could have gone very differently if one of the key systems hadn&rsquo;t been designed or configured correctly. We had excellent, experienced people on our staff, and that helped a lot, but even experienced people make mistakes.</p>
<p>Regular testing is the only way to know for sure that your redundancy plans are good enough. If it&rsquo;s something your business needs, you had better make sure you&rsquo;re testing it. Using upgrade procedures that kill processes without shutdown warnings is a great way to make this a regular practice. <a href="https://netflixtechblog.com/tagged/chaos-engineering">Netflix&rsquo;s Chaos Engineering approach</a>, which involves routinely and automatically breaking things in production, demands major respect also. Recovery drills are tedious and time-consuming, but they too are an important part of ensuring system continuity.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/architecture" term="architecture" label="architecture" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/war-stories" term="war-stories" label="war stories" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Household Organization: Hallway Whiteboard]]></title>
            <link href="https://jessemcdowell.ca/2025/01/Household-Organization-Hallway-Whiteboard/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2024/11/Abandoning-Household-Organization/?utm_source=atom_feed" rel="related" type="text/html" title="Abandoning Household Organization" />
                <link href="https://jessemcdowell.ca/2024/08/Rocketbook-vs-Whiteboard/?utm_source=atom_feed" rel="related" type="text/html" title="Rocketbook vs Whiteboard" />
                <link href="https://jessemcdowell.ca/2024/06/Pragmatic-Potato-Tech-Stack/?utm_source=atom_feed" rel="related" type="text/html" title="The Pragmatic Potato Tech Stack" />
                <link href="https://jessemcdowell.ca/2024/01/My-Experience-with-the-Dvorak-Keyboard-Layout/?utm_source=atom_feed" rel="related" type="text/html" title="My Experience with the Dvorak Keyboard Layout" />
                <link href="https://jessemcdowell.ca/2023/12/Switching-from-Google-Podcasts-to-Spotify/?utm_source=atom_feed" rel="related" type="text/html" title="Switching from Google Podcasts to Spotify" />
            
                <id>https://jessemcdowell.ca/2025/01/Household-Organization-Hallway-Whiteboard/</id>
            
            
            <published>2025-01-06T21:35:01-08:00</published>
            <updated>2025-01-06T21:35:01-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>A couple of years ago I received my Makeway kit. It was a kickstarter project that included an assortment of tracks and tricks for marbles, all suspended on a vertical surface by magnets. For this reason, I mounted a large whiteboard (with a metal backing) in the hallway of our apartment.</p>
<p>About a year ago, after conducting the <a href="https://jessemcdowell.ca/2024/11/Abandoning-Household-Organization/">household organization surveys</a>, I started using the whiteboard as a physical weekly calendar for my family. I used some thin black masking tape to make a grid that would withstand the whiteboard eraser. I also got some small magnetic dry-erase rectangles to simplify rescheduling common recurring tasks.</p>
<p><img src="whiteboard.jpg" alt="A whiteboard mounted on the wall with a weekly calendar marked out in tape, and a small marble track set up over top of it."></p>
<p>The biggest benefit of the mounted calendar has been the process of updating the board once a week. It forces me to look through all the calendars. This is when I&rsquo;ll notice scheduling conflicts and allows me to make adjustments to keep the week flowing smoothly.</p>
<p>Although it&rsquo;s the biggest benefit, the need to manually update the board is also the biggest drawback. Since we primarily use online calendars, the board can quickly get out of date when things change, and for us, they can change a lot as the week progresses. It also doesn&rsquo;t include the weather, which is very handy when planning outdoor activities.</p>
<p>When the board was a novelty, my wife and I would regularly stop to look, but now that we&rsquo;re used to it, it takes intention to notice it. This means we can&rsquo;t use the whiteboard to communicate changes to the schedule.</p>
<p>The whiteboard calendar hasn&rsquo;t been worthless, but it hasn&rsquo;t radically improved our organization either. A larger family might have better results. If you&rsquo;re curious, a piece of paper taped to the wall would be easier to start out.</p>
<p>The whiteboard itself has been good. It&rsquo;s been great to have a central place to write notes, draw pictures, and otherwise play around. The marble tracks have been tremendously fun as well, and we can set them up over top of our schedule.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/household-organization" term="household-organization" label="household organization" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/reviews" term="reviews" label="reviews" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Encoding Time Series Data]]></title>
            <link href="https://jessemcdowell.ca/2024/12/Encoding-Time-Series-Data/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/11/This-Blog-Hexo-generated-static-site-hosted-on-GitHub-Pages/?utm_source=atom_feed" rel="related" type="text/html" title="This Blog: Hexo-generated static site hosted on GitHub Pages" />
                <link href="https://jessemcdowell.ca/2014/04/controller-led-navigation-in-angular/?utm_source=atom_feed" rel="related" type="text/html" title="Controller Led Navigation in Angular" />
                <link href="https://jessemcdowell.ca/2013/06/reading-server-graphs-connected-users/?utm_source=atom_feed" rel="related" type="text/html" title="Reading Server Graphs: Connected Users" />
                <link href="https://jessemcdowell.ca/2011/08/doubling-data-for-performance-testing/?utm_source=atom_feed" rel="related" type="text/html" title="Doubling Data for Performance Testing" />
                <link href="https://jessemcdowell.ca/2010/11/64-bit-iis-vs-32-bit-assemblies/?utm_source=atom_feed" rel="related" type="text/html" title="64-bit IIS vs. 32-bit Assemblies" />
            
                <id>https://jessemcdowell.ca/2024/12/Encoding-Time-Series-Data/</id>
            
            
            <published>2024-12-02T10:29:50-08:00</published>
            <updated>2024-12-02T14:53:53-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>I spent just shy of 9 years working on a suite of products that stored and processed environmental sensor data. This kind of data is naturally stored and processed as a time series. A time series is a set of data points, each with a time and value, and often more. They are common for sensor data, but can be used for pretty much anything that is measured over time such as prices, performance metrics, and analytical data.</p>
<p>There is a lot of subtlety to working with time series data, especially if you are concerned with scientific accuracy. One surprising challenge, which I&rsquo;ll be discussing in this post, is storing and transmitting the data efficiently.</p>
<p>At my previous company, the data we dealt with was relatively low frequency (typically a reading every 5-15 minutes), but some of the data sets went back into the 1800s! Although the data is simple in structure, the size can get surprisingly big when you need to move around millions of points.</p>
<div class="table-of-contents">
    <nav id="TableOfContents">
  <ul>
    <li><a href="#test-application">Test application</a></li>
    <li><a href="#in-application">In application</a></li>
    <li><a href="#json">JSON</a></li>
    <li><a href="#csv">CSV</a></li>
    <li><a href="#custom-binary-format">Custom binary format</a></li>
    <li><a href="#unpacking-precise-timestamps-in-javascript">Unpacking precise timestamps in JavaScript</a></li>
    <li><a href="#standardized-binary-format">Standardized binary format</a></li>
    <li><a href="#compressed-binary">Compressed binary</a></li>
    <li><a href="#compressed-csv-and-json">Compressed CSV and JSON</a></li>
    <li><a href="#downsampling">Downsampling</a></li>
    <li><a href="#conclusion">Conclusion</a></li>
  </ul>
</nav>
</div>

<h2 id="test-application">Test application</h2>
<p>I built a simple app to measure the transmission size of the data in the different variations discussed in this post. You don&rsquo;t need to look at it to follow along, but you might find it interesting. The source code is available here: <a href="https://github.com/jessemcdowell/time-series-encoding-test">https://github.com/jessemcdowell/time-series-encoding-test</a></p>
<p>The test application is written in TypeScript, and uses <a href="https://nodejs.org/">node</a>, <a href="https://expressjs.com/">Express</a>, and <a href="https://axios-http.com/">Axios</a>. It took some fiddling to be able to control compression, and to measure the transmission size (as opposed to the total size of the data, which would normally be more important). The numbers below also include other transmission overhead like headers and chunk encoding. This will skew the numbers a bit, but the results should still be more than good enough to illustrate the differences between approaches.</p>
<h2 id="in-application">In application</h2>
<p>A typical way to store a point in application memory is as an array of simple structures/objects. With an 8-byte integer for the timestamp, and an 8-byte floating point for the value, you can represent each point with 16 bytes. This will have enough precision for most use cases.</p>
<p>For one million points packed in an efficient array, this would consume 16 million bytes of memory. I&rsquo;m going to use this as a baseline for evaluating other options.</p>
<p>This is an efficient, convenient way to handle the data in an application, but chances are you will want to write it to disk, or return it from a web service. It is possible to send this binary representation over the wire, but there are some drawbacks to consider. I&rsquo;ll come back to this.</p>
<h2 id="json">JSON</h2>
<p>JSON is a common way to transmit data between server and client or server and server. A simple representation might look like the following:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">[</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;time&#34;</span><span class="p">:</span> <span class="s2">&#34;2024-01-01T00:00:00.000Z&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;value&#34;</span><span class="p">:</span> <span class="mf">0.54188</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;time&#34;</span><span class="p">:</span> <span class="s2">&#34;2024-01-01T01:00:00.000Z&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;value&#34;</span><span class="p">:</span> <span class="mf">0.32624</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;time&#34;</span><span class="p">:</span> <span class="s2">&#34;2024-01-01T02:00:00.000Z&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nt">&#34;value&#34;</span><span class="p">:</span> <span class="mf">0.76939</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span></code></pre></div><p>This is just three points, and you can see that it&rsquo;s quite a few bytes. If you exclude the whitespace (included above for readability), it&rsquo;s still 157 bytes with each point taking 52 bytes. This means 1 million points takes about 52 million bytes (just over 51MB)!</p>
<p>Here&rsquo;s how it breaks down:</p>
<ul>
<li>19 bytes - object wrapper, commas, and property names</li>
<li>26 bytes - time string and its quotes</li>
<li>7 bytes - the value</li>
</ul>
<p>52 bytes is a lot more than the 16 we need in application memory. This is more than 200% overhead! It also has lower precision than the in memory version can support. It is possible to use a less precise timestamp format, or to use smaller names for the properties (like <code>t</code> and <code>v</code>). Those would help a bit, but we can do better.</p>
<h2 id="csv">CSV</h2>
<p>CSV has a lot less wrapping characters, and doesn&rsquo;t need to repeat the property names for each point. Here are the same values as the JSON example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csv" data-lang="csv"><span class="line"><span class="cl"><span class="s">time</span><span class="p">,</span><span class="s">value</span><span class="p">
</span></span></span><span class="line"><span class="cl"><span class="p"></span><span class="s">2024-01-01T00:00:00.000Z</span><span class="p">,</span><span class="s">0.54188</span><span class="p">
</span></span></span><span class="line"><span class="cl"><span class="p"></span><span class="s">2024-01-01T01:00:00.000Z</span><span class="p">,</span><span class="s">0.32624</span><span class="p">
</span></span></span><span class="line"><span class="cl"><span class="p"></span><span class="s">2024-01-01T02:00:00.000Z</span><span class="p">,</span><span class="s">0.76939</span><span class="p">
</span></span></span></code></pre></div><p>The total is 109 bytes (or 112 with CRLF line endings). The header is just 11 bytes, and can be omitted if desired. Each point is 33 bytes, which is better than the 52 of JSON, but more than double that of our 16 bytes in memory (106% overhead). 1 million points takes just under 36 million bytes.</p>
<p>There are other advantages to CSV as well. It&rsquo;s easy to produce, easy to parse, and can handle multiple values or other types of data for each timestamp easily. It&rsquo;s more readable in debugging tools, and can  be consumed easily in many applications, more easily than JSON. It has capacity for extensibility via adding columns, though it can&rsquo;t naturally handle structured data the same way JSON can.</p>
<h2 id="custom-binary-format">Custom binary format</h2>
<p>A time series can be stored or transmitted in a binary format very similar to the way it would be stored in application memory. You can encode it however you want, but my simple implementation takes 16 million bytes which is considerably better than both text formats discussed above. There is some added complexity to this approach, however. In particular, you will need your own versioning strategy if you ever need to change the structure.</p>
<p>You may also have some difficulty getting your web server and client to transmit and receive the data correctly. An easy way to get around this is to re-pack the binary data into something like Base64 and send that through a normal string property. This is especially helpful if you have a bunch of different time series or other related data you want to return in the same response. The drawback to this approach is that Base64 adds about 33% overhead, so the same million points from before would take 21.3 million bytes instead.</p>
<h2 id="unpacking-precise-timestamps-in-javascript">Unpacking precise timestamps in JavaScript</h2>
<p>Another bit of complexity you might encounter with binary data, or any kind of data really, is the inherent limitation of JavaScript when dealing with 64-bit integers. For most applications this probably won&rsquo;t matter. At my previous company, however, it was a significant and memorable challenge. Some of our features required quoting exact timestamps back to the server, and even a single millisecond of lost precision broke things.</p>
<p>Another challenge was different timestamp representations on the client and server. Our server (C# and C++ based) used a single 64-bit integer to represent 100-nanosecond intervals. Our client (JavaScript) used two number properties (one to store seconds, and a second to store nanoseconds). When unpacking the binary data, every timestamp had to be divided by 10000 for seconds, and the remainder multiplied by 100 for nanoseconds. Doing this with floating points in native JavaScript was very slow. It took some frustrating research and experimentation, but I got the performance gains I needed using a bit of Web Assembly.</p>
<h2 id="standardized-binary-format">Standardized binary format</h2>
<p>Another option is to use a common binary format to transmit the data. <a href="https://msgpack.org/">MsgPack</a> is very similar to JSON, but a bit smaller and faster. It also has the same drawback with time series data: it includes the key names with every single point. A MsgPack equivalent of the million points take around 28 million bytes. This is 76% overhead compared to the 16 million bytes in memory.</p>
<p><a href="https://protobuf.dev/">Protobuf</a> (the binary protocol used in gRPC) does not include keys in the data, but it is harder to use. You have to define a schema that is kept is sync between your client and server. The same million points takes around 14 million bytes in Protobuf. This is a modest 11% less than the custom binary format, but with more safeguards and flexibility.</p>
<h2 id="compressed-binary">Compressed binary</h2>
<p>Gzip compression is an easy way to reduce the size of the data being transmitted. Even better, this is a feature you probably already have enabled on your web servers and clients.</p>
<p>I tested a few configurations of compressed binary data to see how they performed. One interesting discovery was that the compression was noticeably better if I arranged the data in sets (<code>time1-time2-time3-value1-value2-value3</code> instead of pairs such as <code>time1-value1-time2-value2-time3-value3</code>. I assume this is because of how gzip handles adjacent similarities. Here are the results for similar 1-million point blocks:</p>
<ul>
<li>Binary in any arrangement with no compression: 16 million bytes</li>
<li>Binary pairs (<code>time-value-time-value</code>) with gzip: 10.3 million bytes</li>
<li>Binary sets (<code>time-time-value-value</code>) with gzip: 8.7 million bytes</li>
<li>Binary pairs, encoded in Base64, with gzip: 10.8 million bytes</li>
<li>Binary sets, encoded in Base64, with gzip: 10.1 million bytes</li>
</ul>
<p>I also tested MsgPack and Protobuf with gzip. Even though they are arranged in pairs, they performed slightly better:</p>
<ul>
<li>MsgPack: 9.8 million bytes</li>
<li>Protobuf: 8.2 million bytes</li>
</ul>
<p>One drawback to sets over pairs is that you would need all the points available to send them in sequence. If you had a very large amount of points and wanted to stream them incrementally, you would need to develop your own chunk-capable format. This is also a drawback of MsgPack which writes the size of each array at its start. I don&rsquo;t know if Protobuf has this limitation too.</p>
<h2 id="compressed-csv-and-json">Compressed CSV and JSON</h2>
<p>Compression is usually pretty effective wherever you have lots of repeating sequences, and that is certainly the case with JSON and CSV time series data. The same millions points packed in JSON took 7.5 million bytes when gzipped. That&rsquo;s a compression factor over 7 times as compared to the plain JSON, and more than 2x better than straight binary!</p>
<p>A million points of CSV compressed to 6.9 million bytes, which is even better. The compression factor is not as impressive, but the overall size is smaller. This is the smallest of all the options in this post.</p>
<h2 id="downsampling">Downsampling</h2>
<p>Another great way to reduce the size of a time series is to send fewer points. Downsampling (sometimes called Decimation) is a technique that excludes some or most of the points, and it can have a tremendous impact on the size of the data and the performance of your applications.</p>
<p>If you use this technique for data that is being graphed, the user might not even be able to notice. For example, If you are displaying a year of data in a spacious 1200-pixel wide area, you only have 3.3 pixels of width per day. If you have a point per hour, most of those 24 points won&rsquo;t change the image at all and can be safely excluded. If you had a point every minute, you can exclude almost all the points!</p>
<p>There are a few ways to do this. The best technique will depend on your use case and the correct interpolation method for the data. I&rsquo;m not going to get into those in this post. The result of downsampling is still a time series, so you should be able to use any of the techniques already discussed to transmit the downsampled data efficiently.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Time series data can be larger than you expect, but there are lots of ways to store and transmit them more efficiently. For most projects, I would start with CSV compressed by gzip, and use downsampling where possible. For cases with more strenuous requirements, there are plenty of other options to consider.</p>
<p>My testing and discussion were purely focused on the size of the data. Fewer bytes will generally lead to better performance, but in some situations, serialization speed or memory utilization might be more important. If that matters to you, you should <a href="https://pragmaticpotato.com/services/">work with an architect</a> to find the best approach.</p>
<!--
Test results 2024-10-02:
```text
json, no compression:         53 624 980
json, gzip:                    7 507 865
csv, no compression:          34 701 387
csv, gzip:                     6 890 209
msgpack, no compression:      28 121 167
msgpack, gzip:                 9 909 851
protobuf, no compression:     14 251 672
protobuf, gzip:                8 049 274
binary pairs, no compression: 16 000 000
binary pairs, gzip:           10 281 562
binary sets, gzip:             8 546 006
base64 pairs, no compression: 21 333 336
base64 pairs, gzip:           10 878 917
base64 sets, gzip:            10 070 165
```
-->
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/nodejs" term="nodejs" label="nodejs" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/performance" term="performance" label="performance" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/typescript" term="typescript" label="typescript" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/web" term="web" label="web" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Abandoning Household Organization]]></title>
            <link href="https://jessemcdowell.ca/2024/11/Abandoning-Household-Organization/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2024/08/Rocketbook-vs-Whiteboard/?utm_source=atom_feed" rel="related" type="text/html" title="Rocketbook vs Whiteboard" />
                <link href="https://jessemcdowell.ca/2024/06/Pragmatic-Potato-Tech-Stack/?utm_source=atom_feed" rel="related" type="text/html" title="The Pragmatic Potato Tech Stack" />
                <link href="https://jessemcdowell.ca/2024/05/Cross-Cutting-Concerns/?utm_source=atom_feed" rel="related" type="text/html" title="Cross-Cutting Concerns - Ten Approaches" />
                <link href="https://jessemcdowell.ca/2024/03/The-True-Cost-of-Dependencies/?utm_source=atom_feed" rel="related" type="text/html" title="The True Cost of Dependencies" />
                <link href="https://jessemcdowell.ca/2023/11/Using-Architectural-Decision-Records/?utm_source=atom_feed" rel="related" type="text/html" title="Using Architectural Decision Records" />
            
                <id>https://jessemcdowell.ca/2024/11/Abandoning-Household-Organization/</id>
            
            
            <published>2024-11-04T07:02:12-08:00</published>
            <updated>2024-11-04T07:02:12-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>I have been trying to improve the organization of my household for years. My wife and I have been using shared Google calendars for over a decade, but there are plenty of issues we could still improve. It was only minor challenges when it was just the two of us, but once we had a child, the systems we had started to show their limits.</p>
<p>I quit my job with the dream of building my own product, but I started without any particular ideas about what to build. It didn&rsquo;t take long before I was considering working on this exact problem. Not only was it something I would benefit from, but it was also something I would find interesting, and could enjoy building and maintaining by myself.</p>
<p>My first urge was to start coding. I had a clear idea of what I wanted, and my head was spinning with potential designs. However, years of experience have taught me the importance of starting with solid research, so I started there instead.</p>
<p>The first thing I discovered was that there are already several products specifically for this problem. I tried a few out, and while they were not all fantastic, a couple of them were pretty good. There were some differences, but they mostly all had the same core features: a shared calendar, a shared task list, and a shared shopping list.</p>
<p>Many had been around for years. Some were free. The fact that none of them seemed to be particularly successful was my first hint that there was a problem.</p>
<p>My next step was putting together a survey and sending it out via my social networks. I got 67 responses. It&rsquo;s certainly not enough for any kind of scientific validity, but it was enough to notice some trends. I found many people who had the same kinds of problems I did, and most wanted to use technology to help. Desire is normally a good thing for an entrepreneur to find, but there had to be a reason that quality products that had existed for years weren&rsquo;t working.</p>
<p>In the survey, I also asked participants if they would participate in a one-on-one virtual interview in exchange for a gift card. Most agreed. I selected a handful, and set up some meetings.</p>
<p>One of the people I spoke with had used and abandoned two of the best products I&rsquo;d tried. I asked him about it, and his answers are the reason I decided to abandon this problem. He thought technology was a great idea, but he couldn&rsquo;t make his partner use it. He tried using it on his own, but it didn&rsquo;t help, it just became another chore for him to manage.</p>
<p>I had originally approached this problem like business software, but there is a fundamental difference between a workplace and a family: it&rsquo;s easier to motivate an employee than a member of your family. It&rsquo;s not enough to make something that appeals to one member of the family, a successful app would have to be intrinsically motivating to everyone in the household. This is an extremely high bar to clear.</p>
<p>If I take a holistic view of household organization, I don&rsquo;t think another piece of software is the most effective solution. A shared calendar is the main thing a family needs, and there are some good free options. If you need more, there are a few free apps already. If you want to stay offline, you can use a whiteboard, or even a piece of paper hung on the wall. All that leaves for me is to show people how to use these things, but writing a self-help book or teaching a class isn&rsquo;t what I want to work on.</p>
<p>The interviews weren&rsquo;t a complete waste, however. Some of the people I talked to had very organized households, and those conversations were insightful. Here were a few ideas that stood out to me:</p>
<ul>
<li>Having a weekly planning ritual makes sure everyone knows what&rsquo;s happening, and creates an opportunity to discuss issues. This seems to be especially helpful for larger households, households with multiple generations, and/or households with children old enough to contribute.</li>
<li>Planning week by week (as opposed to day-by-day) simplifies the process and makes family routines more predictable.</li>
<li>Having a calendar that everyone can see, be it through software or posted on the wall, gives everyone the same view of the week.</li>
<li>Assigning chores to specific people, and making them accountable for those chores, helps to make sure that those chores get done.</li>
</ul>
<p>The exercise of researching the market, talking to people, and learning about the problem was interesting and enjoyable. It cost me some time and a bit of money, but I have no regrets. Maybe some of it will help with my next idea.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/design" term="design" label="design" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/household-organization" term="household-organization" label="household organization" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Exporting to Google Sheets API from C#]]></title>
            <link href="https://jessemcdowell.ca/2024/10/Exporting-to-Google-Sheets-API-from-CSharp/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/05/Sustainable-Errors/?utm_source=atom_feed" rel="related" type="text/html" title="Sustainable Errors" />
                <link href="https://jessemcdowell.ca/2023/11/Announcing-Pragmatic-Potato-Software/?utm_source=atom_feed" rel="related" type="text/html" title="Announcing Pragmatic Potato Software" />
                <link href="https://jessemcdowell.ca/2012/04/installutil-and-badimageformatexception/?utm_source=atom_feed" rel="related" type="text/html" title="InstallUtil and BadImageFormatException - Facepalm" />
                <link href="https://jessemcdowell.ca/2011/05/consulting-a-brave-new-world/?utm_source=atom_feed" rel="related" type="text/html" title="Consulting - A Brave New World" />
                <link href="https://jessemcdowell.ca/2011/04/the-average-test/?utm_source=atom_feed" rel="related" type="text/html" title="The Average Test" />
            
                <id>https://jessemcdowell.ca/2024/10/Exporting-to-Google-Sheets-API-from-CSharp/</id>
            
            
            <published>2024-10-06T14:35:38-07:00</published>
            <updated>2024-10-06T14:35:38-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>I recently did a project for a client that needed a way to share a bit of live data with an external organization. He originally asked for a simple website with authentication and an API. I could have built this, but for the frequency and volumes of data they were using, I suggested a simpler approach: a Google Sheet and a recurring job that updates it.</p>
<p>Any online spreadsheet is a great way to share a small amount of data. Users can filter, sort, make formulas, export the data, or even write their own integrations. It&rsquo;s also possible (depending on how changes are handled) to manually annotate or override data that&rsquo;s been exported. Implementing all of this in a custom web page is possible, but some of it is pretty tricky.</p>
<p>The project was a success, and I&rsquo;m happy with the results. Working with the Google Sheets API was not as straightforward as I had expected though. It was hard to find documentation, or even many examples showing how to use it. I&rsquo;m not sure why this is. In this post I&rsquo;ll share some of the things I learned.</p>
<div class="table-of-contents">
    <nav id="TableOfContents">
  <ul>
    <li><a href="#googleapissheetsv4-package">Google.Apis.Sheets.v4 Package</a></li>
    <li><a href="#authentication">Authentication</a></li>
    <li><a href="#spreadsheet-sheet-and-range-identifiers">Spreadsheet, Sheet, and Range Identifiers</a></li>
    <li><a href="#update-types">Update Types</a></li>
    <li><a href="#etags">ETags</a></li>
    <li><a href="#performance--update-size">Performance / Update Size</a></li>
    <li><a href="#conclusion">Conclusion</a></li>
  </ul>
</nav>
</div>

<h2 id="googleapissheetsv4-package">Google.Apis.Sheets.v4 Package</h2>
<p>The <a href="https://www.nuget.org/packages/Google.Apis.Sheets.v4/">Google.Apis.Sheets.v4 package</a> is the official .NET client library for the Google Sheets API. You can install it with NuGet.</p>
<p>It has <a href="https://googleapis.dev/dotnet/Google.Apis.Sheets.v4/latest/api/Google.Apis.Sheets.v4.html">official documentation</a>, but I found it to be almost useless. I recommend the <a href="https://developers.google.com/sheets/api/reference/rest">Google Sheets API documentation</a> instead. It has much more information about how the API works and what the arguments mean. Methods and properties in .NET client library match the Rest API documentation exactly, I assume this is a result of the client library being auto-generated.</p>
<p>My first tip is to make sure you&rsquo;re using the exact name for an API endpoint. For example, these two APIs, although seemingly similar, take different arguments, and behave totally differently:</p>
<ul>
<li><code>service.Spreadsheets.BatchUpdate()</code></li>
<li><code>service.Spreadsheets.Values.BatchUpdate()</code></li>
</ul>
<p>Another thing to be aware of is that the methods above don&rsquo;t actually call the service, they create a request object. They almost look like they can be used in a fluent style, but this is only possible for the simplest get calls. I found it much easier to assign the request object into a variable so that I could set its properties directly. For example:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="cl"><span class="kt">var</span> <span class="n">range</span> <span class="p">=</span> <span class="s">&#34;!A1:A3&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kt">var</span> <span class="n">change</span> <span class="p">=</span> <span class="k">new</span> <span class="n">ValueRange</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">Range</span> <span class="p">=</span> <span class="n">range</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">MajorDimension</span> <span class="p">=</span> <span class="s">&#34;ROWS&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="n">Values</span> <span class="p">=</span> <span class="p">[[</span><span class="s">&#34;one&#34;</span><span class="p">,</span> <span class="s">&#34;two&#34;</span><span class="p">,</span> <span class="s">&#34;three&#34;</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="kt">var</span> <span class="n">request</span> <span class="p">=</span> <span class="n">service</span><span class="p">.</span><span class="n">Spreadsheets</span><span class="p">.</span><span class="n">Values</span><span class="p">.</span><span class="n">Update</span><span class="p">(</span><span class="n">change</span><span class="p">,</span> <span class="s">&#34;SHEET_ID&#34;</span><span class="p">,</span> <span class="n">range</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="n">request</span><span class="p">.</span><span class="n">ValueInputOption</span> <span class="p">=</span> <span class="n">SpreadsheetsResource</span><span class="p">.</span><span class="n">ValuesResource</span><span class="p">.</span><span class="n">UpdateRequest</span><span class="p">.</span><span class="n">ValueInputOptionEnum</span><span class="p">.</span><span class="n">RAW</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="kt">var</span> <span class="n">result</span> <span class="p">=</span> <span class="k">await</span> <span class="n">request</span><span class="p">.</span><span class="n">ExecuteAsync</span><span class="p">();</span>
</span></span></code></pre></div><p>Notice how I passed an array of arrays to the <code>Values</code> property? The API uses lists of lists for most values. This is necessary when the data includes multiple rows, but a bit annoying when updating single rows or cells. Fortunately it&rsquo;s pretty easy to make an inline Array in modern C#, and an Array implements <code>IList</code>.</p>
<p>Each request object does have a <code>Configure()</code> method that allows you to set properties inline, but I found it even more verbose than working with the request object directly.</p>
<h2 id="authentication">Authentication</h2>
<p>There are <a href="https://developers.google.com/workspace/guides/create-credentials">a bunch of ways to authenticate to the Google APIs</a>, but I found the choices overwhelming. For my purposes (a background service that needs to authenticate as a single user without a human present) a <a href="https://cloud.google.com/iam/docs/service-account-overview">service account</a> seemed like the best choice.</p>
<p>You can generate a service account in <a href="https://console.cloud.google.com/">the Google Cloud Console</a>. You&rsquo;ll need a project. Also make sure to enable the Google Sheets API for your project. The UI in the console changes all the time, so you&rsquo;ll be better off looking up instructions in the official documentation.</p>
<p>Once the account is created, you can download the JSON key file, and use this in your client code like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="cl"><span class="kt">var</span> <span class="n">credential</span> <span class="p">=</span> <span class="n">GoogleCredential</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">FromJson</span><span class="p">(</span><span class="s">&#34;&lt;your JSON here&gt;&#34;</span><span class="p">)</span> <span class="c1">// or use .FromFile() and pass the path</span>
</span></span><span class="line"><span class="cl">    <span class="p">.</span><span class="n">CreateScoped</span><span class="p">(</span><span class="n">SheetsService</span><span class="p">.</span><span class="n">ScopeConstants</span><span class="p">.</span><span class="n">Spreadsheets</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">using</span> <span class="nn">var</span> <span class="n">service</span> <span class="p">=</span> <span class="k">new</span> <span class="n">SheetsService</span><span class="p">(</span><span class="k">new</span> <span class="n">BaseClientService</span><span class="p">.</span><span class="n">Initializer</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">HttpClientInitializer</span> <span class="p">=</span> <span class="n">credential</span>
</span></span><span class="line"><span class="cl">    <span class="p">});</span>
</span></span></code></pre></div><p>When retrieving this file, also take note of the service account email address (the property <code>client_email</code> in the JSON key file). You will need to share any google documents with this email address for the account to have access to them.</p>
<h2 id="spreadsheet-sheet-and-range-identifiers">Spreadsheet, Sheet, and Range Identifiers</h2>
<p>There are three kinds of identifiers you need to be aware of (<a href="https://developers.google.com/sheets/api/guides/concepts">documentation here</a>):</p>
<ul>
<li><strong>Spreadsheet</strong> - This is the top-level document identifier. You&rsquo;ll find it in the URL for the Google Sheets Spreadsheet.</li>
<li><strong>Sheet</strong> - This is the tab within the Spreadsheet, also identified by the gid in the URL. It&rsquo;s usually passed as the first part of the range (before the <code>!</code>). I&rsquo;ve found that an empty string refers to the first (auto-generated) sheet in the document (gid 0), or you can use the name of the sheet. I haven&rsquo;t had success passing the gid (instead of the name) even though the documentation says it&rsquo;s supported.</li>
<li><strong>Range</strong> - Most of the API calls require a range. This refers to a rectangular group of Cells, and there are two ways to specify them:
<ul>
<li><strong>A1 notation</strong> is the most familiar and most supported. For example <code>Sheet1!A1:D1</code> will refer to the first four Cells in the first Row of the Sheet named &ldquo;Sheet1&rdquo;.</li>
<li><strong>R1C1 notation</strong> is another syntax that can be used with some api methods. The above example would look like <code>Sheet1!R1C1:R1C4</code> in R1C1 notation. It can be pretty handy when doing computed updates because you don&rsquo;t have to convert the column index to a letter/letters. Unfortunately, it doesn&rsquo;t seem to be supported by all the API methods.</li>
</ul>
</li>
</ul>
<p>If you need to compute the column letter(s) for a given index, you can use this code:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="cl"><span class="kd">private</span> <span class="kd">static</span> <span class="kt">string</span> <span class="n">GetColumnNameFromIndex</span><span class="p">(</span><span class="kt">int</span> <span class="n">columnIndex</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kd">const</span> <span class="kt">int</span> <span class="n">numberOfLetters</span> <span class="p">=</span> <span class="m">26</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">columnIndex</span> <span class="p">&gt;=</span> <span class="n">numberOfLetters</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">GetColumnNameFromIndex</span><span class="p">(</span><span class="n">columnIndex</span> <span class="p">/</span> <span class="n">numberOfLetters</span> <span class="p">-</span> <span class="m">1</span><span class="p">)</span> <span class="p">+</span>
</span></span><span class="line"><span class="cl">               <span class="n">GetColumnNameFromIndex</span><span class="p">(</span><span class="n">columnIndex</span> <span class="p">%</span> <span class="n">numberOfLetters</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kd">const</span> <span class="kt">char</span> <span class="n">firstColumnLetter</span> <span class="p">=</span> <span class="sc">&#39;A&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">((</span><span class="kt">char</span><span class="p">)(</span><span class="n">firstColumnLetter</span> <span class="p">+</span> <span class="n">columnIndex</span><span class="p">)).</span><span class="n">ToString</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><h2 id="update-types">Update Types</h2>
<p>There are three kinds of changes you can make to a sheet:</p>
<ul>
<li><strong>Update</strong> - this is the simplest, allowing you to update a single range of cells. Use these sparingly. Batch updates will be much more efficient where they&rsquo;re possible.</li>
<li><strong>Batch Update</strong> - this method takes an array of range update blocks and processes them all together.</li>
<li><strong>Append</strong> - This method allows you to stick rows on the end of the document. It requires a range, but it&rsquo;s okay to use row 1. It will automatically stick the new values at the end of the sheet for you.</li>
</ul>
<h2 id="etags">ETags</h2>
<p>All change requests and get responses include an ETag property. In an ideal world, you would include the ETag from your get request when you send an update so that any changes that have occurred since are taken into account. Unfortunately, this property never seems to be set during get requests, and I couldn&rsquo;t figure out how to make it work.</p>
<p>In all of my testing, the APIs were responding quickly enough that it likely wouldn&rsquo;t matter. If you have a much longer process between gets and updates, you may need to figure this out. Please comment below if you do!</p>
<h2 id="performance--update-size">Performance / Update Size</h2>
<p>I couldn&rsquo;t find any documentation on the limits of the API, so I did some rough testing instead. I found that a batch update with 10,000 cell updates in it (each as a separate change in the request) took only 1.9 seconds. This goes way beyond what my client could ever need.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Google sheets are a great way to share data from an automated process. Although it took a little while to figure out how to read and write data at all, once I had it working, the Google Sheets API worked great. Even better, my client was thrilled with the results.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/.net" term=".net" label=".net" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/c" term="c" label="c#" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/consulting" term="consulting" label="consulting" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Using WeeChat and weechat-android on Windows 11]]></title>
            <link href="https://jessemcdowell.ca/2024/09/Using-WeeChat-on-Windows-11/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/12/Switching-from-Google-Podcasts-to-Spotify/?utm_source=atom_feed" rel="related" type="text/html" title="Switching from Google Podcasts to Spotify" />
            
                <id>https://jessemcdowell.ca/2024/09/Using-WeeChat-on-Windows-11/</id>
            
            
            <published>2024-09-05T17:43:38-07:00</published>
            <updated>2024-10-06T02:25:25-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>Internet Relay Chat (IRC) is an old text-based chat system that, while seemingly in decline, still has many users around the world. It&rsquo;s not as convenient as modern alternatives, but for those of us that are familiar with it, and especially those of us that have friends there, it&rsquo;s still a very viable way to communicate.</p>
<p>There are a number of IRC clients available. On windows, <a href="https://www.mirc.com/">mIRC</a> is popular and easy to set up, but it shows its age. <a href="https://www.irccloud.com/">IRCCloud</a> is very convenient, especially if you want to use a mobile device, but it lacks some common features, and you are limited to two servers on the free account. There are other options, of course, but they all have limitations.</p>
<p>If you like doing things the hard way, you can use <a href="https://weechat.org/">WeeChat</a> like me. It&rsquo;s powerful, has a bunch of plugins, and importantly, is still maintained. It also has a relay protocol that allows remote clients (like one on your phone) to read and respond to messages when you need to step away. It is, however, a terminal application, so it can take some getting used to.</p>
<p>WeeChat is built for Linux, but it is still very possible to run it on modern Windows. I&rsquo;m confident you can get it working with the following steps.</p>
<div class="table-of-contents">
    <nav id="TableOfContents">
  <ul>
    <li><a href="#running-weechat-in-a-podman-container">Running WeeChat in a Podman Container</a>
      <ul>
        <li><a href="#why-not-use-wsl-directly">Why not use WSL directly</a></li>
      </ul>
    </li>
    <li><a href="#using-weechat-relay-and-a-mobile-client">Using WeeChat Relay and a mobile client</a></li>
    <li><a href="#setting-up-weechat-relay-with-tls">Setting up WeeChat Relay with TLS</a></li>
  </ul>
</nav>
</div>

<h2 id="running-weechat-in-a-podman-container">Running WeeChat in a Podman Container</h2>
<p>The best method I&rsquo;ve found for running WeeChat is inside a Podman container. <a href="https://podman.io/">Podman</a> is a container engine similar to Docker, but free on Windows. Setting it up to run WeeChat is rather easy:</p>
<ol>
<li>(Recommended, but not required): Install <a href="https://github.com/microsoft/terminal">Windows Terminal</a>:
<code>winget install Microsoft.WindowsTerminal</code></li>
<li>Download and install the Podman CLI from <a href="https://github.com/containers/podman/releases">the Podman Releases page</a>. See <a href="https://github.com/containers/podman/blob/main/docs/tutorials/podman-for-windows.md">the Podman installation instructions</a> for more info.</li>
<li>Create a directory for storing WeeChat configuration and logs. I use <code>$HOME/OneDrive/WeeChat</code> in the examples here, but you can use any location you like</li>
<li>Run the following in Windows Terminal (PowerShell). Or better yet, save it as a script somewhere in your path for easy use later:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-PowerShell" data-lang="PowerShell"><span class="line"><span class="cl"><span class="k">if</span> <span class="p">((</span><span class="n">podman</span> <span class="n">machine</span> <span class="n">info</span> <span class="p">|</span> <span class="nb">Out-String</span><span class="p">).</span><span class="py">Contains</span><span class="p">(</span><span class="s1">&#39;machinestate: Stopped&#39;</span><span class="p">))</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">podman</span> <span class="n">machine</span> <span class="nb">start
</span></span></span><span class="line"><span class="cl"><span class="nb"></span><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">podman</span> <span class="n">run</span> <span class="p">-</span><span class="n">-rm</span> <span class="n">-it</span> <span class="p">-</span><span class="n">-pull</span> <span class="n">newer</span> <span class="p">-</span><span class="n">-env</span> <span class="s1">&#39;WEECHAT_HOME=/home/user/weechat&#39;</span> <span class="n">-v</span> <span class="nv">$HOME</span><span class="p">/</span><span class="n">OneDrive</span><span class="p">/</span><span class="n">WeeChat</span><span class="err">:</span><span class="p">/</span><span class="n">home</span><span class="p">/</span><span class="n">user</span><span class="p">/</span><span class="n">weechat</span> <span class="p">-</span><span class="n">-tz</span> <span class="n">local</span> <span class="n">weechat</span><span class="p">/</span><span class="n">weechat</span><span class="err">:</span><span class="nb">latest-alpine</span> <span class="n">weechat</span> <span class="nv">@args</span>
</span></span></code></pre></div></li>
</ol>
<p>And that&rsquo;s it. You should now be running WeeChat.</p>
<p>Some notes about the startup script:</p>
<ul>
<li>The first block of the script runs the <code>podman machine start</code> command for you automatically if needed. This saves you a step the first time you run podman after each reboot of your computer</li>
<li>The container is being re-created and deleted every time it&rsquo;s run (because of <code>podman run</code> and <code>--rm</code>), and new images are automatically downloaded (because of <code>--pull newer</code>). You could create a container and start it each time, but continually recreating it this way ensures that you get updates automatically when new images are published</li>
<li>the left half of <code>-v</code> specifies where data will be saved on your computer (outside the container). This is important, because everything else inside the container will be reset every time you run this script</li>
<li>The <code>--env</code> command forces WeeChat (via a supported environment variable) to write everything (configuration and logs) into a single folder inside the container which allows the file mapping with a single <code>-v</code></li>
<li>(Added 2024-10) <code>weechat @args</code> at the end of the command line allows the PowerShell script to pass arguments through to WeeChat. I have been using this to sometimes send the <code>--no-connect</code> option when I want to prevent auto-connect or auto-join.</li>
</ul>
<h3 id="why-not-use-wsl-directly">Why not use WSL directly</h3>
<p>It is absolutely possible to run WeeChat in WSL, but I prefer the Podman approach because:</p>
<ul>
<li>it&rsquo;s easier to expose the WeeChat Relay port - I found it difficult to share a port for an app in WSL to my local network</li>
<li>you get updates to WeeChat automatically without running <code>apt update; apt upgrade</code></li>
<li>you are certain no state is being secretly left behind inside the WSL environment</li>
<li>you don&rsquo;t have to worry about breaking WeeChat if you break WSL, and don&rsquo;t lose anything if you decide to reset it</li>
<li>it uses less space. The normal Ubuntu image used by WSL is pretty big, which is wasteful if you aren&rsquo;t using it for anything else</li>
</ul>
<h2 id="using-weechat-relay-and-a-mobile-client">Using WeeChat Relay and a mobile client</h2>
<p>One of the main reasons I went with WeeChat was the WeeChat Relay protocol and clients like <a href="https://github.com/ubergeek42/weechat-android">weechat-android</a>. This allows you to keep WeeChat running on your main computer, but access it remotely to check for or respond to messages. I find this handy when I need to do a chore or take a break. Because IRC depends on persistent connections, it is hard to use it (reliably) on a mobile device directly. And because WeeChat is exposing the relay, people don&rsquo;t even know that you&rsquo;re switching devices.</p>
<p>You can set up the relay service with the following steps. There are also <a href="https://weechat.org/files/doc/weechat/stable/weechat_user.en.html#relay">detailed instructions in the documentation</a> if you have specific questions.</p>
<ol>
<li>Make up a password</li>
<li>Use the following commands in WeeChat to store the password, and use it for your relay server:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/secure set relay new_password
</span></span><span class="line"><span class="cl">/set relay.network.password &#34;${sec.data.relay}&#34;
</span></span></code></pre></div></li>
<li>Use the following command in WeeChat to enable WeeChat Relay:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/relay add weechat 9500
</span></span></code></pre></div></li>
<li>Run the following in PowerShell (as Administrator) to allow WeeChat Relay through the Windows firewall, and to proxy any traffic from the local network to the relay in the container:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-PowerShell" data-lang="PowerShell"><span class="line"><span class="cl"><span class="nb">New-NetFirewallRule</span> <span class="n">-DisplayName</span> <span class="s2">&#34;WeeChat Relay&#34;</span> <span class="n">-Direction</span> <span class="n">Inbound</span> <span class="n">-Protocol</span> <span class="n">TCP</span> <span class="n">-LocalPort</span> <span class="mf">9500</span> <span class="n">-Action</span> <span class="n">Allow</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">netsh</span> <span class="n">interface</span> <span class="n">portproxy</span> <span class="nb">set </span><span class="n">v4tov4</span> <span class="n">listenport</span><span class="p">=</span><span class="mf">9500</span> <span class="n">listenaddress</span><span class="p">=</span><span class="mf">0.0</span><span class="p">.</span><span class="py">0</span><span class="p">.</span><span class="py">0</span> <span class="n">connectport</span><span class="p">=</span><span class="mf">9500</span> <span class="n">connectaddress</span><span class="p">=</span><span class="mf">127.0</span><span class="p">.</span><span class="py">0</span><span class="p">.</span><span class="py">1</span>
</span></span></code></pre></div></li>
<li>Add the following to the <code>podman run</code> command to expose the relay port from the container: <code>-p 9500:9500</code>. The complete command will now be:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-PowerShell" data-lang="PowerShell"><span class="line"><span class="cl"><span class="n">podman</span> <span class="n">run</span> <span class="p">-</span><span class="n">-rm</span> <span class="n">-it</span> <span class="p">-</span><span class="n">-pull</span> <span class="n">newer</span> <span class="p">-</span><span class="n">-env</span> <span class="s1">&#39;WEECHAT_HOME=/home/user/weechat&#39;</span> <span class="n">-v</span> <span class="nv">$HOME</span><span class="p">/</span><span class="n">OneDrive</span><span class="p">/</span><span class="n">WeeChat</span><span class="err">:</span><span class="p">/</span><span class="n">home</span><span class="p">/</span><span class="n">user</span><span class="p">/</span><span class="n">weechat</span> <span class="p">-</span><span class="n">-tz</span> <span class="n">local</span> <span class="n">-p</span> <span class="mf">9500</span><span class="err">:</span><span class="mf">9500</span> <span class="n">weechat</span><span class="p">/</span><span class="n">weechat</span><span class="err">:</span><span class="nb">latest-alpine</span> <span class="n">weechat</span> <span class="nv">@args</span>
</span></span></code></pre></div></li>
<li>Install <a href="https://play.google.com/store/apps/details?id=com.ubergeek42.WeechatAndroid.dev">weechat-android</a> on your Android device. I haven&rsquo;t found an iOS equivalent. If you have one you&rsquo;d recommend, please post it in the comments.</li>
<li>Configure your client. You can use the following settings in weechat-android:
<ul>
<li>Connect type: Plain Connection</li>
<li>Relay host: IP address of the computer running WeeChat (inside your network)</li>
<li>Relay port: 9500</li>
<li>Relay password: The password you created earlier</li>
</ul>
</li>
</ol>
<p>As long as WeeChat is running on your computer, and the client is connected to the same network, you should now be able to connect to the relay.</p>
<p>If you want to access WeeChat relay from outside your home, you&rsquo;ll have to set up some sort of vpn to your home network. I do not suggest opening this port to the internet directly without setting up TLS encryption first.</p>
<h2 id="setting-up-weechat-relay-with-tls">Setting up WeeChat Relay with TLS</h2>
<p>WeeChat supports TLS for the relay protocol, which makes it much safer for connections going over the internet. However, it requires a DNS record that points to your WeeChat instance, and a self-signed certificate that matches the name. This appears to be a security limitation in Android itself, so you&rsquo;ll have to deal with it if you want TLS.</p>
<p>For my own purposes, I stuck an A record in one of my domains that points to the local fixed IP address of my main computer. If you&rsquo;re also using the relay protocol over your home network, you could use a dynamic DNS service like <a href="https://www.duckdns.org/">Duck DNS</a> that allows you to specify the IP address manually. Dynamic DNS will also be helpful if you are opening the port to the internet from your router.</p>
<p>Once you have DNS figured out, generating a self-signed certificate can be done relatively easily.</p>
<ol>
<li>Run the following in PowerShell to start an interactive shell in a temporary container:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-PowerShell" data-lang="PowerShell"><span class="line"><span class="cl"><span class="n">podman</span> <span class="n">run</span> <span class="p">-</span><span class="n">-rm</span> <span class="n">-it</span> <span class="n">-v</span> <span class="nv">$HOME</span><span class="p">/</span><span class="n">OneDrive</span><span class="p">/</span><span class="n">WeeChat</span><span class="err">:</span><span class="p">/</span><span class="n">weechat</span> <span class="p">-</span><span class="n">-entrypoint</span> <span class="n">sh</span> <span class="n">alpine</span><span class="p">/</span><span class="n">openssl</span>
</span></span></code></pre></div><ul>
<li>Note: Make sure the directory mapping (on the left) matches the location on your host machine where you store your WeeChat data.</li>
</ul>
</li>
<li>Run the following with the DNS name you set up:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl"><span class="nb">export</span> <span class="nv">HOSTNAME</span><span class="o">=</span>yourweechatrelay.duckdns.org
</span></span></code></pre></div></li>
<li>Run the following to generate a self-signed certificate in the WeeChat directory:
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">mkdir -p weechat/tls
</span></span><span class="line"><span class="cl">openssl req -x509 -newkey rsa:4096 -nodes <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    -keyout weechat/tls/relay.pem -out weechat/tls/relay.pem <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    -days <span class="m">3650</span> -subj <span class="s2">&#34;/CN=</span><span class="nv">$HOSTNAME</span><span class="s2">&#34;</span> -addext <span class="s2">&#34;subjectAltName=DNS:</span><span class="nv">$HOSTNAME</span><span class="s2">&#34;</span>
</span></span></code></pre></div></li>
<li>Run <code>exit</code> to stop the container</li>
<li>Run the following in WeeChat to replace the relay server with a TLS-enabled relay server
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">/relay del weechat
</span></span><span class="line"><span class="cl">/set relay.network.tls_cert_key &#34;${weechat_config_dir}/tls/relay.pem&#34;
</span></span><span class="line"><span class="cl">/relay add ssl.weechat 9500
</span></span></code></pre></div></li>
</ol>
<p>From weechat-android, you can change the Connect type to &ldquo;WeeChat SSL&rdquo;. The next time you connect, you&rsquo;ll be asked to verify the certificate. After this all communication between WeeChat and your client should be encrypted.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/howto" term="howto" label="howto" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Rocketbook vs Whiteboard]]></title>
            <link href="https://jessemcdowell.ca/2024/08/Rocketbook-vs-Whiteboard/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2024/06/Pragmatic-Potato-Tech-Stack/?utm_source=atom_feed" rel="related" type="text/html" title="The Pragmatic Potato Tech Stack" />
                <link href="https://jessemcdowell.ca/2024/01/My-Experience-with-the-Dvorak-Keyboard-Layout/?utm_source=atom_feed" rel="related" type="text/html" title="My Experience with the Dvorak Keyboard Layout" />
                <link href="https://jessemcdowell.ca/2024/05/Cross-Cutting-Concerns/?utm_source=atom_feed" rel="related" type="text/html" title="Cross-Cutting Concerns - Ten Approaches" />
                <link href="https://jessemcdowell.ca/2024/03/The-True-Cost-of-Dependencies/?utm_source=atom_feed" rel="related" type="text/html" title="The True Cost of Dependencies" />
                <link href="https://jessemcdowell.ca/2023/12/Switching-from-Google-Podcasts-to-Spotify/?utm_source=atom_feed" rel="related" type="text/html" title="Switching from Google Podcasts to Spotify" />
            
                <id>https://jessemcdowell.ca/2024/08/Rocketbook-vs-Whiteboard/</id>
            
            
            <published>2024-08-06T22:24:33-07:00</published>
            <updated>2024-08-06T22:24:33-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>I have always liked using whiteboards. They are a great tool for communicating, but also great for exploring thoughts and stimulating creativity. There are computerized equivalents, and they have their advantages, but I&rsquo;ve never found them as effective because I get distracted too easily. I can spend more time fiddling with the lines and colours than working on my actual problem.</p>
<p>I like whiteboards so much that I bought a stack of them, and an artist&rsquo;s portfolio bag to carry them around. I imagined popping out a few drawings at a meeting, or handing boards around for a creative session. I still love the idea, but in more than twenty years I&rsquo;ve never even taken them out of the house.</p>
<p>These days I use a <a href="https://getrocketbook.com/">Rocketbook</a> for most of my creative explorations. I received one as a gift at work a few years ago, and have been in love with it ever since. It looks and performs like a normal notebook, but it&rsquo;s easy to scan the pages, and the pages can be reused after erasing them with a wet cloth. This is achievable because of some clever design and a specially optimized app.</p>
<p>If I&rsquo;m working at home, or travelling, the Rocketbook is very convenient. It&rsquo;s also good when I&rsquo;m working in an office and need to sketch something. More than just replacing a whiteboard, it can be helpful for notes. Entering notes in a laptop is usually better, but if I have a couple of quick notes, or don&rsquo;t have a laptop handy, the Rocketbook will do the job.</p>
<p>A whiteboard is still the best tool for an in-person meeting. Having the drawings out in the open where anyone can grab a pen and add to it is very convenient. Online meetings that involve collaboration don&rsquo;t work very well with a whiteboard, or a Rocketbook for that matter. In these situations I prefer a virtual whiteboard. It does, however, also require some preparation before the meeting to prevent wasting precious time in the meeting.</p>
<p>If you&rsquo;re interested in a Rocketbook for yourself, I suggest looking at the Rocketbook Fusion first. It has a mix of lined pages for notes and grid pages for drawings. I like the executive size for its portability. The only hitch is that you need a hard surface to write effectively. This isn&rsquo;t a problem at a desk, but you may want to get a hard cover if you need to take notes on the move. If you&rsquo;re doing a lot of design work at a desk, a letter-sized Rocketbook Matrix may be a better choice.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/design" term="design" label="design" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/gadgets" term="gadgets" label="gadgets" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/reviews" term="reviews" label="reviews" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[The Pragmatic Potato Tech Stack]]></title>
            <link href="https://jessemcdowell.ca/2024/06/Pragmatic-Potato-Tech-Stack/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2024/05/Cross-Cutting-Concerns/?utm_source=atom_feed" rel="related" type="text/html" title="Cross-Cutting Concerns - Ten Approaches" />
                <link href="https://jessemcdowell.ca/2024/03/The-True-Cost-of-Dependencies/?utm_source=atom_feed" rel="related" type="text/html" title="The True Cost of Dependencies" />
                <link href="https://jessemcdowell.ca/2023/11/Using-Architectural-Decision-Records/?utm_source=atom_feed" rel="related" type="text/html" title="Using Architectural Decision Records" />
                <link href="https://jessemcdowell.ca/2023/11/This-Blog-Hexo-generated-static-site-hosted-on-GitHub-Pages/?utm_source=atom_feed" rel="related" type="text/html" title="This Blog: Hexo-generated static site hosted on GitHub Pages" />
                <link href="https://jessemcdowell.ca/2023/08/My-Architectural-Report-Template/?utm_source=atom_feed" rel="related" type="text/html" title="My Architectural Report Template" />
            
                <id>https://jessemcdowell.ca/2024/06/Pragmatic-Potato-Tech-Stack/</id>
            
            
            <published>2024-06-24T12:02:43-07:00</published>
            <updated>2024-06-24T12:02:43-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>I <a href="/2023/11/Announcing-Pragmatic-Potato-Software/">recently launched my new company</a>, <a href="https://pragmaticpotato.com/">Pragmatic Potato Software Inc</a>. The creation of a company itself is pretty easy, but setting up everything you need to do business can become overwhelming quickly. There are a lot of compelling options available, each promising the moon. It&rsquo;s not that simple though.</p>
<p>I&rsquo;m going to be writing about the technology stack I&rsquo;m using to run my company, and why I made the choices I did. There is a lot more to a company than its technology, but I&rsquo;m not an expert in setting those up.</p>
<p>I made these choices for my company, keeping in mind my needs and values. I would probably recommend something similar for another small company, but it would depend. I didn&rsquo;t perform an exhaustive search for each decision. Most options are going to be good enough, and even if I make a poor choice, I can replace most of these with a few hours of work while my company is small. I did put some effort into it though, and I&rsquo;m happy to share that with you.</p>
<p>Here is some more of the context that guided my decisions:</p>
<ul>
<li>Pragmatic Potato Software is a small (single employee) software architecture and development consulting company based in Burnaby, Canada.</li>
<li>I want to minimize the time I&rsquo;m spending managing infrastructure because I&rsquo;d rather spend that time helping customers and potentially earning income.</li>
<li>I am pretty technically capable, and can handle setting up and hosting my own infrastructure. I also don&rsquo;t need to worry much about usability: I am happy to use a code editor when I want to update the text on my website.</li>
<li>I have modest expectations for traffic and customer interactions, so I should be able to use free or low-cost services for most things. I would like to keep my costs down.</li>
</ul>
<div class="table-of-contents">
    <nav id="TableOfContents">
  <ul>
    <li><a href="#domain-registration-cloudflare-and-porkbun">Domain Registration: Cloudflare and Porkbun</a></li>
    <li><a href="#website-hugo-static-site-hosted-on-cloudflare-pages">Website: Hugo static site hosted on Cloudflare Pages</a></li>
    <li><a href="#contact-form-fabform">Contact Form: FabForm</a></li>
    <li><a href="#stock-photography-unsplash-and-squoosh">Stock Photography: Unsplash and Squoosh</a></li>
    <li><a href="#website-analytics-cloudflare-web-analytics">Website Analytics: Cloudflare Web Analytics</a></li>
    <li><a href="#email-zoho-workplace">Email: Zoho Workplace</a></li>
    <li><a href="#document-editing--storage-zoho-workplace--evernote">Document Editing &amp; Storage: Zoho Workplace &amp; Evernote</a></li>
    <li><a href="#task-management-trello--evernote">Task Management: Trello &amp; Evernote</a></li>
    <li><a href="#video-conferencing-zoho-meeting">Video Conferencing: Zoho Meeting</a></li>
    <li><a href="#appointment-scheduling-zoho-bookings">Appointment Scheduling: Zoho Bookings</a></li>
    <li><a href="#bookkeeping-outsourced">Bookkeeping: Outsourced</a></li>
  </ul>
</nav>
</div>

<h2 id="domain-registration-cloudflare-and-porkbun">Domain Registration: Cloudflare and Porkbun</h2>
<p>For any kind of web presence, you need a domain name. Of all the registrars I&rsquo;ve used, my favourite is <a href="https://www.cloudflare.com/products/registrar/">Cloudflare Registrar</a>. They famously don&rsquo;t make any profit on their domain offering, so it is also the best price you can get. I used them for the <code>pragmaticpotato.com</code> domain.</p>
<p>Because I have a Canadian company, I also registered <code>pragmaticpotato.ca</code>, and have it set up to redirect to the .com site. At the time of this writing Cloudflare can&rsquo;t register .ca domains, so I am using Porkbun for that one.</p>
<h2 id="website-hugo-static-site-hosted-on-cloudflare-pages">Website: Hugo static site hosted on Cloudflare Pages</h2>
<p><a href="/2023/11/This-Blog-Hexo-generated-static-site-hosted-on-GitHub-Pages/">I&rsquo;ve written before about my preference for static websites</a>. They are the simplest thing that can possibly work, and that&rsquo;s often a good starting point. I can&rsquo;t imagine anything I&rsquo;d want to do on this website that&rsquo;d require a dynamic back end, so this should be good enough for a long time.</p>
<p>I originally planned to use Hexo for the website because I already use it for this blog, but I had trouble finding a template that I liked for a business landing page. I ended up using <a href="https://gohugo.io/">Hugo</a>. It is very similar to Hexo, but I found several templatse that I liked.</p>
<p>Having used both, I now prefer Hugo. One feature I appreciate is its ability to extend templates through partials rather than modifying the templates directly. This allows some pretty direct customizations, and should make it easier to take updates in the future. I used this functionality to tune the page metadata for search engines and social links. In the template I chose the templates was broken up into several partials to make these sorts of customizations trivial.</p>
<p>The hardest part of putting the website together was writing the text, and I went through several iterations to get it just the way I wanted. Then my friends told me it was garbage, so I rewrote most of it again!</p>
<p>I&rsquo;m using <a href="https://pages.cloudflare.com/">Cloudflare Pages</a> to host the content. It&rsquo;s very similar to GitHub Pages (which I use for this blog), but I like that it&rsquo;s a bit more configurable. I also found the build system a little easier to set up. I liked it enough that I will probably end up moving this blog over at some point too.</p>
<h2 id="contact-form-fabform">Contact Form: FabForm</h2>
<p>I&rsquo;m using <a href="https://fabform.io/">FabForm</a> for the contact form on the website. It is a simple service that was trivial to set up. I also find the one-time pricing much more reasonable than the higher (over time) monthly fees of other popular options. Using this allows me to accept messages on the website without needing a dynamic back-end.</p>
<h2 id="stock-photography-unsplash-and-squoosh">Stock Photography: Unsplash and Squoosh</h2>
<p>I used a few stock photos on the website to make it feel more engaging. <a href="https://unsplash.com/">Unsplash</a> has a massive library of free, high-resolution photos taken by professionals. I made a point of including an attribution page, but that isn&rsquo;t necessary with their licence. I used <a href="https://squoosh.app/">Squoosh</a> to resize and compress the images for efficient load times.</p>
<p>I also hired a professional photographer to take some pictures of me to make the website more personal. This was a particularly fun process, and I&rsquo;m thrilled with the results. The photographer was very reasonable. My new clothing was the most expensive part.</p>
<h2 id="website-analytics-cloudflare-web-analytics">Website Analytics: Cloudflare Web Analytics</h2>
<p>I am using <a href="https://www.cloudflare.com/web-analytics/">Cloudflare Website Analytics</a> for the website, mostly because it was easily available. I also appreciate that they collect minimal information, and in a way that doesn&rsquo;t require popups to comply with modern privacy laws.</p>
<h2 id="email-zoho-workplace">Email: Zoho Workplace</h2>
<p>Microsoft 365 and Google Workspace are the two big players for email and office application, and I did consider each of them. I ended up going with <a href="https://www.zoho.com/workplace/">Zoho Workplace</a> because it is much cheaper and comes bundled with more stuff.</p>
<p>Setting it up was an easy process. Even setting up email for my custom domain was easy. I had it all up and working in less than an hour.</p>
<p>I don&rsquo;t recommend trying to run your own email server, even for medium-sized businesses. Email used to be an open system that anyone could participate in, but with all the spam and especially with the spam prevention mechanisms today, it can be difficult to keep your emails out of spam boxes.</p>
<h2 id="document-editing--storage-zoho-workplace--evernote">Document Editing &amp; Storage: Zoho Workplace &amp; Evernote</h2>
<p>I&rsquo;m gradually moving my (few) stored files over to <a href="https://www.zoho.com/workdrive/">Zoho WorkDrive</a>. I was using a little bit of Google and a bit of Microsoft, but ultimately found their free options too limiting. The final straw for me was editing a contract. I spent a couple of hours trying to get the numbered lists and cross-section links right in the free online Microsoft Word and Google Docs before giving up. Zoho Writer has limitations too, but it proved the easiest to use.</p>
<p>Almost all of my work notes and planning content is in <a href="https://evernote.com/">Evernote</a>, which I&rsquo;ve been using for many years. It&rsquo;s not great for some kinds of content (like sheets), but it is extremely searchable, which makes it fast for pretty much all other information.</p>
<h2 id="task-management-trello--evernote">Task Management: Trello &amp; Evernote</h2>
<p>I&rsquo;ve been using <a href="https://trello.com/">Trello</a> for years to organize myself. I use it primarily to track the things I&rsquo;m working on in the short term. It&rsquo;s especially easy to slap cards into a list and put them in a sensible order. I mix personal and professional tasks in the same list to reduce friction as I go through the day.</p>
<p>For my longer term backlog, I&rsquo;m using notes in <a href="https://evernote.com/">Evernote</a>. It isn&rsquo;t a great tool for project management, but it&rsquo;s good enough for now.</p>
<h2 id="video-conferencing-zoho-meeting">Video Conferencing: Zoho Meeting</h2>
<p>Because I&rsquo;m already using Zoho Workplace, I get <a href="https://www.zoho.com/meeting/">Zoho Meeting</a> included, and it works great. It looks and performs a lot like Zoom, but I don&rsquo;t have to pay extra for it. It&rsquo;s not quite as smooth as Google Meet (which has a particularly streamlined experience), but it has a few more features that I sometimes want.</p>
<h2 id="appointment-scheduling-zoho-bookings">Appointment Scheduling: Zoho Bookings</h2>
<p>Zoho also has <a href="https://www.zoho.com/bookings/">Zoho Bookings</a> which allows customers to schedule meetings with me themselves. This can save a lot of emailing when someone needs to talk to me. I have it integrated with my Google calendar (which I use for my personal and family schedules), so it can automatically avoid conflicts with my other obligations.</p>
<h2 id="bookkeeping-outsourced">Bookkeeping: Outsourced</h2>
<p>Another key part of a business is keeping appropriate financial records. I spent some time looking into software for managing this, but I have decided so far not to bother. My accountant is happy to take care of this for me, and they can do it far faster than I can. Even though I&rsquo;m the kind of person that likes to keep records up to date, and to see the numbers for myself, I would rather spend that time helping customers instead.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/architecture" term="architecture" label="architecture" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/design" term="design" label="design" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/pragmatic-potato" term="pragmatic-potato" label="pragmatic potato" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/reviews" term="reviews" label="reviews" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Cross-Cutting Concerns - Ten Approaches]]></title>
            <link href="https://jessemcdowell.ca/2024/05/Cross-Cutting-Concerns/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2024/03/The-True-Cost-of-Dependencies/?utm_source=atom_feed" rel="related" type="text/html" title="The True Cost of Dependencies" />
                <link href="https://jessemcdowell.ca/2023/11/Using-Architectural-Decision-Records/?utm_source=atom_feed" rel="related" type="text/html" title="Using Architectural Decision Records" />
                <link href="https://jessemcdowell.ca/2023/08/My-Architectural-Report-Template/?utm_source=atom_feed" rel="related" type="text/html" title="My Architectural Report Template" />
                <link href="https://jessemcdowell.ca/2023/11/This-Blog-Hexo-generated-static-site-hosted-on-GitHub-Pages/?utm_source=atom_feed" rel="related" type="text/html" title="This Blog: Hexo-generated static site hosted on GitHub Pages" />
                <link href="https://jessemcdowell.ca/2023/10/Getting-Unstuck-Without-a-Rubber-Duck/?utm_source=atom_feed" rel="related" type="text/html" title="Getting Unstuck Without a Rubber Duck" />
            
                <id>https://jessemcdowell.ca/2024/05/Cross-Cutting-Concerns/</id>
            
            
            <published>2024-05-27T13:18:08-07:00</published>
            <updated>2024-05-27T13:18:08-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>One often (and ironically) repeated rule in programming is: don&rsquo;t repeat yourself. We repeat it so much we even have an abbreviation: DRY. There are good reasons for this advice. Duplicating and modifying code can be a quick and easy way to get a feature done, but it can also lead to problems over time. It&rsquo;s harder to understand code when the valuable logic is mixed with reams of low-value boilerplate. Subtle differences can also sneak in, leading to inconsistent behaviour across the application. Things get even more difficult if you want to make a change.</p>
<p>Cross-cutting concerns are the requirements that span multiple features. This includes things such as error handling, logging, profiling, security policy, and so on. We often want these things to be consistent, and we occasionally want to make systematic adjustments to them. Unfortunately, because of their nature, they can be challenging to centralize.</p>
<p>There are a bunch of tools and techniques for dealing with cross-cutting concerns, and I&rsquo;ve made a list of them. I recommend thinking carefully about which approach(es) you want to use. It may be challenging to switch approaches once one has become widely used in your code base.</p>
<p>Once you abstract away some of these big requirements, you start to think about similar techniques every time you repeat a pattern. This is a deep rabbit hole to go down, but I can&rsquo;t say it&rsquo;s entirely wrong to plunge the depths. Things like classes that report changes, hash functions, even writing property getters and setters can be convenient to automate. This kind of code is often not fun to write, and because we often put little thought into it, honest mistakes can creep in. Worse, it&rsquo;s eve more tedious to test these sorts of things, and the bugs they cause can be tough ones to reproduce and find.</p>
<p>I am focusing on the broader-scoped cross-cutting concerns in this post, but the same tools and techniques can be applied to many places where you need to implement the same behaviour across multiple bits of code.</p>
<div class="table-of-contents">
    <nav id="TableOfContents">
  <ul>
    <li><a href="#aspect-oriented-programming-aop-tools">Aspect Oriented Programming (AOP) tools</a></li>
    <li><a href="#extract-into-a-helper-function">Extract into a helper function</a></li>
    <li><a href="#use-an-established-library">Use an established library</a></li>
    <li><a href="#use-a-common-interface-library">Use a common interface library</a></li>
    <li><a href="#put-it-in-a-base-class">Put it in a base class</a></li>
    <li><a href="#put-the-logic-higher-or-lower-in-the-stack">Put the logic higher or lower in the stack</a></li>
    <li><a href="#use-code-generation-tools">Use code generation tools</a></li>
    <li><a href="#generate-boilerplate-with-ai-powered-tools">Generate boilerplate with AI-powered tools</a></li>
    <li><a href="#use-automated-tests-to-enforce-consistency">Use automated tests to enforce consistency</a></li>
    <li><a href="#do-nothing">Do nothing</a></li>
  </ul>
</nav>
</div>

<h2 id="aspect-oriented-programming-aop-tools">Aspect Oriented Programming (AOP) tools</h2>
<p>Aspect Oriented Programming is an approach that tackles cross-cutting concerns head on. The way they work varies by framework, but essentially they inject behaviour by either wrapping functions or modifying compiled code. You attach behaviours either using language features (ex: Attributes, Annotations), or through pattern matching (ex: namespace, class name, function name, function signature). There are multiple tools available in many common languages, so you should have no trouble finding something for your project.</p>
<p>When they work, these tools can be magical. Slapping a security attribute on a function is far easier than interrogating some security service and writing your own error messages. It also helps us keep our code cleaner: the account balance class does need to be secure, but security isn&rsquo;t really a core part of the logic, and it&rsquo;s nice to keep them untangled.</p>
<p>The problem with this approach is that it can be a little too magical. A developer joining the team could find it difficult to understand why some behaviour is happening where there is no apparent code causing it. It is also not always straightforward to debug this kind of code when it doesn&rsquo;t work (but some tools perform better than others). It can be even more confusing when some shared logic is executed where it&rsquo;s not supposed to.</p>
<p>If you&rsquo;re going this route, there are two flavours to consider: some tools modify the compiled code, and others wrap objects at runtime (often in cooperation with your dependency injection system).</p>
<p>Wrapping objects at runtime is helpful because they can sometimes wrap code you didn&rsquo;t write. They may also make it possible to dynamically add or change behaviours, either via configuration files or your own code. For example, you could inject a policy that wraps all database calls with performance counters, or do this only in a test environment.</p>
<p>Working at (or near) compile time can be a good choice too. Behaviour inserted this way has the benefit of access to the internal bits of your classes, making more powerful policies possible.</p>
<h2 id="extract-into-a-helper-function">Extract into a helper function</h2>
<p>If you don&rsquo;t want magical code, you can still centralize repeated logic with your own helper functions. This gives you one place to make changes, and is easier for new developers to follow. For logic that needs to wrap your code (ex: profiling), you can write a helper function that takes a lambda or delegate. For example (implementation simplified for illustration):</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="cl"><span class="kd">public</span> <span class="kd">static</span> <span class="k">void</span> <span class="n">Main</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="n">Profile</span><span class="p">(</span><span class="s">&#34;Application Startup&#34;</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="c1">// startup code here</span>
</span></span><span class="line"><span class="cl">    <span class="p">});</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="kd">public</span> <span class="kd">static</span> <span class="k">void</span> <span class="n">Profile</span><span class="p">(</span><span class="kt">string</span> <span class="n">description</span><span class="p">,</span> <span class="n">Action</span> <span class="n">profiledFunction</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">var</span> <span class="n">executionTime</span> <span class="p">=</span> <span class="n">Stopwatch</span><span class="p">.</span><span class="n">StartNew</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">profiledFunction</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">finally</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">Log</span><span class="p">(</span><span class="s">$&#34;{description} took {executionTime.ElapsedMilliseconds}ms&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>The difficulty with this approach is that it can be deceptively challenging to design a good helper function. The implementation can be changed easily so long as you don&rsquo;t need to change the function signature. For example, if you are writing your own logging helper, what arguments do you require? Do you want the class name? Do you want a stack trace? Do you want to support string formatting inline? Every additional argument adds more effort for developers using the helper function, however, if you don&rsquo;t require enough information, you may not be able to make that desired change to your centralized logic without making expensive sweeping changes anyway.</p>
<p>This technique may be best for cases where you have a bit of duplication in a handful of areas. If your helper function is being called from hundreds or thousands of places, you may want a technique that is a bit more robust.</p>
<h2 id="use-an-established-library">Use an established library</h2>
<p>Picking a library and using it can be a good approach for common concerns. Logging is an example where this makes sense. There are lots of fantastic, well-established libraries available. They have mature APIs, tested and refined in thousands of apps. Every framework I&rsquo;ve seen has been configurable for most common use cases. Many also support plugins for more specialized needs.</p>
<p>Logging is also the kind of thing that developers want to interact with: it can be helpful to throw a bit of extra information into the logs when you&rsquo;re trying to track down an elusive bug. For comparison, if you are wrapping your code with logging using an aspect oriented framework, you may not have a natural way to pass specific information in to a log message.</p>
<p>Compared to aspect oriented programming, however, you do have to invoke the library whenever you want to use it. If you wanted to log all calls to your service layer, you would have to call your logging framework for all your service methods (or use one of the other automatic techniques in this list).</p>
<p>Another drawback to this approach is that you can become stuck with the library you choose because the cost of replacing it will be prohibitive. This may or may not be the kind of thing you have to worry about though. I have seen more than zero teams switch logging frameworks in an established project, but I&rsquo;ve ever seen an example where it was necessary.</p>
<h2 id="use-a-common-interface-library">Use a common interface library</h2>
<p>If you want the stability of a mature API, but really might need the ability to change your implementation, you could look for a common interface intermediary. There are lots of examples of community-maintained interface projects, and even some built into base frameworks. With these, most of your code gets implemented referencing the common interface, but the actual implementation is wired up through a plugin. Java&rsquo;s <a href="https://commons.apache.org/proper/commons-logging/">Apache Commons Logging</a> is a great example of this.</p>
<p>There are also more advanced interfaces that do more than wrap another implementation. <a href="https://opentelemetry.io/">OpenTelemetry</a> is a fantastic library for recording telemetry, traces, and logs. Its greatest benefit is that you can add one or more plugins to expose this data to your various monitoring tools such as a privately hosted Prometheus system, or a commercial observability platform.</p>
<p>A common interface is a particularly useful tool when they are so widely used that common libraries start to use them too. For example, parts of .Net Core are now instrumented using Open Telemetry, so you can pick up valuable data in many helpful areas just by adding a plugin.</p>
<p>When choosing one of these interfaces, make sure you&rsquo;re picking something that&rsquo;s widely used in your component ecosystem. If you are forced to use two (either because you have components that use difference ones, or you decide to change), you might be able to make a simple bridge to stick them together. It is far nicer to pick the right library the first time though.</p>
<p>Another thing to be careful about is that these abstractions can sometimes be complicated. Using an abstract authorization interface could have a lot of bells and whistles you don&rsquo;t need, or aren&rsquo;t even supported in your implementation. The more complicated the interface is, the more likely you&rsquo;ll find diverging usage patterns across your code, and they may not universally work. This erodes some of the benefits of using a common library in the first place.</p>
<h2 id="put-it-in-a-base-class">Put it in a base class</h2>
<p>Inheritance is the classic object-oriented way to share logic, and it can be very effective. Features like abstracts and virtual functions make it easy to deal with exceptions, which many approaches to centralizing logic have trouble with. It&rsquo;s also quite helpful having the compiler to enforce the rules for you, making it safer to change the structure in the future.</p>
<p>The problem with inheritance is that where some is good, more is definitely not better. It is a powerful tool, but you can hit its limits pretty fast. Most languages only support single inheritance, and the ones that support multiple inheritance regret it. This means that you can only inject one set of behaviours into a class. If you want logging on some classes, but logging and authorization on others, you might need multiple layers of base classes to pile the features on or off. If you have a handfull of concerns, you could find yourself making lots of base classes to pick and choose where they go. This makes this technique generally unsuitable for the classic cross-cutting concerns. Another limitation of inheritance is sometimes it isn&rsquo;t available to you: you might need your classes to derive from another type to participate in some other system.</p>
<p>Another challenge with using base classes for shared code appears when you have dependencies. You either need to include your dependencies in every constructor, or do some tricky stuff with statics to gain access to them. For example, if you are using a base class for authorization, you may need to pass a reference to your authorization service from every implementation of the base class. If you ever wanted to make a change that required a new dependency, you&rsquo;d have to go and change the constructor for every implementation. This isn&rsquo;t fun if it&rsquo;s widely used.</p>
<p>This approach can, however, work well for small-scale repeating concerns. One example that comes to mind is as a foundation for a plugin architecture. Abstract methods are a great way to ensure implementations provide the entry points you require. If you go this route, I suggest keeping your utilities out of the base class to avoid versioning problems.</p>
<h2 id="put-the-logic-higher-or-lower-in-the-stack">Put the logic higher or lower in the stack</h2>
<p>Sometimes you can find a single place higher or lower in the stack to insert some shared logic. For example, for a common requirement to log errors in a web service, your web framework might support adding some code that intercepts every request. This code can check for and log errors before responses are returned. I generally prefer this kind of approach for error handling: logging general errors all over the place takes a lot of effort. It&rsquo;s much nicer when you can let most errors bubble up the stack and know that the application will record and report them correctly.</p>
<p>You can also sometimes use this technique lower in the stack. For example, you might be able to wrap a database connection to add some generic profiling logic for your database interactions.</p>
<h2 id="use-code-generation-tools">Use code generation tools</h2>
<p>Some advanced IDEs have tools for generating code. This makes it easy to insert the exact same code over and over. It also makes it possible to share the patterns across a team.</p>
<p>The code that&rsquo;s generated will be duplicated. This will make some kinds of changes to the pattern harder, but for some cross-cutting concerns that may not be as important. Even so, if the code is identical, you may be able to get pretty far into a sweeping change with carefully crafted compare-and-replace commands.</p>
<p>This approach may be best in places where you want some common code, but you can&rsquo;t use one of the other more automatic approaches for whatever reason.</p>
<h2 id="generate-boilerplate-with-ai-powered-tools">Generate boilerplate with AI-powered tools</h2>
<p>We can quickly generate code with one of the new AI-powered tools, and they should have no trouble generating boilerplate for us along with everything else. This could seem like an attractive option for dealing with cross-cutting concerns, but I am skeptical.</p>
<p>Generative AIs can generate boilerplate, but they will be making it up every time they write code. You would need to provide appropriate instructions for the kinds of boilerplate you want, and remembering to do this is not that different from adding it yourself every time anyway. Even if you do, there is no guarantee that the generated code will be consistent. It may be mostly cosmetic differences, but the threat of a functional difference is always looming.</p>
<p>If you ever needed to change the pattern across your code base, you would have all the duplication, but subtle differences (even cosmetic ones) could make more automatic approaches impossible. Scanning all your code manually to change your error handling approach would not be fun.</p>
<p>It&rsquo;s hard to say how these tools will perform in the future. Maybe they&rsquo;ll get much better at including required boilerplate in a particular way. For now, I recommend avoiding this approach to this particular problem.</p>
<h2 id="use-automated-tests-to-enforce-consistency">Use automated tests to enforce consistency</h2>
<p>Sometimes there is no good way to share the implementation of a cross-cutting concern. In these cases, it may make sense to write an automated test that makes sure that the policy is present. It may sound a bit daunting, but it can actually be pretty easy to write a test that finds every class in your project and checks it. You may be able to use reflection to find and call methods. For some scenarios, it may be easier to write a test that looks at the source files directly.</p>
<p>Some concerns are so important that it may be worthwhile to write these sweeping tests even if you can implement the implementation automatically using one of the other techniques in this list. Authorization is a great example: you might want to test that all of your service classes fail with the right error if no permissions are present.</p>
<p>When writing these kinds of tests, I find it helpful to put a list of exceptions in a constant nearby. For example, you would want your login service to work for a user that hasn&rsquo;t logged in yet. There may not be any other exceptions to the rule, but you can easily add them to the list if they appear.</p>
<h2 id="do-nothing">Do nothing</h2>
<p>Depending on the dynamics of your team and the kind of concerns your dealing with, it may not be possible or even worthwhile to use one of these techniques. For example, an open source project may not want to enforce strict coding requirements on unpaid volunteers. Automatic handling of a concern may be too complicated for most contributors to understand, or make it too difficult to set up a development environment.</p>
<p>So what happens in this worst case scenario? You get inconsistent implementation of a requirement, inconsistent implementations when the requirement is implemented, and it becomes harder to change the behaviour across the system. On the other hand, you don&rsquo;t have the overhead of these approaches either. You don&rsquo;t have any magic code, or any unexpected tricks hidden in middleware.</p>
<p>For something like a prototype, or for a small internal project that&rsquo;s unlikely to grow into something bigger, maybe this is good enough.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/architecture" term="architecture" label="architecture" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/design" term="design" label="design" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Writing Code for Better Reviews]]></title>
            <link href="https://jessemcdowell.ca/2024/04/Writing-Code-for-Better-Reviews/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2024/01/Optimal-Code-Reviews/?utm_source=atom_feed" rel="related" type="text/html" title="Optimal Code Reviews" />
                <link href="https://jessemcdowell.ca/2023/08/Regarding-Test-Coverage-Targets/?utm_source=atom_feed" rel="related" type="text/html" title="Regarding Test Coverage Targets" />
                <link href="https://jessemcdowell.ca/2023/05/Sustainable-Errors/?utm_source=atom_feed" rel="related" type="text/html" title="Sustainable Errors" />
                <link href="https://jessemcdowell.ca/2023/04/How-to-Fix-a-Bug/?utm_source=atom_feed" rel="related" type="text/html" title="How to Fix a Bug" />
                <link href="https://jessemcdowell.ca/2023/03/How-to-Report-a-Bug/?utm_source=atom_feed" rel="related" type="text/html" title="How to Report a Bug" />
            
                <id>https://jessemcdowell.ca/2024/04/Writing-Code-for-Better-Reviews/</id>
            
            
            <published>2024-04-24T14:37:05-07:00</published>
            <updated>2024-04-24T14:37:05-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>I believe code reviews are a high-value activity (which <a href="/2024/01/Optimal-Code-Reviews/">I&rsquo;ve written about before</a>), but they take time and slow down your development process. With a few simple tricks, you can make it easier for reviewers to understand your changes, allowing them to give you better feedback faster. Not only does this save everyone time, but it also improves the quality of your code.</p>
<div class="table-of-contents">
    <nav id="TableOfContents">
  <ul>
    <li><a href="#make-your-intentions-understood">Make your intentions understood</a></li>
    <li><a href="#group-changes-by-intention-one-intention-at-a-time">Group changes by intention, one intention at a time</a></li>
    <li><a href="#break-up-large-changes-into-multiple-reviews">Break up large changes into multiple reviews</a></li>
    <li><a href="#avoid-high-noise-low-value-changes">Avoid high-noise, low-value changes</a></li>
    <li><a href="#establish-shared-hygiene-to-reduce-noise">Establish shared hygiene to reduce noise</a></li>
    <li><a href="#rebase-with-care">Rebase with care</a></li>
    <li><a href="#review-your-work-before-asking-others">Review your work before asking others</a></li>
    <li><a href="#understand-your-review-tool">Understand your review tool</a></li>
  </ul>
</nav>
</div>

<h2 id="make-your-intentions-understood">Make your intentions understood</h2>
<p>Good commit messages and review titles are important. It may be (and in fact, should be) obvious from your code change what you&rsquo;re trying to do, but a good message is still important. There are lots of cases where someone needs to scan the change list, and good messages make this a lot easier. It also helps your reviewer understand what you&rsquo;re doing.</p>
<p>The key to a good commit message is describing your intention. &ldquo;Added some code&rdquo; may be correct, but it&rsquo;s not helpful. &ldquo;Check for invalid email addresses&rdquo; explains why you added the code. Your audience is a future developer trying to figure out why a feature broke, and what they care about is getting a quick picture of what you could have changed.</p>
<p>A little humour can be fun sometimes, but be careful with this. Small tasteful jokes are fine, but sarcasm or jokes that obscure your meaning will just slow others down.</p>
<h2 id="group-changes-by-intention-one-intention-at-a-time">Group changes by intention, one intention at a time</h2>
<p>I tend to meander through changes as opposed to attacking them linearly, and I think this is pretty common. I run into small things while I&rsquo;m wandering through the code, and I want to fix them right away. I could take a note and come back later, but this sometimes feels like moving backwards. The compromise that I normally go with is fixing the side-thing anyway, but presenting it separately.</p>
<p>If the change is very small (ex: a typo in a private method name - the fix will only affect a couple lines of code in one file) it may be okay to tuck it in. If it&rsquo;s only a few changes but spread across a few files, putting it in its own commit is better.</p>
<p>The worst offender is the sweeping change. No matter how much a typo in a widely used function name might bother, adding a couple of hundred lines of noise to the review is absolutely going to slow it down. A new, separate review is a great way to present these. It&rsquo;s a lot easier to verify simple changes if they are the only kind of change present. With Git and GitHub, it&rsquo;s pretty easy to rebase a single sweeping change commit into its own branch and put it up for for review immediately.</p>
<p>Some &ldquo;simple&rdquo; fixes are not as simple as they seem, and could introduce a bug. If there is any doubt at all, it may be worth separating these changes so that they get separate scrutiny in the test and documentation pipelines. This usually means creating a new issue in your issue tracker. If they&rsquo;re small enough, and if your tools allow it, you might still be able to include the change (in its own commit) in your same code review.</p>
<h2 id="break-up-large-changes-into-multiple-reviews">Break up large changes into multiple reviews</h2>
<p>Sometimes a single change is big enough that you may want to break it into smaller pieces. For most cases, multiple commits may be enough, but for the largest changes you may want multiple reviews. This can be especially kind for a reviewer, or helpful if a change spans multiple areas of expertise.</p>
<p>One common and sensible rule is that every review should be able to build and pass tests before it is merged. That needs to be the first consideration when breaking a change down into smaller reviews. It can be temping to break up reviews by folder, or application layer, or some other simple delineation, but it will make it harder for reviewers to understand what is happening.</p>
<h2 id="avoid-high-noise-low-value-changes">Avoid high-noise, low-value changes</h2>
<p>Reordering code, rearranging imports, or running automatic reformatting functions can create a lot of changed lines in a review tool. Worse, it can sometimes be mistaken for removal and re-addition of code, making it harder to notice any changes made at the same time. This also makes it harder to track the true history for your lines of code. Sometimes there is enough value to justify the noise, but make the decision consciously.</p>
<h2 id="establish-shared-hygiene-to-reduce-noise">Establish shared hygiene to reduce noise</h2>
<p>Using an explicit <code>.editorconfig</code> is an easy starting point. Automatic reformatting tools can save a lot of time. The noise should be minimal if everyone follows the same rules.</p>
<p>There are some other practices that can help reduce noise:</p>
<ul>
<li>use trailing commas in lists (if your language supports it) - this prevents the last line from changing when a new line is added</li>
<li>keep large lists in alphabetical order - this makes it easier to find code, and prevents the temptation to regroup and reorder things</li>
<li>keep code files small (ex: one class per file)</li>
<li>keep auto-generated code in separate files from custom code (ex: partial classes)</li>
</ul>
<h2 id="rebase-with-care">Rebase with care</h2>
<p>Avoid rebasing when a review is underway. Sometimes a rebase is necessary, but it can disrupt the change history that reviewers are in the middle of processing. GitHub can sometimes be pretty bad with this. It&rsquo;s okay to rebase before anyone has looked at your review. If you have no choice, see if you can delay the rebasing until all feedback has been addressed and accepted.</p>
<h2 id="review-your-work-before-asking-others">Review your work before asking others</h2>
<p>Quickly scan your changes before sharing them with others. I have caught innumerable typos or accidental changes this way. You could leave them in the review, but it wastes time for your reviewers, and it makes you look sloppy. It can also add an unnecessary iteration to the review process if there was no other feedback.</p>
<h2 id="understand-your-review-tool">Understand your review tool</h2>
<p>There can be subtle differences in the reviewing experience depending on the tools and process your team is using. Take time to understand how your tool works, and make sure you follow the processes your peers prefer.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/practices" term="practices" label="practices" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/quality" term="quality" label="quality" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/code-reviews" term="code-reviews" label="code reviews" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[The True Cost of Dependencies]]></title>
            <link href="https://jessemcdowell.ca/2024/03/The-True-Cost-of-Dependencies/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/11/Using-Architectural-Decision-Records/?utm_source=atom_feed" rel="related" type="text/html" title="Using Architectural Decision Records" />
                <link href="https://jessemcdowell.ca/2023/08/Regarding-Test-Coverage-Targets/?utm_source=atom_feed" rel="related" type="text/html" title="Regarding Test Coverage Targets" />
                <link href="https://jessemcdowell.ca/2023/08/My-Architectural-Report-Template/?utm_source=atom_feed" rel="related" type="text/html" title="My Architectural Report Template" />
                <link href="https://jessemcdowell.ca/2023/05/Sustainable-Errors/?utm_source=atom_feed" rel="related" type="text/html" title="Sustainable Errors" />
                <link href="https://jessemcdowell.ca/2013/06/reading-server-graphs-connected-users/?utm_source=atom_feed" rel="related" type="text/html" title="Reading Server Graphs: Connected Users" />
            
                <id>https://jessemcdowell.ca/2024/03/The-True-Cost-of-Dependencies/</id>
            
            
            <published>2024-03-27T15:35:10-07:00</published>
            <updated>2024-03-27T15:35:10-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>I used to use Getform for a contact form on <a href="https://pragmaticpotato.com">my consulting company website</a>. I recently received an email from them announcing that their free tier was dropping from 50 submissions per month to a lifetime limit of 25. This makes it useless for anything more than a trial, and their lowest tier is a more expensive than other similar options.</p>
<p>I&rsquo;m not here to complain about companies taking back free offerings. I don&rsquo;t like the change, and I wish they&rsquo;d given me more than three days of notice, but they are a businesses, and businesses need to make money. It is a good reminder though: even if something is free to use, it still takes time and effort to integrate, to maintain, and you may occasionally need to throw it out and find a replacement.</p>
<p>Dependencies are inescapable in this business. We write programs using frameworks, written and compiled in computer languages, executing in operating systems that run on chips and boards designed and manufactured by others. It would be insane to build bespoke implementations of these for most purposes. Fortunately, these lowest level components are designed to minimize breaking changes.</p>
<p>Most software relies on a lot more dependencies, and that is often a sensible choice. There are costs to building things yourself even when they are simple: design, testing, and upkeep. Upgrading components to get security updates is a pain, but making your own vulnerabilities that never get detected or fixed has drawbacks too.</p>
<p>So if there are drawbacks to depending on third parties and drawbacks to building everything yourself, what should you do? It depends! Either way: Before adding a dependency, consider its maturity, stability, and how actively its supported. If the choice is hard to change (ex: a logging framework that will be referenced throughout your code), check again, and harder! Before building it yourself, consider the true cost, including testing and maintenance.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/architecture" term="architecture" label="architecture" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/design" term="design" label="design" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/devops" term="devops" label="devops" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/quality" term="quality" label="quality" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Being Honest and Positive at Work]]></title>
            <link href="https://jessemcdowell.ca/2024/02/Being-Honest-and-Positive-at-Work/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2016/08/infiltrating-an-organization/?utm_source=atom_feed" rel="related" type="text/html" title="Infiltrating an Organization (or: Joining a New Team)" />
                <link href="https://jessemcdowell.ca/2023/07/Horizontal-One-on-Ones-and-Talking-Practice/?utm_source=atom_feed" rel="related" type="text/html" title="Horizontal One-on-Ones and Talking Practice" />
                <link href="https://jessemcdowell.ca/2021/01/breaking-past-senior-developer/?utm_source=atom_feed" rel="related" type="text/html" title="Breaking Past Senior Developer" />
                <link href="https://jessemcdowell.ca/2011/10/sharpening-the-saw/?utm_source=atom_feed" rel="related" type="text/html" title="Sharpening The Saw" />
                <link href="https://jessemcdowell.ca/2024/01/Optimal-Code-Reviews/?utm_source=atom_feed" rel="related" type="text/html" title="Optimal Code Reviews" />
            
                <id>https://jessemcdowell.ca/2024/02/Being-Honest-and-Positive-at-Work/</id>
            
            
            <published>2024-02-28T11:37:11-08:00</published>
            <updated>2024-02-28T11:37:11-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>Two things that are important to me are being honest and treating everyone with dignity. However, some situations make it difficult to do both at the same time. It is possible, though. It takes effort, creativity, and a bit of trust, but it gets easier with practice.</p>
<p>When I talk about being honest, I actually mean being candid. Candor means both being honest and also communicating in good faith. Many fantasy authors have written about characters who never lie but are also fundamentally dishonest. Candor eliminates this loophole. I push myself to communicate everything that feels important, even if it&rsquo;s uncomfortable or to my own disadvantage.</p>
<p>Why would I do this? Honestly, I don&rsquo;t know. I learned about candor from a business book, and the idea of it resonated so strongly with me that I decided to make it part of what I am. It has been a difficult road, but it has also been good for me. Saying the difficult can sting in the moment, but it builds trust over time. Trust is very important when working in a team. It&rsquo;s helped bring attention to problems. It&rsquo;s also helped me to strengthen the important friendships in my life.</p>
<blockquote>
<p>Being honest may not get you a lot of friends but it&rsquo;ll always get you the right ones.
&ndash; John Lennon</p></blockquote>
<p>Being honest is hardest when emotions are involved. Sometimes I am worried about hurting feelings, or sometimes I am frustrated or upset with the way things have gone. In these moments it is easier, and in our culture sometimes even expected, that we say something polite and positive even if we feel otherwise. Even if it&rsquo;s well-intentioned, this approach diminishes trust and allows problems to fester. Even worse, these hard feelings can amplify the longer we keep them unvoiced.</p>
<p>Blurting out our first thoughts is not a winning strategy either. Care needs to be taken to make sure what we say is honest, but also polite.</p>
<p>The way I do this is to re-frame my opinion so that I see the situation in a constructive way. I&rsquo;m not pretending that I feel that way, I convince myself to feel that way. This is essential to being candid and professional at the same time. If this sounds hard, you&rsquo;re right, it is. But I believe it is absolutely worth the effort.</p>
<p>Here are some questions I can ask myself to find another opinion:</p>
<ul>
<li>Am I misunderstanding the intentions of the other person?</li>
<li>Is the other person pushing beyond their comfort zone?</li>
<li>Is the failure a result of a system that allows mistakes to go undiscovered?</li>
<li>Is it possible the other person has personal circumstances that are influencing their capacity?</li>
<li>Are my feelings a result of my own personal baggage?</li>
</ul>
<p>With practice, this gets faster and easier, but sometimes it&rsquo;s harder. Stepping away for a bit can help. Talking about it with someone else can help. Or sometimes I need to consider the source of the feelings and what that says about me. There are also ways to express being upset or disappointed sincerely that are less offensive.</p>
<p>Sometimes feedback is best given privately. I had a boss who often said, &ldquo;Give praise in public and criticism in private.&rdquo; I think this is good advice for the most part. People can get defensive and upset if they feel like they&rsquo;re being shamed. That will not improve the working environment. If you are giving feedback, make it as constructive as you can. Focus on actions that happened and their consequences, giving concrete examples whenever you can. Generalizations and judgements about the other person should be avoided. A lot has been written on this subject by people who are much better at it than me. If you are in a management position, I highly recommend taking the time to study and practice these skills.</p>
<p>There are times when feedback goes sideways and tempers can come out. This is a good time to take a break; 30 minutes for the nervous system to calm down is great. Make sure to talk in private, and talk in person if you can. It isn&rsquo;t fun when things get to this point, but it can happen when people care about what they&rsquo;re working on. It isn&rsquo;t fun, but it is a good sign.</p>
<p>If things go really wrong, make the effort to apologize once the dust has settled, and try to move through it. Repair also builds trust and improves relationships, so even if you get it wrong sometimes, don&rsquo;t lose hope.</p>
<p>There are unfortunate times when being honest isn&rsquo;t possible. I try very hard not to keep things secret just because it&rsquo;s easier, but sometimes there is no choice.  One example that comes to mind is hiring and firing decisions. These situations can be hard, but they are unavoidable when you get to a certain level of influence in an organization.</p>
<p>Operating this way at work and in life has been difficult, but it has also been very freeing. I don&rsquo;t have to carry the baggage of the truths I haven&rsquo;t shared, and I attract the kind of people who want honest feedback. It has helped surface more problems and make my life and my work better. I have no intention of changing.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/career" term="career" label="career" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/leadership" term="leadership" label="leadership" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/practices" term="practices" label="practices" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[My Experience with the Dvorak Keyboard Layout]]></title>
            <link href="https://jessemcdowell.ca/2024/01/My-Experience-with-the-Dvorak-Keyboard-Layout/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/12/Switching-from-Google-Podcasts-to-Spotify/?utm_source=atom_feed" rel="related" type="text/html" title="Switching from Google Podcasts to Spotify" />
                <link href="https://jessemcdowell.ca/2023/11/This-Blog-Hexo-generated-static-site-hosted-on-GitHub-Pages/?utm_source=atom_feed" rel="related" type="text/html" title="This Blog: Hexo-generated static site hosted on GitHub Pages" />
                <link href="https://jessemcdowell.ca/2023/10/Review-Grammarly-Premium/?utm_source=atom_feed" rel="related" type="text/html" title="Review: Grammarly Premium" />
                <link href="https://jessemcdowell.ca/2010/08/changing-from-an-ipod-to-a-creative-zen/?utm_source=atom_feed" rel="related" type="text/html" title="Changing From an iPod to a Creative Zen" />
            
                <id>https://jessemcdowell.ca/2024/01/My-Experience-with-the-Dvorak-Keyboard-Layout/</id>
            
            
            <published>2024-01-31T09:57:13-08:00</published>
            <updated>2024-01-31T09:57:13-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>I&rsquo;ve been happily using the Dvorak keyboard layout for more than 25 years. I switched because I thought typing faster would be a benefit in my career. I can type faster, and it also reduces the strain on my wrists. It does have some drawbacks though. If you had asked me before writing this post I would have told you that I love it, but now I&rsquo;m not so sure.</p>
<div class="table-of-contents">
    <nav id="TableOfContents">
  <ul>
    <li><a href="#how-i-switched">How I switched</a></li>
    <li><a href="#faster-typing--reduced-wrist-strain">Faster typing &amp; reduced wrist strain</a></li>
    <li><a href="#availability-of-dvorak-layouts">Availability of Dvorak layouts</a></li>
    <li><a href="#qwerty-is-inescapable">QWERTY is inescapable</a></li>
    <li><a href="#cut-copy-and-paste">Cut, Copy, and Paste</a></li>
    <li><a href="#login-screens-and-shared-sessions">Login screens and shared sessions</a></li>
    <li><a href="#video-games">Video games</a></li>
    <li><a href="#japanese-ime-and-the-dvorak-layout">Japanese IME and the Dvorak layout</a></li>
    <li><a href="#final-thoughts">Final thoughts</a></li>
  </ul>
</nav>
</div>

<h2 id="how-i-switched">How I switched</h2>
<p>It seems pretty reckless in retrospect, but I was young then, and it seemed like a good idea. So one day I decided to go for it. I changed the keyboard setting and dropped suddenly from about 60 words per minute to less than 1.</p>
<p>If that wasn&rsquo;t jarring enough, I decided to also break my hunt-and-peck typing habit at the same time. I had an older mechanical keyboard with a uniform key size, so I was able to re-arrange the key covers. I moved all of them into random positions except F and J because of their little bumps that are essential for touch typing. This way I had no choice but to memorize the new positions. It was disruptive and frustrating, but it was also very effective.</p>
<p>I was up to a workable speed after a couple of days. I could match my QWERTY speed after a couple of weeks.</p>
<p>Once I was comfortably using the new layout, I moved all the key covers to their positions in the Dvorak layout, including F and J. A dot of epoxy on the new starting keys (U and J) allowed me to keep touch typing.</p>
<h2 id="faster-typing--reduced-wrist-strain">Faster typing &amp; reduced wrist strain</h2>
<p>I can type faster with the Dvorak layout, and considerably faster than most of my peers. I don&rsquo;t measure often, but I have recorded myself at 160 words per minute. I can only approach these speeds when I&rsquo;ve been writing a lot of text for several days in a row, which hasn&rsquo;t happened very often. It turns out that typing fast doesn&rsquo;t actually help me much at all.</p>
<p>Almost all of my typing as a programmer is in short spurts. An email here, a chat message there. Writing source code is not much faster on Dvorak because of all the symbol characters used. I am not a slow programmer, but typing speed only becomes a limiting factor when I&rsquo;m writing the kind of low-value boilerplate code I should be avoiding in the first place.</p>
<p>The most helpful benefit I got from switching was learning to touch type, but that doesn&rsquo;t require a switch to the Dvorak layout.</p>
<p>The biggest benefit I get from the layout itself is reduced wrist strain. The more efficient layout means my fingers don&rsquo;t need to move as much. It&rsquo;s hard to quantify the improvement, but even as an invincible teenager, I noticed it immediately.</p>
<h2 id="availability-of-dvorak-layouts">Availability of Dvorak layouts</h2>
<p>When it comes to using a computer, the Dvorak layout has been implemented pretty much everywhere. Even in the early 90s, it was readily available on Windows, Linux, and Apple systems.</p>
<p>There are also keyboards that can do the mapping at the physical level. The one I tried had a switch that alternated between QWERTY and Dvorak. The computer it was attached to would have no idea when you were using Dvorak. Although these eliminate some mapping problems, I don&rsquo;t find them that helpful. I also don&rsquo;t want to carry a special keyboard around wherever I want to type.</p>
<p>I have tried printing the Dvorak layout onto my keyboards, but this has also been impractical. I have yet to find an ink or paint that can withstand more than a month of typing. I don&rsquo;t look at the keys when typing, so even if I did, it wouldn&rsquo;t help me. It might help a stranger using my computer, but it would still be excruciatingly difficult for them. If someone else needs to use my computer the first thing they should do is change the layout back to QWERTY.</p>
<p>I don&rsquo;t bother with any of this nowadays. I have a normal QWERTY keyboard on my laptop, using just the Dvorak setting in the OS.</p>
<h2 id="qwerty-is-inescapable">QWERTY is inescapable</h2>
<p>There are keyboards everywhere nowadays, physical and virtual, and almost all are in the QWERTY layout. I have one on my phone, one on my TV&rsquo;s menu, one on my gaming console, and even one on my printer. Most of these devices have no way to change the layout.</p>
<p>I occasionally want to use a computer that isn&rsquo;t my own, such as when I&rsquo;m pair programming, or helping my dad with his computer. I could change the layout in these cases, but it&rsquo;s often too much of a nuisance.</p>
<p>For these reasons, I had to re-learn how to type on QWERTY shortly after switching to Dvorak. I&rsquo;m unsure how or why, but I developed an ability to use both depending on where I&rsquo;m looking - QWERTY if I&rsquo;m looking at the keyboard or Dvorak if I&rsquo;m looking away. It&rsquo;s as if I have two different sets of muscle memory for typing and my eyes control which is engaged. This has been a benefit, but it causes some weird side effects too.</p>
<p>The most bizarre is that I find it nearly impossible to use shortcut keys one-handed because I have trouble finding the Drovak letter if I need to look at the keys. Apple computers have a layout called &ldquo;Dvorak - QWERTY ⌘&rdquo; that switches the layout back to QWERTY for command sequences. It solves the problem, but it isn&rsquo;t available in Windows.</p>
<p>I use the QWERTY layout on touch screens like my phone for the same reason, even though the Dvorak layout is often available. It was even worse back when I was using a Microsoft Surface; because I sometimes had a physical keyboard, I had to change the layout back and forth whenever I needed the touch keyboard.</p>
<h2 id="cut-copy-and-paste">Cut, Copy, and Paste</h2>
<p>One of the most infuriating challenges with the Dvorak layout has to be the shortcut keys for Cut, Copy, and Paste. X, C and V are excellent keys for QWERTY users because you can press them easily using only your left hand. This is especially great if you use your mouse with your right hand. On the Dvorak layout, C is where the QWERTY I is, and V is where the QWERTY period key is. This is a significant disadvantage.</p>
<p>I can switch back to QWERTY temporarily if I have a lot of pasting to do, but I&rsquo;ll invariably need to modify a couple of words here or change the punctuation there. Switching back and forth constantly doesn&rsquo;t end up being easier.</p>
<h2 id="login-screens-and-shared-sessions">Login screens and shared sessions</h2>
<p>Login screens on shared computers can be a real pain. It was an advantage at first - it was a fantastic way to stop my sister from using my computer when we lived with our parents - but it is much less useful in the workplace.</p>
<p>The behaviour is different in every operating system and often inconsistent. For example, if you lock your session in Windows, the unlock screen will use the keyboard layout of the session you locked. If you reboot your computer instead, the login screen will use the system default.</p>
<p>Most modern operating systems don&rsquo;t require typing a username when logging in. This means the only characters you type are masked password characters, so you often can&rsquo;t tell when the layout is wrong. Newer operating systems show the layout on the login screen, but the indicator is too subtle, even when I know to look for it. I often don&rsquo;t check the layout until after one or two failed attempts. In a strict workplace, it doesn&rsquo;t take very many before an account gets locked.</p>
<p>Another variation of this crops up when connecting to remote computers or virtual machines. Every technology can be different, but most transmit the hardware key codes rather than the ASCII character values. This means you must change the OS-level layout setting on the system you&rsquo;re connecting to.</p>
<p>Windows Remote Desktop (RDP) sets the layout for you when you start a session. It takes the value from the session where you start the RDP client, which is usually helpful. However, it doesn&rsquo;t change automatically when you connect to an existing session, nor does it switch back to the system default when you leave a session idle. If I&rsquo;m sharing an account with someone (for example, a test server on a local network; sharing accounts in production is bad!), I could get either keyboard layout when I connect. I am used to dealing with this, but it has wasted a lot of time for my colleagues when they&rsquo;ve intercepted my old sessions.</p>
<h2 id="video-games">Video games</h2>
<p>Some video games suffer from a similar problem. Games from the olden days tended to work on hardware key codes, but newer web-based games usually use ASCII codes. A and D for left and right is universal, but it sucks on a Dvorak layout. The A is in the same place, but the D is over in the same position as the QWERTY H. Don&rsquo;t even get me started on W and S. The best way to deal with this is to change the keyboard mappings in the game, but I usually switch the keyboard layout back to QWERTY because it&rsquo;s easier.</p>
<p>Video games with in-game text chat are the most difficult. I need to be in QWERTY mode to play, then switch back to Dvorak mode to write a message. To make matters worse, the newer language/layout switcher (Windows + Space) thrusts you out of full-screen games because it has a dialog window. Some games handle both gracefully (using ASCII codes for text and key codes for gameplay), but it&rsquo;s not universal.</p>
<h2 id="japanese-ime-and-the-dvorak-layout">Japanese IME and the Dvorak layout</h2>
<p>Another unusual drawback appeared when I started learning Japanese. Japanese mixes 4 alphabets: the Latin alphabet, two phonetic alphabets with 46 characters each, and about 100,000 characters imported from traditional Chinese. A keyboard with every possible character would be obviously impractical, so text is written phonetically and converted as you go. This is done with an IME (Input Method Editor), an operating system feature that intercepts keystrokes and translates them into the appropriate characters.</p>
<p>Because the IME is a kind of keyboard itself, it replaces the keyboard mapping. This means there is no built-in way to use the Dvorak layout with Japanese writing. It used to be possible to change a registry key to replace the keyboard layout, but this option seems to no longer work with the updated Japanese IME that ships with Windows 11. There are instructions available to use the old IME, but who knows how long that&rsquo;ll be available.</p>
<p>Typing Japanese via the QWERTY layout is painful for me. The IME is constantly popping up suggestions, and since I can&rsquo;t touch type on the QWERTY layout, I have to keep looking up and down to use it properly. This is no longer much of a problem for me since I&rsquo;ve stopped practicing Japanese, but it would be if I ever picked it back up. This is one of the few reasons I would consider a physical Dvorak keyboard.</p>
<h2 id="final-thoughts">Final thoughts</h2>
<p>I am surprised by what I&rsquo;ve written. I had imagined a small blurb about how I switched and then re-sharing the Japanese IME registry hack (which I&rsquo;ve since discovered no longer works). I did not realize I had so much to say about my experiences! Would I recommend the Dvorak layout to others? No, I would not, and it surprises me that I say that. I can&rsquo;t even think of a situation where the benefits would be important enough to justify the disadvantages.</p>
<p>Would I recommend Dvorak users switch back to QWERTY? I feel a sadness I don&rsquo;t understand when I consider the answer for myself, but I think the answer is yes.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/gadgets" term="gadgets" label="gadgets" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/reviews" term="reviews" label="reviews" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Optimal Code Reviews]]></title>
            <link href="https://jessemcdowell.ca/2024/01/Optimal-Code-Reviews/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/08/Regarding-Test-Coverage-Targets/?utm_source=atom_feed" rel="related" type="text/html" title="Regarding Test Coverage Targets" />
                <link href="https://jessemcdowell.ca/2023/05/Sustainable-Errors/?utm_source=atom_feed" rel="related" type="text/html" title="Sustainable Errors" />
                <link href="https://jessemcdowell.ca/2023/04/How-to-Fix-a-Bug/?utm_source=atom_feed" rel="related" type="text/html" title="How to Fix a Bug" />
                <link href="https://jessemcdowell.ca/2023/03/How-to-Report-a-Bug/?utm_source=atom_feed" rel="related" type="text/html" title="How to Report a Bug" />
                <link href="https://jessemcdowell.ca/2023/11/Using-Architectural-Decision-Records/?utm_source=atom_feed" rel="related" type="text/html" title="Using Architectural Decision Records" />
            
                <id>https://jessemcdowell.ca/2024/01/Optimal-Code-Reviews/</id>
            
            
            <published>2024-01-04T14:11:32-08:00</published>
            <updated>2024-01-31T00:00:00-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>I am a strong advocate for code reviews. They have many advantages including improving code quality and team communication. On the other hand, they take a lot of time and add yet another delay to the development pipeline. With the wrong team culture, they can create hard feelings and discourage honest collaboration. This is a heavy price to pay, and yet, I&rsquo;ve seen many teams that do them without ever talking about how or why.</p>
<p>With a bit of intentionality and the right support systems, code reviews can become a catalyst for positive change that are absolutely worth the time we put into them.</p>
<div class="table-of-contents">
    <nav id="TableOfContents">
  <ul>
    <li><a href="#why-we-do-code-reviews">Why we do code reviews</a></li>
    <li><a href="#first-automate-everything-you-effectively-can">First, automate everything you (effectively) can</a></li>
    <li><a href="#what-to-look-for-when-reviewing-code">What to look for when reviewing code</a></li>
    <li><a href="#self-editing-and-maintaining-a-positive-culture">Self-editing and maintaining a positive culture</a></li>
    <li><a href="#suggesting-alternatives">Suggesting alternatives</a></li>
    <li><a href="#number-of-reviewers">Number of reviewers</a></li>
    <li><a href="#dont-say-anything-if-you-dont-need-to">Don&rsquo;t say anything if you don&rsquo;t need to</a></li>
    <li><a href="#accepting-feedback-on-my-feedback">Accepting feedback on my feedback</a></li>
  </ul>
</nav>
</div>

<h2 id="why-we-do-code-reviews">Why we do code reviews</h2>
<p>There are a lot of reasons for code reviews, but the one I find the most valuable is that they promote shared ownership of the code. Sharing code forces teams to get aligned on architecture and coding practices. This gives you more flexibility to distribute work, reducing bottlenecks and guarding business continuity. The developers who work on your critical systems will appreciate the ability to take vacations as well.</p>
<p>Another advantage is that reviews improve code quality simply by being part of the development process. Developers can be lazy people (ask me how I know), but we get a little extra incentive to tidy things up when we know someone else will be looking. Since we spend much more time reading our code than writing it, this will improve efficiency in the long run.</p>
<p>Reviews can also be a more efficient way to onboard developers, especially for the kinds of knowledge that are hard to articulate. For a small team with little planned growth or churn, writing a huge body of documentation may not be a great use of time. Of course, learning through review feedback doesn&rsquo;t always feel very nice when you&rsquo;re on the receiving end, but this is another reason why it&rsquo;s valuable. Making it normal to receive constructive feedback without shame is essential if you want a team where members push themselves and grow.</p>
<h2 id="first-automate-everything-you-effectively-can">First, automate everything you (effectively) can</h2>
<p>One simple way to reduce the time you spend on code reviews is to automate as much as you can. Some things require a human touch, but some other things are much easier for computers.</p>
<p>Compilers are typically very easy to automate, and it can take humans lots of time to detect compilation errors. If you automate only the compilation of any proposed changes, you can prevent a lot of build breaks and wasted time. This is helpful even with a team of one.</p>
<p>You should be running your unit tests automatically as well. Not only will this make sure any new tests pass, but it&rsquo;ll prevent failing tests from getting merged back to the main codebase, which can in turn disrupt other developers.</p>
<p>Longer-running automation tests or e2e tests may be too slow or expensive to run on every proposed change, but you should consider the tradeoffs. Even the most well-intentioned team can find it irresistible to ignore a failing test when a tight deadline is looming. Once you have a couple of tests that fail regularly, it doesn&rsquo;t take long before confidence disappears and decay sets in.</p>
<p>There are lots of tools for checking basic code formatting and whitespace. These are also good to automate and should be set up early in any project. I have seen so much time wasted in reviews talking about white space and proper nesting. Let the machines give feedback in minutes instead of wasting developer time on it. Make sure you have an <code>.editorconfig</code> that matches the rules you&rsquo;re checking so that your IDEs get most code correct before the review starts.</p>
<h2 id="what-to-look-for-when-reviewing-code">What to look for when reviewing code</h2>
<p>I try to focus mostly on readability and maintainability. Do I understand the intention of the change? If I needed to change this code in 6 months would I be able to find it and figure out what it&rsquo;s doing? Do the names being used describe what they are?</p>
<p>I don&rsquo;t check if the change meets its requirements unless I happen to notice it. Code is not a great level to test functionality at, and I expect the author to test this themselves, and a further manual test to be done once the change is in a build. I do check that any commit messages adequately describe what the change is.</p>
<p>I also like to check that a good number of tests have been written, and any complicated or risky code has sufficient coverage. If I&rsquo;m honest though, I don&rsquo;t typically check the contents of test methods. I find most tests difficult to read, and I expect the developer to have checked that they work already.</p>
<h2 id="self-editing-and-maintaining-a-positive-culture">Self-editing and maintaining a positive culture</h2>
<p>Before I post a code review comment I like to ask myself a few questions:</p>
<ul>
<li>Is the change important? Have I communicated that?</li>
<li>Is it clear what needs to be changed?</li>
<li>Is it reasonable to expect the author to make the change?</li>
</ul>
<p>At this point in my career, I find that a lot of my code review comments are more instinctual than formed logically. I don&rsquo;t think it&rsquo;s a bad thing to operate this way, but instincts can drift or misfire sometimes. Even if something feels wrong it might not be a problem, or it might not be important. I use my checklist to validate my instincts. It&rsquo;s not uncommon for me to add a bunch of comments while doing a review, then go back and delete some of them before hitting the send button.</p>
<p>Sometimes I have concerns about the underlying approach or the broader scope of work the change is a part of. In these cases, I check myself carefully to make sure my feedback is important. If I don&rsquo;t want to be a part of every decision made by the team, I have to trust the team to make them without me. Big changes are also more expensive, and the code review step is pretty late in the process. Sometimes the stakes are high enough though.</p>
<p>As with all business communication, it is important to be mindful of tone in code reviews. I always try to assume positive intent and try to reflect that in my comments. If I have a lot of feedback or serious feedback, I might speak with the author directly. A quick face-to-face or video chat makes it easier to diffuse hard feelings.</p>
<p>Another thing I avoid is the &ldquo;I would have done it like&hellip;&rdquo; pattern. At best it&rsquo;s a poor justification for a change, at worst it implies superiority. It is much better to use &ldquo;I suggest doing X because Y&rdquo;.</p>
<h2 id="suggesting-alternatives">Suggesting alternatives</h2>
<p>The easiest way to ensure a comment is reasonable is to provide an alternative myself. For example, I won&rsquo;t leave a comment about something&rsquo;s name unless I can offer a better name, and give a good reason why the old name doesn&rsquo;t make sense.</p>
<p>I also like to use the diff feature in GitHub comments to show the actual change if it&rsquo;s reasonable to do so. This often doesn&rsquo;t work for naming (because there will be references that also have to be changed), but it&rsquo;s great for little stuff like typos in user-facing strings. It is also sometimes the easiest way to communicate an improved algorithm or propose new function signatures.</p>
<h2 id="number-of-reviewers">Number of reviewers</h2>
<p>Sometimes it&rsquo;s important to get lots of eyes on a critical change. Sometimes it&rsquo;s not important but people do it anyway. Regardless of the reason, it can be especially painful to get code approved when multiple reviewers have to approve. In these circumstances, and especially when I&rsquo;m late to the review, I temper my feedback further than usual so that the code review won&rsquo;t go around in circles forever.</p>
<p>The worst is when two reviewers disagree on the best approach and argue about it in the code review. Whenever this happens I ask the two reviewers to talk to each other directly to resolve the issue. It can be beneficial for the author to join in as well.</p>
<p>I generally recommend just one reviewer for most code reviews as this gets the most benefit with the least cost. I also suggest making sure the whole team is doing reviews. You could assign them all to the most senior member of the team, but doing reviews is a valuable teaching experience and not all code requires an expert opinion. If a code review does require an expert opinion, I prefer to ask those experts to focus on specific areas but let someone else handle the broader review. Having new team members join code reviews is also an easy way to expose them to team practices and any work in progress.</p>
<h2 id="dont-say-anything-if-you-dont-need-to">Don&rsquo;t say anything if you don&rsquo;t need to</h2>
<p>Sometimes the code is good enough, especially if the change is small and clear. If it&rsquo;s good enough, resist the urge to nitpick and mark it as reviewed.</p>
<h2 id="accepting-feedback-on-my-feedback">Accepting feedback on my feedback</h2>
<p>When someone puts comments on the code I&rsquo;ve put up for review, I take those comments seriously. Sometimes I don&rsquo;t agree, but I put in extra effort to make sure I maintain an appreciative and curious tone. Even bad feedback is often grounded in good reasons. It might be a hint that another other part of the change isn&rsquo;t clear. If I believe the comment is an error, I respectfully point it out and provide references if I can.</p>
<p>When I&rsquo;m reviewing, I often make suggestions without testing them. I also go from memory, and sometimes I make mistakes. When someone challenges a comment of mine, I take it just as seriously. This isn&rsquo;t a sign of disrespect, it&rsquo;s just the nature of working on hard problems in a team.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/quality" term="quality" label="quality" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/practices" term="practices" label="practices" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/code-reviews" term="code-reviews" label="code reviews" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Switching from Google Podcasts to Spotify]]></title>
            <link href="https://jessemcdowell.ca/2023/12/Switching-from-Google-Podcasts-to-Spotify/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/11/This-Blog-Hexo-generated-static-site-hosted-on-GitHub-Pages/?utm_source=atom_feed" rel="related" type="text/html" title="This Blog: Hexo-generated static site hosted on GitHub Pages" />
                <link href="https://jessemcdowell.ca/2023/10/Review-Grammarly-Premium/?utm_source=atom_feed" rel="related" type="text/html" title="Review: Grammarly Premium" />
            
                <id>https://jessemcdowell.ca/2023/12/Switching-from-Google-Podcasts-to-Spotify/</id>
            
            
            <published>2023-12-14T10:17:07-08:00</published>
            <updated>2023-12-14T10:17:07-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>Back in September <a href="https://blog.youtube/news-and-events/podcast-destination-on-youtube-music/">Google announced that Google Podcasts will be shut down</a>. It is supposed to remain available until April of 2024 and be available for exporting data until June. I chose not to wait.</p>
<p>I have been using Spotify happily for music for years now, so I decided to give it a try. I didn&rsquo;t do any research or look for other options. I had some confidence that it would be a reasonable option however because Spotify has been putting a lot of effort and money into its podcast offering.</p>
<h2 id="is-spotify-a-good-replacement-for-google-podcasts">Is Spotify a good replacement for Google Podcasts</h2>
<p>Short version:</p>
<p>If you want something that will work on a wide variety of devices, has a huge library of content, and is likely to be around for a while, then Spotify is a good choice. If you want a simple replacement that behaves the same as Google Podcasts, you may have better results elsewhere.</p>
<p>Long version:</p>
<p>In terms of the availability of the app on multiple devices, availability of podcasts, and sound quality, Spotify performs excellently. They are a huge company with lots of content, and everything works great.</p>
<p>I can listen to podcasts on my phone, on my computer, in my car (via Android Auto), or on my kitchen speaker (via my Google Nest Mini). I can even listen to part of an episode on any of these devices and finish it on another device. It has remembered my position every time, something which was often not the case with Google Podcasts.</p>
<p>The biggest disadvantage has been the disruption of my listening workflows.</p>
<p>When I wanted to listen to a podcast on Google Podcasts, I would open the app, pick an item out of the Queue, and play it. When it finished, it would usually (depending on how I started it) start on the podcasts at the top of my Queue. If I wanted to listen to a few in a row without touching my phone (for example, while working in the garden), I could pick the next couple podcasts I wanted to hear and put them at the top of the list. Spotify&rsquo;s &ldquo;Your Episodes&rdquo; playlist does not allow custom ordering, but I have found another way to do it.</p>
<p>The other annoying tidbit is that using the Spotify app or a Podcast app isn&rsquo;t enough to specify what I want to hear. If I was in the middle of a podcast when I get into my car, I&rsquo;ll be listening to a podcast there too. It&rsquo;s not a big deal on the phone, but it can be pretty annoying to change this through the Android Auto interface.</p>
<h2 id="how-to-migrate-your-data-to-spotify">How to migrate your data to Spotify</h2>
<p>It took me less than 30 minutes to migrate my subscriptions and queue manually. There is supposed to be an export added to Google Podcasts, but I assume it&rsquo;s not yet available because I couldn&rsquo;t find it. I also never found an import in Spotify, so maybe it wouldn&rsquo;t matter.</p>
<p>To do it manually, just search for and click &ldquo;Follow&rdquo; on any podcasts you had subscribed to. Next, search for and click the plus button on any episodes that were in your queue. I suggest using a computer for this, or at least two different devices. If you didn&rsquo;t already know: <a href="https://podcasts.google.com/">Google Podcasts has a web version</a> that gives you access to everything you&rsquo;ll need.</p>
<h2 id="my-spotify-podcast-workflow">My Spotify Podcast workflow</h2>
<p>When I want to play an episode, I use &ldquo;Add to Queue&rdquo; instead of the play button. I can then use the next track button to switch from my current song to the podcast. This makes it easy to queue up a couple of specific podcasts, and makes the player switch automatically back to music when they are all finished. If I want to switch immediately back to music, the next track button will save my place and move on.</p>
<p>I don&rsquo;t like to play podcasts directly from the &ldquo;Your Episodes&rdquo; playlist because you can&rsquo;t control the order and it will jump automatically to whatever comes next. Playing from within each podcast&rsquo;s page is okay if you want it to go from one episode to the next.</p>
<p>I&rsquo;ve pinned the &ldquo;Your Episodes&rdquo; playlist, which makes it easier to start an episode. In the mobile version, this playlist has a settings menu. Setting &ldquo;Remove played episodes&rdquo; to &ldquo;After playing&rdquo; will make this list behave the most like Google Podcasts&rsquo; queue.</p>
<p>I occasionally look at the &ldquo;New Episodes&rdquo; playlist and save any episodes that interest me. This playlist only appears in the mobile version, but it is extremely helpful because it shows the latest episodes from all the podcasts I follow. I don&rsquo;t pin this playlist however, since Spotify is so stingy with its pins.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/howto" term="howto" label="howto" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/reviews" term="reviews" label="reviews" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Announcing Pragmatic Potato Software]]></title>
            <link href="https://jessemcdowell.ca/2023/11/Announcing-Pragmatic-Potato-Software/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2011/05/consulting-a-brave-new-world/?utm_source=atom_feed" rel="related" type="text/html" title="Consulting - A Brave New World" />
                <link href="https://jessemcdowell.ca/2023/09/Choosing-Powerful-Names/?utm_source=atom_feed" rel="related" type="text/html" title="Choosing Powerful Names" />
                <link href="https://jessemcdowell.ca/2023/07/Horizontal-One-on-Ones-and-Talking-Practice/?utm_source=atom_feed" rel="related" type="text/html" title="Horizontal One-on-Ones and Talking Practice" />
                <link href="https://jessemcdowell.ca/2023/06/Importance-of-Alignment/?utm_source=atom_feed" rel="related" type="text/html" title="Importance of Alignment" />
                <link href="https://jessemcdowell.ca/2023/05/Polyglot-Unconference-2023/?utm_source=atom_feed" rel="related" type="text/html" title="Polyglot Unconference 2023" />
            
                <id>https://jessemcdowell.ca/2023/11/Announcing-Pragmatic-Potato-Software/</id>
            
            
            <published>2023-11-30T14:07:52-08:00</published>
            <updated>2023-11-30T14:07:52-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>I am very pleased to announce the launch of my new company, <a href="https://pragmaticpotato.com/">Pragmatic Potato Software</a>.</p>
<p>Pragmatic Potato is helping customers with software architecture and development. We can help with architectural investigations, software design, development process, interviewing, and even development. You can find out more about the kind of services we&rsquo;re offering on our spiffy new website: <a href="https://pragmaticpotato.com/services/">pragmaticpotato.com/services/</a>.</p>
<p>If you&rsquo;re interested, <a href="https://pragmaticpotato.com/contact/">contact me through my website</a>. Let&rsquo;s have a quick conversation and see if we can help you.</p>
<p>Some of you may know that I&rsquo;m also working on a product. I am keeping these efforts in stealth mode for now, but you can <a href="https://pragmaticpotato.com/products/">sign up to be notified</a> when I&rsquo;m ready to say more.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/career" term="career" label="career" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/consulting" term="consulting" label="consulting" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/pragmatic-potato" term="pragmatic-potato" label="pragmatic potato" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Using Architectural Decision Records]]></title>
            <link href="https://jessemcdowell.ca/2023/11/Using-Architectural-Decision-Records/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/10/Getting-Unstuck-Without-a-Rubber-Duck/?utm_source=atom_feed" rel="related" type="text/html" title="Getting Unstuck Without a Rubber Duck" />
                <link href="https://jessemcdowell.ca/2023/08/Regarding-Test-Coverage-Targets/?utm_source=atom_feed" rel="related" type="text/html" title="Regarding Test Coverage Targets" />
                <link href="https://jessemcdowell.ca/2023/08/My-Architectural-Report-Template/?utm_source=atom_feed" rel="related" type="text/html" title="My Architectural Report Template" />
                <link href="https://jessemcdowell.ca/2023/07/Horizontal-One-on-Ones-and-Talking-Practice/?utm_source=atom_feed" rel="related" type="text/html" title="Horizontal One-on-Ones and Talking Practice" />
                <link href="https://jessemcdowell.ca/2023/05/Sustainable-Errors/?utm_source=atom_feed" rel="related" type="text/html" title="Sustainable Errors" />
            
                <id>https://jessemcdowell.ca/2023/11/Using-Architectural-Decision-Records/</id>
            
            
            <published>2023-11-21T17:51:51-08:00</published>
            <updated>2023-11-21T17:51:51-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>Architectural Decision Records are a simple technique that promotes good architectural thinking and better collaboration. They don&rsquo;t need to be big or complicated to be effective, but they do take some time, and are yet another step between setting goals and delivering value. If you use them in the right circumstances they can be a big help.</p>
<div class="table-of-contents">
    <nav id="TableOfContents">
  <ul>
    <li><a href="#what-is-an-architectural-decision-record">What is an Architectural Decision Record?</a></li>
    <li><a href="#why-use-architectural-decision-records">Why use Architectural Decision Records?</a></li>
    <li><a href="#when-should-i-use-an-architectural-decision-record-process">When should I use an Architectural Decision Record process?</a></li>
    <li><a href="#can-i-introduce-an-architectural-decision-record-process-to-an-existing-project">Can I introduce an Architectural Decision Record process to an existing project?</a></li>
    <li><a href="#what-architectural-decision-record-template-should-i-use">What Architectural Decision Record template should I use?</a></li>
    <li><a href="#where-should-i-store-architectural-decision-records">Where should I store Architectural Decision Records?</a></li>
    <li><a href="#maintaining-architectural-decision-records">Maintaining Architectural Decision Records</a></li>
    <li><a href="#summary">Summary</a></li>
  </ul>
</nav>
</div>

<h2 id="what-is-an-architectural-decision-record">What is an Architectural Decision Record?</h2>
<p>An Architectural Decision Record (often abbreviated ADR) is just as the name implies: a document that describes an architectural decision. It can include various details, but should at a minimum describe the problem being solved and the solution that was chosen. It can be a simple text document with a page or two of text, or it can be longer with specific sections that are important to the team.</p>
<h2 id="why-use-architectural-decision-records">Why use Architectural Decision Records?</h2>
<p>The most obvious benefit of architectural decision records is that they record your important technical decisions. Years later, if you&rsquo;re considering replacing Framework X, you can see the original analysis that went into its selection. There may have been good reasons your new candidate wasn&rsquo;t chosen. Or more likely, the decision may have been based on assumptions that are now known to be invalid.</p>
<p>My favourite reason for using Architectural Decision Records is that they are an easy way to encourage good architectural habits. For example, forcing a developer to write down the problem they are trying to solve makes it harder to add technologies with no purpose (yes, this happens). Asking for alternatives considered makes sure that alternatives were actually considered. Even considering if an ADR is necessary is a valuable exercise.</p>
<p>The process of writing and reviewing ADRs also makes technical decisions more visible, and accessible to more of the team. With these simple guardrails in place, you can hand a decision off to an intermediate developer and know that your most senior developers will still be involved. Distributing work like this helps prevent bottlenecks, but it also gives everyone a chance to grow their skills.</p>
<p>Having a formal decision-making process is especially helpful for big teams where decisions can impact a lot of people. Or if a team has a partially involved architect. When I was an architect with responsibilities for multiple teams, I found this process to be an excellent way to stay in the loop, and a great entry point to get involved when needed.</p>
<h2 id="when-should-i-use-an-architectural-decision-record-process">When should I use an Architectural Decision Record process?</h2>
<p>This is really two different-but-not-so-different questions: when should a team have an ADR process, and what kind of changes should require an ADR process? The answer to both is effectively the same thing: when is a decision architecturally significant?</p>
<p>The way I answer this question is to ask what the impact would be if the decision is wrong. If there is a low risk of serious consequences and I can easily change to a different solution, then I make a good-enough choice and move on.</p>
<p>Here is an incomplete list of reasons why a decision might be architecturally significant:</p>
<ul>
<li>A lot of code would need to be rewritten if the decision gets changed. For example, when selecting a:
<ul>
<li>programming language</li>
<li>base framework</li>
<li>automated test framework</li>
</ul>
</li>
<li>Mistakes could have serious impacts. For example, if the project/change has anything to do with:
<ul>
<li>the privacy of sensitive data</li>
<li>security</li>
<li>financial transactions</li>
<li>life-critical systems</li>
<li>civic infrastructure</li>
</ul>
</li>
<li>The software is used by many different teams, and a change could impact many of them. For example: something exposed in the api of an open-source library or shared module used by multiple teams</li>
<li>The software could be used and maintained for many years or have a lot of different people working on it</li>
<li>The software is difficult to change if there is a mistake. For example: firmware</li>
</ul>
<p>If any of the above are true for your project generally, then implementing a formal ADR process could be helpful.</p>
<p>When you have an ADR process, you should also provide some guidance about when it needs to be used. For the same reasons, it makes sense to base it on the potential impact of a wrong decision. You can use an arbitrary replacement cost for your definition (for example: &ldquo;more than a week of effort to replace&rdquo;), or list potential impacts like privacy or security risks.</p>
<p>Even without a formal ADR process, individual contributors can also benefit from using one of these templates. Filling out a good template makes sure you truly understand your problem and that you don&rsquo;t skip any important steps. This is helpful even without soliciting feedback. I use this technique myself sometimes. This is also effectively what <a href="/2023/08/My-Architectural-Report-Template/">my Architectural Report template</a> is.</p>
<h2 id="can-i-introduce-an-architectural-decision-record-process-to-an-existing-project">Can I introduce an Architectural Decision Record process to an existing project?</h2>
<p>Yes, but it&rsquo;s harder for the team to appreciate the value of it.</p>
<p>You get all the benefits of standardizing the decision-making process, but it&rsquo;s hard to realize the value of documentation without a significant majority of your important decisions documented. Retroactively documenting decisions isn&rsquo;t very practical either: without tremendous notes or exceptional memory, the results can easily lack sufficient value.</p>
<p>If you have a strong reason to improve the quality of future decisions, and the team believes in it, then you have a shot at adding an ADR process to an existing project. I wouldn&rsquo;t recommend it otherwise.</p>
<h2 id="what-architectural-decision-record-template-should-i-use">What Architectural Decision Record template should I use?</h2>
<p>You can find several decent templates with a quick internet search. Joel Parker Henderson has helpfully posted <a href="https://github.com/joelparkerhenderson/architecture-decision-record/">a list of Architecture Decision Record templates</a> and some other helpful resources in GitHub, which I&rsquo;ve used in the past.</p>
<p>My favourite template is the <a href="https://github.com/joelparkerhenderson/architecture-decision-record/tree/main/locales/en/templates/decision-record-template-by-michael-nygard">Decision record template by Michael Nygard</a>. I appreciate its simplicity as a starting point, and it&rsquo;s trivial to add other sections as you need them.</p>
<p>The best template will depend ultimately on the benefits you want to get from your ADR process. For example, if you&rsquo;re building life-critical systems you might include a section for safety testing measures to make sure they are considered with every decision. If your organization is concerned with copy-left licensing, you can include a section for any new dependencies and their licences.</p>
<h2 id="where-should-i-store-architectural-decision-records">Where should I store Architectural Decision Records?</h2>
<p>The best place to store these documents is the place your team will look for them. If you have a wiki where all your technical documentation goes, that is a good choice. Some people like to put these records into source control, and that&rsquo;s a good choice too. Whatever location you choose, make sure it has good search facilities and some kind of change history. A mechanism for giving feedback in the document itself is nice as well.</p>
<p>One advantage to source control is that you can use your familiar code review tools and processes to manage them. One drawback to source control is that it encourages simple text formats, and that makes it harder to include diagrams or other rich content.</p>
<p>If you do put your ADRs into source control, think about which repository they belong in. Some people like to keep them in the same repository as the code they relate to, but this won&rsquo;t work if your project spans multiple repositories. If you use branches heavily, it may be confusing to see the documents in different while working. When in doubt, create a new repository for your technical documentation.</p>
<p>Another detail to consider is the scope of the decisions being made. If your team maintains multiple pieces of software you might have different (and conflicting) decisions for each of them. Make sure it&rsquo;s obvious what your documents apply to.</p>
<h2 id="maintaining-architectural-decision-records">Maintaining Architectural Decision Records</h2>
<p>Decision records can get stale with time. If you do start using them, remember to also occasionally go through and clean them up.</p>
<p>To help with this, many teams use an append-only approach. If a decision gets replaced, a new document will be created, and the old decision will get a note inserted marking it as deprecated. Decision records can be numbered to make cross-referencing them easier.</p>
<h2 id="summary">Summary</h2>
<p>Architectural Decision Records can improve your architectural thinking without much effort, and they have the added benefit of leaving useful documentation behind. If you add them early in a project, they can improve the quality of the decisions your team makes.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/architecture" term="architecture" label="architecture" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/design" term="design" label="design" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/practices" term="practices" label="practices" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[This Blog: Hexo-generated static site hosted on GitHub Pages]]></title>
            <link href="https://jessemcdowell.ca/2023/11/This-Blog-Hexo-generated-static-site-hosted-on-GitHub-Pages/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/10/Review-Grammarly-Premium/?utm_source=atom_feed" rel="related" type="text/html" title="Review: Grammarly Premium" />
                <link href="https://jessemcdowell.ca/2023/08/Regarding-Test-Coverage-Targets/?utm_source=atom_feed" rel="related" type="text/html" title="Regarding Test Coverage Targets" />
                <link href="https://jessemcdowell.ca/2023/08/My-Architectural-Report-Template/?utm_source=atom_feed" rel="related" type="text/html" title="My Architectural Report Template" />
                <link href="https://jessemcdowell.ca/2023/07/Horizontal-One-on-Ones-and-Talking-Practice/?utm_source=atom_feed" rel="related" type="text/html" title="Horizontal One-on-Ones and Talking Practice" />
                <link href="https://jessemcdowell.ca/2014/04/controller-led-navigation-in-angular/?utm_source=atom_feed" rel="related" type="text/html" title="Controller Led Navigation in Angular" />
            
                <id>https://jessemcdowell.ca/2023/11/This-Blog-Hexo-generated-static-site-hosted-on-GitHub-Pages/</id>
            
            
            <published>2023-11-01T10:34:32-07:00</published>
            <updated>2023-11-01T10:34:32-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>A couple of years ago I switched this blog from a WordPress site hosted on GoDaddy to a statically generated site. The new setup is faster, more secure, and cheaper to operate. And it was easy to do!</p>
<div class="table-of-contents">
    <nav id="TableOfContents">
  <ul>
    <li><a href="#static-vs-dynamic-websites">Static vs Dynamic websites</a></li>
    <li><a href="#comments-and-contact-pages-on-a-static-website">Comments and contact pages on a static website</a></li>
    <li><a href="#https-and-custom-domains">HTTPS and custom domains</a></li>
    <li><a href="#price-of-static-websites">Price of static websites</a></li>
    <li><a href="#creating-editing-and-publishing-posts">Creating, editing, and publishing posts</a></li>
    <li><a href="#converting-from-wordpress-to-hexo">Converting from WordPress to Hexo</a></li>
    <li><a href="#summary">Summary</a></li>
  </ul>
</nav>
</div>

<h2 id="static-vs-dynamic-websites">Static vs Dynamic websites</h2>
<p><a href="https://wordpress.org/">WordPress</a> is a very popular blogging platform. You can use it hosted on WordPress.com, or any number of other web providers that offer it. Because it&rsquo;s open source, you can also host it yourself easily. In my case, I had a virtual server running in GoDaddy where I maintained the installation myself.</p>
<p><a href="https://hexo.io/">Hexo</a> is a static website generator. When you want to publish changes, you run a command that causes it to spit out a bunch of static html, css, and so on that you can then host anywhere you like. These tools can store the raw content any way they like, but Hexo and all the other generators I&rsquo;ve played with use a directory structure with simple markdown files.</p>
<p>Dynamic websites are generated by software running on the web server. Ignoring any caches or other optimizations, every web request triggers a call to the running application, which in turn makes database requests and invokes any loaded plugins before a page gets sent back to the browser. For medium-sized websites, this isn&rsquo;t noticeable because the server will be constantly warmed up and ready to respond. For very small websites like mine that go idle, a fresh web request can take more than 30 seconds to respond while everything is initialized. This is not ideal.</p>
<p>Static websites have fantastic performance. The server only has to return the pre-computed content, and the providers that do this at scale do it very well. Even if I only get one hit a month, that page will still load very quickly, likely even faster than a warmed-up WordPress instance. It depends on the hosting platform, but static websites can typically also handle massive traffic spikes without any extra effort, and at a much lower cost. Your small self-hosted WordPress site, by comparison, could fall over when one of your posts goes viral.</p>
<p>Another benefit to static websites is their security. Because it&rsquo;s just plain files, there is much less attack surface. As long as your account is secure, it should be impossible for an attacker to modify the content. Even if someone did, the content and site generator can be kept separate, so the site can easily be replaced with fresh, clean content. With a WordPress site, you have the running server, the WordPress software, and the database all running in the cloud. They are prone to bugs and attacks like any other cloud infrastructure, and they need to be updated regularly to keep them secure.</p>
<h2 id="comments-and-contact-pages-on-a-static-website">Comments and contact pages on a static website</h2>
<p>The biggest drawback to static websites is that they are, well, static. There are a few tricks you can use, but some features are much more difficult to implement. For a simple blog like mine, this wasn&rsquo;t a problem.</p>
<p>Comments are a common blog feature that is trivial for a dynamic site to implement. For this blog, I recently set up <a href="https://utteranc.es/">utterances</a>. It uses GitHub Issues (in a free public repository) like a database to store comments, and retrieves / renders them on each page with a bit of client-side javascript. This way I don&rsquo;t need to run <code>hexo generate</code> every time someone posts a comment.</p>
<p>A reader does need a GitHub account to add a comment with this system, but this seems like a reasonable limitation given my audience. There are other static-capable comment systems available, each with different styles and storage mechanisms. There are commercial offerings too, which may or may not be more expensive than using a hosted WordPress site for your blog to begin with.</p>
<p>I don&rsquo;t have a contact form on this site, but I easily set one up on another static website using <a href="https://getform.io/">getform</a>. For the frequency of messages I expect there, it should also be free.</p>
<p>It gets unfortunately difficult to push much further than this on a static website. Some things can be done with client-side javascript or advanced application firewalls, but these can get complicated and/or expensive pretty quickly. It may be worth it if you want the scalability advantages of a static site, but if you just want a cheap website with a couple of dynamic features, a hosted WordPress site may be your best bet.</p>
<h2 id="https-and-custom-domains">HTTPS and custom domains</h2>
<p>The main reason I left WordPress on GoDaddy was the desire to move my blog to HTTPS. Nowadays it&rsquo;s not that hard to get a free certificate from <a href="https://letsencrypt.org/">Let&rsquo;s Encrypt</a>, but the certificates don&rsquo;t last very long, so it&rsquo;s best to automate the process. At the time I changed,  automating this wasn&rsquo;t possible with the package I had from GoDaddy. I could have changed to a more expensive virtual machine option or bought a proper certificate. I went with option 3 instead.</p>
<p>HTTPS requires a certificate, and the certificate has to contain the domain name of the website you&rsquo;re using it for. That means that you need a custom certificate to go with your custom domain name. The good news is that many hosting services can procure certificates on your behalf, and some will do it for free.</p>
<p>GitHub Pages is one of these services, and it was easy to set up. There were a couple of steps necessary so they could validate that I had control of the domain, but now they just take care of it for me. They can also provide redirects from HTTP to HTTPS, and from www to the apex website. GitHub Pages has <a href="https://docs.github.com/en/pages/configuring-a-custom-domain-for-your-github-pages-site/about-custom-domains-and-github-pages">good documentation</a> if you want to set this up for yourself.</p>
<h2 id="price-of-static-websites">Price of static websites</h2>
<p>Because GitHub Pages is free (for public repositories, at the time of this writing), the only thing I&rsquo;m paying for is the domain name and the DNS service, both of which I get from AWS. The <code>.ca</code> domain costs $13 USD per year, and the Route53 hosted zone costs me $0.90 USD per month (the minimum possible since I don&rsquo;t exceed the initial usage limits). This is much less than I was paying GoDaddy for a virtual server.</p>
<p>There are a few equivalents to GitHub Pages that are also free. There are also cheaper options than AWS for domain registration and DNS.</p>
<p>If you don&rsquo;t want the hassle of setting this all up, there are lots of hosted WordPress solutions around, and other blogging/website platforms too. Some of these are even free, but the free ones each have their own limitations. I haven&rsquo;t searched exhaustively, but I don&rsquo;t know of any free options that support custom domains and control over advertising.</p>
<h2 id="creating-editing-and-publishing-posts">Creating, editing, and publishing posts</h2>
<p>It can take a bit of effort to get a static site generator set up and configured with a nice theme, but things get easier once you have it all working. You still need to run a few commands in a terminal, which will be too much for some people. Even the mildly technically challenged should be able to keep themselves going with a few basic commands written down. This is what I use:</p>
<p>When I want to start a new post, I use the <code>hexo new</code> command:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">hexo new draft &#39;This Blog: Hexo-generated static site hosted on GitHub Pages&#39;
</span></span></code></pre></div><p>This command generates a markdown file in the draft folder with some initial &ldquo;front-matter&rdquo;. I can then edit the content in any text editor I wish. These days I use <a href="https://code.visualstudio.com/">VS Code</a>. Markdown is a very simple mostly-text format for making pages that is very easy to learn. Front-matter is a bit of data about the page that go at the top of the document such as the title, publish date, tags, categories, and so on. You can see <a href="https://github.com/hexojs/site/blob/7f465a267b7bee09c236bf67606cdb72b6fbb708/source/docs/writing.md?plain=1">an example of what this looks like</a> in the source for the Hexo documentation.</p>
<p>If I want to see how a page will look (for example: if I&rsquo;m using some tricky markdown syntax), I use <code>hexo server --drafts</code>. This hosts a website on my computer that looks exactly like the blog will when I publish it. I also sometimes use commands like <code>hexo list tag --draft</code> to make sure I&rsquo;m using the same tags across multiple posts.</p>
<p>Once I&rsquo;m happy with a post, I run the <code>hexo publish</code> command:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">hexo publish &#39;This Blog: Hexo-generated static site hosted on GitHub Pages&#39;
</span></span></code></pre></div><p>The main thing this does is move the file and its attachments from the drafts folder into the posts folder. It also renames the file and updates the front-matter with the published date and time. You could do this manually, but the command is quick and reliable. Then I need to re-generate the static content for publishing:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">hexo generate
</span></span></code></pre></div><p>This goes through all the content files and configuration and generates everything needed for the website. The new post gets its own folder and html file, but it will also cause changes to the front page and a number of peripheral pages. The theme I&rsquo;m using supports paging, so it also pushes the post at the bottom of the front page back to page 2, the bottom post from page 2 to page 3, and so on. There are also the tag, category, and archive pages that get updated similarly. It might seem like a lot, but I assure you, the software doesn&rsquo;t mind.</p>
<p>After this, it&rsquo;s a simple matter of uploading the content to my hosting provider. In the case of GitHub Pages, this means copying it to the website repository, commiting, and then pushing the branch. Nowadays I use a PowerShell script to generate, copy, commit, and push in a single step.</p>
<p>I use Git and GitHub to manage my hexo folders because I am very comfortable with these tools. This isn&rsquo;t necessary though. You could just as easily store your hexo files on a cloud drive. You could store it on your local hard drive too, but I highly recommend using somewhere that has automatic backups and some form of change history.</p>
<p>Because I&rsquo;m using drafts for incomplete posts, I commit and push any changes I&rsquo;ve made regularly. The drafts aren&rsquo;t included in the generated site, so I can keep a few sitting around while publishing other posts or changes. I originally went a little bonkers with a branch-per-post but found it made my process unnecessarily complicated.</p>
<p>You can simplify publishing your site further. Some static page providers can automatically run the generate step for you. I chose not to do this for my blog because I didn&rsquo;t want my drafts and their edit history to be visible in the public repository (as a public repository is required to use Pages for free). There are other options that don&rsquo;t have this limitation.</p>
<h2 id="converting-from-wordpress-to-hexo">Converting from WordPress to Hexo</h2>
<p>Most of the work of migrating was achieved using a WordPress export file and the Hexo migration tool:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-gdscript3" data-lang="gdscript3"><span class="line"><span class="cl"><span class="n">hexo</span> <span class="n">migrate</span> <span class="n">wordpress</span> <span class="n">wordpress</span><span class="o">-</span><span class="k">export</span><span class="o">.</span><span class="n">xml</span> <span class="o">--</span><span class="n">paragraph</span><span class="o">-</span><span class="n">fix</span> <span class="o">--</span><span class="n">import</span><span class="o">-</span><span class="n">image</span> <span class="n">original</span>
</span></span></code></pre></div><p>It took a bit of fiddling after this to get everything working perfectly. I had some issues with my code snippets (I was using a gist plugin in WordPress), and some of the attachment references didn&rsquo;t work with my theme. The nice thing about working with markdown files is that it was really easy to search for patterns once I found them. With Git and branches, I could also quickly test fixes and revert them if they didn&rsquo;t work.</p>
<p>I didn&rsquo;t have a lot of content when I made the switch, but it only took me a few hours.</p>
<h2 id="summary">Summary</h2>
<p>Static websites can be cheaper, perform better, and require less maintenance than self-hosted options. They have their limitations, but for a simple blog like mine, it was an easy choice that I&rsquo;m still happy with.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/architecture" term="architecture" label="architecture" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/blogging" term="blogging" label="blogging" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/github" term="github" label="github" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/reviews" term="reviews" label="reviews" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/web" term="web" label="web" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Review: Grammarly Premium]]></title>
            <link href="https://jessemcdowell.ca/2023/10/Review-Grammarly-Premium/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2010/09/my-first-open-source-project/?utm_source=atom_feed" rel="related" type="text/html" title="My First Open Source Project" />
                <link href="https://jessemcdowell.ca/2010/07/my-first-post/?utm_source=atom_feed" rel="related" type="text/html" title="My First Post" />
            
                <id>https://jessemcdowell.ca/2023/10/Review-Grammarly-Premium/</id>
            
            
            <published>2023-10-19T08:50:37-07:00</published>
            <updated>2023-10-19T08:50:37-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>I spend a fair bit of time working on this blog. More than I probably should given my unimpressive view metrics. It does help me to crystalize my thoughts and practice articulating points that are important to me, but I&rsquo;m spending an awful lot of time doing it. Ultimately, I write because I enjoy writing. I just wish I could spend less time per post.</p>
<p>For a while there I was getting hammered by ads from <a href="https://www.grammarly.com/">Grammarly</a>, and in a fit of frustration, or maybe I was avoiding yet another hour of editing, I signed up. I didn&rsquo;t start with the free trial, I went straight to Grammarly Premium expecting that it would be great.</p>
<p>Within an hour I had installed every app and plugin I could. A couple of days later most of them were removed or almost completely disabled. I cancelled my subscription before the first month was over.</p>
<p>I&rsquo;m getting ahead of myself though. Let me first explain why I thought it would be so helpful.</p>
<p>The quickest posts I&rsquo;ve written here have taken me a couple of hours where the more detailed ones can take 10 to 20 hours to complete. About 75% of that time is spent editing the posts. Given the amount of time I&rsquo;m spending editing, I assumed a tool like Grammarly could help me free up some time.</p>
<p>I ran the numbers for a few recent posts to illustrate:</p>
<p><img src="draft-time-vs-edit-time.svg" alt="Stacked bar chart showing times between 1.5 and 14 hours for a sampling of 6 recent posts"></p>
<p>It seemed like magic the first time I ran a blog post through Grammarly. The app filled up with suggestions, and many of them were helpful. Spelling mistakes, grammar errors, and little formatting mistakes were easy to spot and eliminate.</p>
<p>Here&rsquo;s an example of a paragraph I edited one of the first times I used Grammarly on <a href="/2023/09/Case-of-the-Slow-Matchmaking-Routine/">a recent blog post</a>:</p>
<p><img src="matchmaking-performance-grammarly-edits.png" alt="A comparison of a paragraph before and after editing with the differences highlighted."></p>
<p>Unfortunately, Grammarly made a lot of other suggestions too, and you can see some of them in the example above. It tried to eliminate passive voice and remove emphasis from important points. It frequently suggested replacements for words (to reduce duplication) that would ruin common industry expressions. Worst of all, it felt like it was removing my personal style from the blog. Every time I skipped a suggestion I also had this little nagging voice in my head suggesting that maybe it was right and my style of communicating was wrong.</p>
<p>Even if I skipped the tone suggestions and stuck purely to grammar and formatting errors, it didn&rsquo;t speed up my editing process very much. I call it editing, but it might be more correct to call it rewriting. My first drafts have typos and grammatical errors, but they are also often disorganized and lacking a coherent point. AI is going to have to advance a lot further to fix that for me!</p>
<p>Grammarly is still helpful with annoying tasks like checking for &ldquo;its&rdquo; and &ldquo;it&rsquo;s&rdquo;, and it&rsquo;s definitely better than a standard spell checker for catching typos and grammatical errors. I don&rsquo;t typically enjoy editing, but I enjoy this kind of mindless checking the least of all.</p>
<p>Having switched to the free version of Grammarly, I get far fewer suggestions about tone and word choice, which I find better. The app does still share those suggestions occasionally in an attempt to upsell me, but I don&rsquo;t find it too intrusive.</p>
<p>I don&rsquo;t use any of the apps or plugins now. I found them to be exceedingly intrusive. The Windows app added a giant green G button that covered text boxes, and a lot of times it was covering things I wanted to see. The VS Code plugin covered my rough drafts in red and blue lines, and my programmer muscle memory couldn&rsquo;t just ignore them. The browser plugin was the least intrusive, but still too noisy and often not helpful.</p>
<p>Now I use Grammarly primarily via its web interface, and only as a final pass at the end of the editing process.</p>
<p>The technology is promising, and it has helped a bit with some of the more mechanical editing I do. Until it gets a lot better, though, I will only be using it for very specific tasks. I&rsquo;d have to see some pretty major improvements before I&rsquo;d consider Grammarly Premium again.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/blogging" term="blogging" label="blogging" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/reviews" term="reviews" label="reviews" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Getting Unstuck Without a Rubber Duck]]></title>
            <link href="https://jessemcdowell.ca/2023/10/Getting-Unstuck-Without-a-Rubber-Duck/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/05/Sustainable-Errors/?utm_source=atom_feed" rel="related" type="text/html" title="Sustainable Errors" />
                <link href="https://jessemcdowell.ca/2023/09/Choosing-Powerful-Names/?utm_source=atom_feed" rel="related" type="text/html" title="Choosing Powerful Names" />
                <link href="https://jessemcdowell.ca/2023/08/Regarding-Test-Coverage-Targets/?utm_source=atom_feed" rel="related" type="text/html" title="Regarding Test Coverage Targets" />
                <link href="https://jessemcdowell.ca/2023/08/My-Architectural-Report-Template/?utm_source=atom_feed" rel="related" type="text/html" title="My Architectural Report Template" />
                <link href="https://jessemcdowell.ca/2023/07/Design-by-Dogma-Antipattern/?utm_source=atom_feed" rel="related" type="text/html" title="Design by Dogma Antipattern" />
            
                <id>https://jessemcdowell.ca/2023/10/Getting-Unstuck-Without-a-Rubber-Duck/</id>
            
            
            <published>2023-10-03T18:03:27-07:00</published>
            <updated>2023-10-03T18:03:27-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>Building software is a mostly creative endeavour, and as such, it sometimes resists progress. No matter how hard you try to push forward, if you are truly stuck, continuing on the same path is unlikely to work. Fortunately, there are a few different tricks you can use to get going again.</p>
<h2 id="take-a-rest">Take a rest</h2>
<p>The easiest way I&rsquo;ve found to get back on track is to take a break. When in the office this was often making a cup of tea or eating a snack in the break room. It could have been a ten-minute walk around the block, but if I&rsquo;m honest, I have rarely tried this.</p>
<p>Working from home, I typically load the dishwasher or throw a load of laundry in the wash. If I have time, I might do a bit of yoga or exercise.</p>
<p>My favourite is a quick shower which seems to reset thoughts and stimulate creative thinking. Rinsing and drying my face in a bathroom sink is not nearly as effective, but in an office, it may be better than nothing.</p>
<p>If I have the time, or if I&rsquo;m particularly stuck, a night of sleep can do wonders. I almost always have something else just as urgent to work on, so I&rsquo;ll usually switch to that for the rest of the day.</p>
<h2 id="rubber-ducking-and-alternatives">Rubber ducking and alternatives</h2>
<p>Every now and then I get so thoroughly stuck that no amount of taking breaks is going to help. Or sometimes a problem is urgent enough that waiting a day to sleep would be irresponsible. That&rsquo;s when I <a href="https://en.wikipedia.org/wiki/Rubber_duck_debugging">bust out my rubber duck</a>, at least metaphorically. I don&rsquo;t actually have or want a rubber duck because there are several similar alternatives that work as well or better.</p>
<p>The one I use most often is to write an email asking an imaginary colleague for advice. If you treat it like a real work email, it forces you to rethink where you are from someone else&rsquo;s perspective, and can often unlock some insights or surface some tidbits that got overlooked along the way. Writing an email also forces you to check your facts and go through your numbers again. This is harder when a rubber duck is waiting impatiently for you to finish your sentence. Another advantage is that you can use it in an environment where talking would be a nuisance to others.</p>
<p>Sometimes writing an email isn&rsquo;t the best way to communicate a problem though. I have also drawn diagrams on whiteboards with similar results. There are lots of ways to communicate a problem, the key is that you make the effort to reframe it for the benefit of someone not directly involved.</p>
<p>If you are really really stuck, or your problem is really really important, it might be worthwhile to explain it to a real person. This can be a better first step if you&rsquo;re working in areas you don&rsquo;t know very well. It can also be helpful if you&rsquo;re new to a team as it helps you demonstrate trust for your new peers and accelerates team building.</p>
<h2 id="remove-all-distractions">Remove all distractions</h2>
<p>It&rsquo;s certainly not the first thing I want to try, but when a problem is particularly stressful or unpleasant, I can sometimes find myself working on other things on the side of my desk. It may be listening to and editing a playlist, or maintaining an idle chat with a friend. It could also be changing that load of laundry I threw in earlier as a small break. However I got there, if I&rsquo;m really stuck, removing these extra things is another important step.</p>
<h2 id="literally-changing-your-perspective">Literally changing your perspective</h2>
<p>If I&rsquo;ve been looking at a piece of code long enough, sometimes I need to change how I&rsquo;m looking at it to really see it. An easy way to do this is to change the font, or sometimes even just the font size. Even if the content hasn&rsquo;t changed, rearranging the letters a tiny bit seems to force my brain to process it like it&rsquo;s something new.</p>
<p>If I&rsquo;m feeling particularly drained or frustrated with a problem, I have also found it helpful to move to another place for a bit. This is easiest with a laptop, or you may be able to use Remote Desktop from an empty meeting room. In a pinch, I&rsquo;ve taken a pad of paper to a coffee shop to see what happens and been pleasantly surprised.</p>
<h2 id="confirm-the-problem-is-important">Confirm the problem is important</h2>
<p>If you are really, really, really stuck on a problem, maybe there is a good reason. It may be beyond your current capability. It may also not be important enough to justify more effort. A quick chat with your manager shouldn&rsquo;t hurt. If it is dangerous to ask your manager, you may want to find a new manager, but that&rsquo;s <a href="/2023/06/Importance-of-Alignment/">a topic for another post</a>.</p>
<h2 id="ask-for-help">Ask for help</h2>
<p>If your problem is important enough, and you&rsquo;ve made an effort and failed to get yourself unstuck, it&rsquo;s not inappropriate to ask for help. Pair programming is an excellent technique for tackling complicated problems.</p>
<p>Searching or asking online for an answer could also help, but I&rsquo;ve not personally had much luck here. Your results may vary.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/design" term="design" label="design" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/practices" term="practices" label="practices" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Choosing Powerful Names]]></title>
            <link href="https://jessemcdowell.ca/2023/09/Choosing-Powerful-Names/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/08/My-Architectural-Report-Template/?utm_source=atom_feed" rel="related" type="text/html" title="My Architectural Report Template" />
                <link href="https://jessemcdowell.ca/2023/07/Design-by-Dogma-Antipattern/?utm_source=atom_feed" rel="related" type="text/html" title="Design by Dogma Antipattern" />
                <link href="https://jessemcdowell.ca/2023/07/Horizontal-One-on-Ones-and-Talking-Practice/?utm_source=atom_feed" rel="related" type="text/html" title="Horizontal One-on-Ones and Talking Practice" />
                <link href="https://jessemcdowell.ca/2023/06/Importance-of-Alignment/?utm_source=atom_feed" rel="related" type="text/html" title="Importance of Alignment" />
                <link href="https://jessemcdowell.ca/2023/05/Polyglot-Unconference-2023/?utm_source=atom_feed" rel="related" type="text/html" title="Polyglot Unconference 2023" />
            
                <id>https://jessemcdowell.ca/2023/09/Choosing-Powerful-Names/</id>
            
            
            <published>2023-09-18T12:44:36-07:00</published>
            <updated>2023-09-18T12:44:36-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>A junior developer needs to strengthen their technical skills to advance. An intermediate developer needs to strengthen their organizational skills to advance. Senior developers need to master these and also demonstrate that they can move multiple teams forward together. Influencing people (and especially developers) is no easy task, but a positive reputation can do a lot of the heavy lifting. One of the easiest ways to amplify your reputation is to put some extra effort in when choosing a name.</p>
<p>The name of the things you build is the first and most frequent way that your work will be represented to others. It is the very first of the first impressions. A catchy name can generate interest in you and bring more attention to your hard work. A name that shows your personality can make you more approachable and attract people who see the world like you do.</p>
<p>This is especially important in larger companies. There is so much more activity and so many other people trying to move forward that maximizing the impact of your first impressions really helps. I got into this habit when I worked in a big company, and I&rsquo;ve carried it with me ever since. It has served me well.</p>
<p>The technique I use is a pretty standard design technique: get as many options as I (reasonably) can, then pick the best one. I often go through hundreds or thousands of candidates when choosing a name. When it&rsquo;s important enough, I&rsquo;ve gone through more.</p>
<p>Even though I have been through the process several times now, I sometimes find it overwhelming at the start. It goes by quickly once I get started, though, and I can usually find a pretty spectacular name for anything in a couple of hours.</p>
<p>To generate candidates, I like to list out all the things I want the name to convey. I have had good luck with lists of words in relevant domains. For example, when naming a new module for an environmental monitoring tool, I got a great name from a list of meteorological terms.</p>
<p>Once I have some good candidates, I check my finalists against a few important criteria. I want a name to:</p>
<ul>
<li>be relevant to the problem being solved</li>
<li>have a playful spirit</li>
<li>focus on the positive</li>
<li>be respectful of all cultures</li>
<li>be easy to say, spell, and pronounce</li>
<li>not be confusable with common programming keywords or industry terms</li>
</ul>
<p>Playful is a part of my personality; you can take or leave that, but I highly suggest staying positive. Even if it&rsquo;s meant in fun, using negative imagery in your name can give people the wrong idea about you. The point of this exercise is to get more people interested, not to push people away.</p>
<p>Being respectful of all cultures is becoming more important as the internet continues connecting the whole world together. Some words can have unexpected or unpleasant connotations for people who live (or have lived) in other cultures. Some words commonly used in Western culture should be retired now. Names from nature, science, geography, or popular fiction are usually safe. References to religions, cultures, or historical figures or groups should be avoided.</p>
<p>After focusing on a name for hours, it&rsquo;s not uncommon to develop tunnel vision. Before pasting a name all over my files and objects, I like to get an external perspective. Asking a boss or a peer can save a lot of time if your name isn&rsquo;t quite as great as you thought.</p>
<p>Once the name is set, all that&rsquo;s left is living up to it.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/career" term="career" label="career" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/design" term="design" label="design" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Case of the Slow Matchmaking Routine]]></title>
            <link href="https://jessemcdowell.ca/2023/09/Case-of-the-Slow-Matchmaking-Routine/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/07/Case-of-the-Appearing-Users/?utm_source=atom_feed" rel="related" type="text/html" title="Case of the Appearing Users" />
                <link href="https://jessemcdowell.ca/2023/05/Is-the-Bug-Fun/?utm_source=atom_feed" rel="related" type="text/html" title="Is the Bug Fun?" />
                <link href="https://jessemcdowell.ca/2023/05/Case-of-the-Disappearing-Users/?utm_source=atom_feed" rel="related" type="text/html" title="Case of the Disappearing Users" />
                <link href="https://jessemcdowell.ca/2023/04/How-to-Fix-a-Bug/?utm_source=atom_feed" rel="related" type="text/html" title="How to Fix a Bug" />
                <link href="https://jessemcdowell.ca/2023/03/How-to-Report-a-Bug/?utm_source=atom_feed" rel="related" type="text/html" title="How to Report a Bug" />
            
                <id>https://jessemcdowell.ca/2023/09/Case-of-the-Slow-Matchmaking-Routine/</id>
            
            
            <published>2023-09-05T15:08:46-07:00</published>
            <updated>2023-09-05T15:08:46-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>The most challenging bug I&rsquo;ve ever fixed was a performance issue in a matchmaking routine. Matchmaking is the process of finding players to compete against each other in a video game. An excellent matchmaking algorithm doesn&rsquo;t just stick players together randomly; it tries to make the game more fun by balancing power levels and preventing anyone from waiting too long for a match.</p>
<p>About six weeks before a game I was working on was scheduled to be feature complete, we discovered our routine couldn&rsquo;t handle our load targets. The rate at which players were being removed from the matchmaking queue started dropping during load tests. Things got bad quickly once it fell below the rate at which we inserted them. Not only would this cause a bad user experience if we didn&rsquo;t fix it, but it made it impossible for us to drive enough traffic to our game servers to test that they could handle the projected load. The company wasn&rsquo;t going to release a game that could crash if it was successful, so we had to fix this issue, and we had to fix it quickly.</p>
<p>To make matters worse, we could only reproduce the bug in our production system. We had to simulate hundreds of thousands of users to hit it, which just wasn&rsquo;t possible on a developer machine. Fortunately, our production system was available for testing. Unfortunately, a team on another continent was running the load tests. It took us a day to deploy a new version and a couple of days for them to run the test. With time zone differences expanding the handoffs, the fastest we could iterate was about once a week.</p>
<p><a href="/2023/04/How-to-Fix-a-Bug/">As I&rsquo;ve described before</a>, I like to find a bug, understand it, and then fix it. In this case we couldn&rsquo;t figure it out by looking at the code, even with many smart people trying. This is one of those rare cases where I had to try fixing a bug before understanding it.</p>
<p>The week of iteration time also meant I had about a week to review the results, consider multiple possibilities, and sprinkle logging and profiling code all over the place, so I did that, too.</p>
<p>I plugged away at it week after week, but the problem was stubbornly difficult to find. The failing load tests were visible to our management chain, so I also had a fair number of questions about when it would be fixed (which I couldn&rsquo;t estimate), how we were fixing it, and what we would do if we couldn&rsquo;t fix it. I was also helping the rest of the team with their deliverables, which had the same deadline, so it was stressful for everyone. Nevertheless, I stuck to my process, narrowed down the location of the problem, and tested my fixes as best as possible in my development environment. Eventually, I figured it out.</p>
<p>The problem originated in a database query we used to find matches. It used a cartesian join, which takes exponentially longer to complete as more rows are added (<code>O(n²)</code> complexity). At some queue length, the processing time increases from milliseconds to seconds, and then it quickly increases from seconds to minutes. It also didn&rsquo;t help that our contrived load test meant thousands of users were all attempting to make matches with the same power rating, thus making it harder to distinguish suitable matches.</p>
<p>After all the time it took to find the problem, it only took a day to fix it. I replaced the algorithm with one that processed matches linearly, giving it <code>O(n)</code> complexity.</p>
<p>About a week later, I got an email confirming that the issue was fixed. Since the report came in overnight, it was the first thing I read in the morning. It was exhilarating, but the day is still bittersweet in my memory. About an hour later, I attended a last-minute team-wide meeting where we were all let go. The game was never released.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/war-stories" term="war-stories" label="war stories" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/debugging" term="debugging" label="debugging" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/video-games" term="video-games" label="video games" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Regarding Test Coverage Targets]]></title>
            <link href="https://jessemcdowell.ca/2023/08/Regarding-Test-Coverage-Targets/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/07/Horizontal-One-on-Ones-and-Talking-Practice/?utm_source=atom_feed" rel="related" type="text/html" title="Horizontal One-on-Ones and Talking Practice" />
                <link href="https://jessemcdowell.ca/2023/05/Sustainable-Errors/?utm_source=atom_feed" rel="related" type="text/html" title="Sustainable Errors" />
                <link href="https://jessemcdowell.ca/2023/04/How-to-Fix-a-Bug/?utm_source=atom_feed" rel="related" type="text/html" title="How to Fix a Bug" />
                <link href="https://jessemcdowell.ca/2023/03/How-to-Report-a-Bug/?utm_source=atom_feed" rel="related" type="text/html" title="How to Report a Bug" />
                <link href="https://jessemcdowell.ca/2023/08/My-Architectural-Report-Template/?utm_source=atom_feed" rel="related" type="text/html" title="My Architectural Report Template" />
            
                <id>https://jessemcdowell.ca/2023/08/Regarding-Test-Coverage-Targets/</id>
            
            
            <published>2023-08-21T11:28:52-07:00</published>
            <updated>2023-08-21T11:28:52-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>Unit tests are undeniably a good thing, but you only realize the full benefits of them when you have enough tests that you can make changes with confidence. If you can make a change, run your tests, and be comfortable enough to ship your changes, then you and your team can get work done much faster. More drastic changes to the shared code become feasible. Life gets better.</p>
<p>It makes sense then that teams want to ensure that code is sufficiently covered with tests. Nobody wants to count tests every time they review a PR, so tools are added that check it automatically. It&rsquo;s then a small step to set a coverage target, and suddenly you have a machine checking every PR for tests. This all makes sense to me, and it was my first instinct too. I don&rsquo;t recommend this approach any more.</p>
<p>The problem with test coverage tools is that they can&rsquo;t (at least, can&rsquo;t yet) measure the quality or value of a test. They instead measure the quantity of code that the tests exercise. This can encourage a misplaced focus on building lots of low-value tests. For example, consider the following piece of relatively standard web service code:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="n">GetResult</span><span class="w"> </span><span class="nf">get</span><span class="p">(</span><span class="n">GetRequest</span><span class="w"> </span><span class="n">request</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">service</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="n">request</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>Here is a standard test for this function:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="cl"><span class="nd">@Test</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">getCallBusinessLayerGetAndReturnResult</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">var</span><span class="w"> </span><span class="n">request</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">GetRequest</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">var</span><span class="w"> </span><span class="n">expected</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">new</span><span class="w"> </span><span class="n">GetResult</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">when</span><span class="p">(</span><span class="n">mockBusiness</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="n">request</span><span class="p">)).</span><span class="na">thenReturn</span><span class="p">(</span><span class="n">expected</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="kd">var</span><span class="w"> </span><span class="n">actual</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">service</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="n">request</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">verify</span><span class="p">(</span><span class="n">mockBusiness</span><span class="p">).</span><span class="na">get</span><span class="p">(</span><span class="n">request</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="n">assertEquals</span><span class="p">(</span><span class="n">expected</span><span class="p">,</span><span class="w"> </span><span class="n">actual</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span></code></pre></div><p>And now imagine thousands of tests like this, all testing very similar functions.</p>
<p>The unit test is testing the expected behavior of the service function, but what are the chances of there being a bug in a function this simple? I think it&rsquo;s far more likely that the test would be written incorrectly than the service function.</p>
<p>The worst case scenario would be if all these service functions and all of these tests were generated by copy-paste and modification. Then it is much more likely that the wrong values get pasted into the test and the implementation at the same time. Unfortunately this kind of boilerplate code is almost always generated with copy and paste because it&rsquo;s fast and and easy.</p>
<p>You could make an argument that you should avoid architectures that encourage lots of boring boilerplate code. I agree with that idea, but in my experience most teams are not mature enough to design systems that prevent it. It is easy to follow simple service-business-repository patterns blindly, and to be honest, for most software this is good enough.</p>
<p>Again: I do think unit tests are a good thing, and good test coverage is essential to get good value from them, but unit tests also have a cost. Unit tests often need to be changed when code is being changed. If you have lots of low-value tests testing lots of simple methods, you can quickly get overwhelmed trying to make non-trivial changes. Unit tests are supposed to make it safer to go faster&hellip; but poorly written tests can do the exact opposite too.</p>
<p>Of course there is an exception to every rule. If you are writing the software for my bank, or for medical equipment, or for self driving cars, please enforce 100% coverage and use several other tools to enure an extremely high quality. For most of us though, not all of the tests are actually worth the effort of writing them, and I don&rsquo;t want to edit the coverage percentage every time I make a change.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/architecture" term="architecture" label="architecture" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/practices" term="practices" label="practices" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/quality" term="quality" label="quality" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/unit-testing" term="unit-testing" label="unit testing" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[My Architectural Report Template]]></title>
            <link href="https://jessemcdowell.ca/2023/08/My-Architectural-Report-Template/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/07/Design-by-Dogma-Antipattern/?utm_source=atom_feed" rel="related" type="text/html" title="Design by Dogma Antipattern" />
                <link href="https://jessemcdowell.ca/2023/07/Horizontal-One-on-Ones-and-Talking-Practice/?utm_source=atom_feed" rel="related" type="text/html" title="Horizontal One-on-Ones and Talking Practice" />
                <link href="https://jessemcdowell.ca/2023/05/Sustainable-Errors/?utm_source=atom_feed" rel="related" type="text/html" title="Sustainable Errors" />
                <link href="https://jessemcdowell.ca/2011/01/when-to-add-an-orm-tool/?utm_source=atom_feed" rel="related" type="text/html" title="When to Add an ORM Tool" />
                <link href="https://jessemcdowell.ca/2010/12/invitations-and-the-vcard-format/?utm_source=atom_feed" rel="related" type="text/html" title="Invitations and the VCard Format" />
            
                <id>https://jessemcdowell.ca/2023/08/My-Architectural-Report-Template/</id>
            
            
            <published>2023-08-08T12:45:13-07:00</published>
            <updated>2023-08-08T12:45:13-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>As an architect I&rsquo;ve been asked to answer a lot of hard questions. I used to waste time figuring out how to structure my answers, preventing me from getting into a good flow sooner. Now I have a simple template that is easy to use, easy to read, and saves me that wasted time up front.</p>
<p>This template works for simple reports that are only a couple of pages, but can easily be adjusted or expanded for more complicated or much larger documents.</p>
<p>The template has these sections:</p>
<ul>
<li>Purpose</li>
<li>Context</li>
<li>Recommendation</li>
<li>Paths Not Taken</li>
<li>Questions and Answers</li>
</ul>
<p>I typically start a new problem by creating a new document and sticking in these headings. Next, I work to understand and document the context of the problem. Once that&rsquo;s complete, I can design and describe my answer.</p>
<div class="table-of-contents">
    <nav id="TableOfContents">
  <ul>
    <li><a href="#surfacing-context">Surfacing Context</a>
      <ul>
        <li><a href="#purpose-section">Purpose Section</a></li>
        <li><a href="#context-section">Context Section</a></li>
        <li><a href="#document-title">Document Title</a></li>
        <li><a href="#stakeholder-check-in">Stakeholder Check-in</a></li>
      </ul>
    </li>
    <li><a href="#describing-a-solution">Describing a Solution</a>
      <ul>
        <li><a href="#recommendation-section">Recommendation Section</a></li>
        <li><a href="#paths-not-taken-section">Paths Not Taken Section</a></li>
        <li><a href="#questions-and-answers-section">Questions and Answers Section</a></li>
      </ul>
    </li>
    <li><a href="#customization">Customization</a></li>
    <li><a href="#template">Template</a></li>
    <li><a href="#acknowledgements">Acknowledgements</a></li>
  </ul>
</nav>
</div>

<h2 id="surfacing-context">Surfacing Context</h2>
<p>It can be tempting to jump straight into solutioning, however, and I cannot emphasize this point enough, you cannot design a good solution to a problem if you don&rsquo;t understand the problem. I enjoy armchair architecture as much as anyone, but a good architect chooses the best solution for the circumstances, not just the first solution they thought of.</p>
<p>I always start with the first two sections of this document. This is usually only a page of text, but it really helps to frame and validate the problem from a business / customer perspective before starting the technical design.</p>
<h3 id="purpose-section">Purpose Section</h3>
<p>This first section has just a few brief sentences. It starts by explaining the purpose of the document, and then the scope of the document, and then a description of who it&rsquo;s written for. It can seem a bit superfluous, but these details are important to remember when writing the rest of the document. It can also help a reader&rsquo;s understanding if they know the context in which the document was written.</p>
<h4 id="example">Example</h4>
<blockquote>
<p>This document recommends an approach to boiling the ocean. It is intended to be used during discussions with potential partners for the project. It is written for the CTO and the PMO.</p></blockquote>
<h3 id="context-section">Context Section</h3>
<p>This is the most important section, and sometimes the hardest thing to get right. I only put in a few nuggets of information, but they need to be important. They serve as a sales pitch for upstream stakeholders and simultaneously guide and rationalize the decisions in the following sections. Even if the context seems obvious it is good to write it out.</p>
<p>I write some bullet points to articulate the importance of the problem and any significant constraints to the design. I keep it to just a few items, ideally between 3 and 6. If there are too many it becomes harder to process, if there are too few I probably shouldn&rsquo;t be spending my time on the problem.</p>
<p>I like to polish each point a bit, but I also avoid flowery or superfluous language. Neutral defensible facts laid out clearly are a powerful tool for building alignment. It&rsquo;s also good to write these in a way that makes sense to stakeholders who are not technically focused.</p>
<p>When I&rsquo;m faced with an unclear problem, I will sometimes stick together some context statements based on my best guesses and send them out for feedback. <a href="https://meta.wikimedia.org/wiki/Cunningham%27s_Law">Cunningham&rsquo;s Law</a> applies to all of design - it&rsquo;s much easier to get corrections to a bad answer than ask for an answer in a vacuum. There are more specific techniques to help extract and validate this kind of information, but they take a lot more time from stakeholders so I try to use them only when necessary.</p>
<h4 id="example-1">Example</h4>
<blockquote>
<ul>
<li>Over 80% of internationally traded goods travel by sea. Marine freight is exposed to many dangers including navigational challenges, extreme weather, and piracy.</li>
<li>The sea level has been steadily rising due to human-caused climate change. As the ocean continues to rise, estimates anticipate hundreds of millions to billions of people will be displaced unless additional measures are taken.</li>
<li>The earth is simultaneously affected by an international housing shortage and the continual loss of essential farm land.</li>
<li>Eliminating the world&rsquo;s oceans can simplify or make new solutions available to all of the problems above, which could in turn create numerous opportunities for raising and generating capital.</li>
</ul></blockquote>
<h3 id="document-title">Document Title</h3>
<p>A descriptive title is an important part of any document. I don&rsquo;t want to spend too long figuring this out up front, but I do make sure I have a good one before any document gets circulated. Once it gets sent out, changing the name can break links and make it harder for people to find.</p>
<p>I also like to include the date in the file name and title. These documents are snapshots of the best information available at the time they are written. Since this is usually before a project has started, they lose a lot of their value once things get underway. A date in the title makes it clear when the document is an archeological artifact.</p>
<p>I always use the <code>yyyy-mm</code> format for dates because it&rsquo;s precise enough, and the metric system is a good thing.</p>
<p>I could update the document as the project goes along, but I don&rsquo;t see much value in this. The document has served its purpose once the decision is accepted and things start. If we need some sort of onboarding guide or architectural documentation, the parts of this document that are valuable for that effort can be easily copied and updated as needed.</p>
<h4 id="example-2">Example</h4>
<blockquote>
<p>How to Boil the Ocean - 2023-08</p></blockquote>
<h3 id="stakeholder-check-in">Stakeholder Check-in</h3>
<p>Once I have the above sections completed I like to check in with my upstream stakeholders.</p>
<p>Sometimes this process can take a bit of time. Depending on the size and urgency of the problem I might start on the technical design while collecting feedback. Parts of the design could be invalidated if the context changes, but usually any effort will still have nuggets that can be reused.</p>
<p>When I don&rsquo;t get any significant feedback it can be a sign that the problem is either not well understood or not important to anyone. I might then schedule some workshops to dig in further before continuing.</p>
<h2 id="describing-a-solution">Describing a Solution</h2>
<p>There is a lot that goes into answering a technical question, and the way I go about it depends on a lot of factors.</p>
<p>Before I get too far into it, I like to think about how much I should be collaborating on the answer. Even if the answer seems obvious to me, it is sometimes valuable to involve the people who&rsquo;ll be building the solution to ensure better buy-in once the project starts.</p>
<p>It&rsquo;s also important to think about the acceptable amount of risk. If the cost of getting the solution wrong is huge, it may be necessary to do more thorough research or build and test some prototypes. If I&rsquo;m just putting some rough estimates together to help plan the roadmap I will put in considerably less effort.</p>
<p>After all the thinking and validating work is done, I dump the best design into the document.</p>
<h3 id="recommendation-section">Recommendation Section</h3>
<p>This is where I describe the answer to the question. I sometimes change the heading depending on the purpose of the document. If I&rsquo;m preparing a thorough design for a complex system I might include multiple sub-headings and a few diagrams. For documents used more for resource planning I might just provide a brief architectural summary and a table of high-level tasks with t-shirt resolution estimates.</p>
<p>There are lots of ways to communicate designs. I do spend some time thinking about how best to do this. If I can use more efficient tools that are suitable for the audience and purpose of the document I can produce less. Smaller documents are easier to read, and easier to update when the feedback comes in.</p>
<h4 id="example-3">Example</h4>
<blockquote>
<p>Use barges with nuclear fission reactors to boil the ocean water. As the sea level drops, the barges can be moved to ensure they stay submerged in water.</p>
<p>The steam generated by the reactors can also be used to generate electricity. Electricity not used for the operation of the project can be sold, adding an additional revenue stream.</p></blockquote>
<h3 id="paths-not-taken-section">Paths Not Taken Section</h3>
<p>I&rsquo;ve found it is often helpful to list some of the other possibilities that were considered but not chosen. Not everyone cares about this, but it can answer a lot of &ldquo;what about X&rdquo; questions from some personality types. It can also be helpful if you look back at a document years later. It&rsquo;s not the most important part of the document though, so I try not to put too much effort into it. Bullet points and rough notes are usually enough.</p>
<h4 id="example-4">Example</h4>
<blockquote>
<ul>
<li>Use a giant laser from space.
<ul>
<li>The laser beams would be a hazard to air and space traffic.</li>
</ul>
</li>
<li>Accelerate global warming by burning a lot of oil.
<ul>
<li>The required oil would make this approach significantly more expensive than other options.</li>
<li>This option could generate too much negative PR.</li>
</ul>
</li>
<li>Pump the water into on-land boiling stations.
<ul>
<li>Cost of pumping would be significant.</li>
<li>Hoses and pumping stations would need to be continually added as the sea level drops.</li>
</ul>
</li>
<li>Use nuclear fusion reactors to generate heat.
<ul>
<li>The technology is not mature enough at the time of this writing. If it does become feasible during the project we could update the design for any new barges being constructed.</li>
</ul>
</li>
</ul></blockquote>
<h3 id="questions-and-answers-section">Questions and Answers Section</h3>
<p>There will often be some things that I want to communicate that don&rsquo;t fit anywhere else in the document. A Question and Answers section is really versatile, and it&rsquo;s an easy place to add more information when responding to feedback.</p>
<h4 id="example-5">Example</h4>
<blockquote>
<ul>
<li>Is boiling the ocean really a responsible thing to do?
<ul>
<li>That is outside the scope of this document.</li>
</ul>
</li>
<li>How will the boiling stations deal with salt accumulation?
<ul>
<li>This will need to be investigated further if we proceed with this project.</li>
</ul>
</li>
</ul></blockquote>
<h2 id="customization">Customization</h2>
<p>Just like in software development, I find it best to use the simplest document that can possibly do the job. The less I write, the easier the document is to read, and the more time I have to work on my next project.</p>
<p>If I don&rsquo;t need one of the sections above I&rsquo;ll remove it. I can also add new sections as I need them. I have added sections with estimate tables, rudimentary project plans, optional features, glossary of terminology, and so on.</p>
<h2 id="template">Template</h2>
<p>I&rsquo;ve created a standalone page with the example from this post so you can use it as a template for your own work. You can find it under <a href="/resources/architectural-report-template/">Resources, Architectural Report Template</a>. If you do use it I suggest replacing the contents before sharing it to prevent any embarrassment.</p>
<h2 id="acknowledgements">Acknowledgements</h2>
<p>This template was inspired by the <a href="https://github.com/joelparkerhenderson/architecture-decision-record/blob/main/templates/decision-record-template-by-michael-nygard/index.md">Decision record template by Michael Nygard</a> which I also recommend for its intended purpose.</p>
<p>If you want to know more about how to write architectural reports, gather information, or validate designs, I highly recommend the book <a href="https://pragprog.com/titles/mkdsa/design-it/">Design It!: From Programmer to Software Architect</a> by Michael Keeling. I learned a lot from this book, and have referred to its contents many times since.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/architecture" term="architecture" label="architecture" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/design" term="design" label="design" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/resources" term="resources" label="resources" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Design by Dogma Antipattern]]></title>
            <link href="https://jessemcdowell.ca/2023/07/Design-by-Dogma-Antipattern/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/05/Sustainable-Errors/?utm_source=atom_feed" rel="related" type="text/html" title="Sustainable Errors" />
                <link href="https://jessemcdowell.ca/2011/01/when-to-add-an-orm-tool/?utm_source=atom_feed" rel="related" type="text/html" title="When to Add an ORM Tool" />
                <link href="https://jessemcdowell.ca/2010/12/invitations-and-the-vcard-format/?utm_source=atom_feed" rel="related" type="text/html" title="Invitations and the VCard Format" />
                <link href="https://jessemcdowell.ca/2010/09/themis-system-design/?utm_source=atom_feed" rel="related" type="text/html" title="Themis: System Design" />
                <link href="https://jessemcdowell.ca/2010/08/shared-resource-service-requirements/?utm_source=atom_feed" rel="related" type="text/html" title="Shared Resource Service Requirements" />
            
                <id>https://jessemcdowell.ca/2023/07/Design-by-Dogma-Antipattern/</id>
            
            
            <published>2023-07-30T13:41:17-07:00</published>
            <updated>2023-07-30T13:41:17-07:00</updated>
            
            
            <content type="html"><![CDATA[<blockquote>
<p>Always use a NoSQL database so your app can scale.</p></blockquote>
<p>NoSQL databases can be more scalable, but schema-on-read has other drawbacks. NoSQL databases are much less capable of transactional changes. Relationships are difficult or impossible. Designing schemas to be efficient is much harder, and requires more up-front knowledge about your problem. NoSQL databases are sometimes the right tool for the job, but they are not the right tool for every job.</p>
<blockquote>
<p>Monoliths are bad. Build Microservices.</p></blockquote>
<p>Microservices have advantages, but they are harder to build. They are harder to deploy. They require more tools, processes, and governance to keep them running and working together properly. If they aren&rsquo;t carved up just right they can create cross-team dependencies and performance bottlenecks. All that being said, they are ane excellent tool for very large organizations to decouple and decompose development teams.</p>
<blockquote>
<p>Never use reflection because it&rsquo;s slow.</p></blockquote>
<p>Reflection can be a heavy operation, but it can also give you extremely valuable information. If it does prove to be a performance problem, there are also other optimizations which can help, or at worst, you can try to restrict lookups to development and test environments. Stack traces in exceptions can be extremely helpful in finding bugs. Even if they do cost a bit to collect them, the developer time wasted figuring out bugs, and the unhappy customers waiting for fixes may cost you more.</p>
<hr>
<p>I have heard these kinds of absolute statements many times in my career. To be fair: building software is hard and there is a lot to learn, and simple statements are a comfortable simplification. The problem is that few absolutes exist.</p>
<p>It doesn&rsquo;t help that so much information in our industry is presented this way. Blogs that pronounce something bad get clicks. Vendors don&rsquo;t like to tell you their drawbacks. Everyone wants you to agree with them.</p>
<p>This kind of blind adherence to arbitrary rules is bad engineering. We should work to understand the advantages and disadvantages of our technical choices, and understand when it is important to get them right.</p>
<p>Since I started with a quote, I&rsquo;ll end with another:</p>
<blockquote>
<p>When you believe in things that you don&rsquo;t understand
Then you suffer
Superstition ain&rsquo;t the way</p>
<p>&ndash; Superstition by Stevie Wonder</p></blockquote>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/design" term="design" label="design" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Horizontal One-on-Ones and Talking Practice]]></title>
            <link href="https://jessemcdowell.ca/2023/07/Horizontal-One-on-Ones-and-Talking-Practice/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2016/08/infiltrating-an-organization/?utm_source=atom_feed" rel="related" type="text/html" title="Infiltrating an Organization (or: Joining a New Team)" />
                <link href="https://jessemcdowell.ca/2011/10/sharpening-the-saw/?utm_source=atom_feed" rel="related" type="text/html" title="Sharpening The Saw" />
                <link href="https://jessemcdowell.ca/2023/06/Importance-of-Alignment/?utm_source=atom_feed" rel="related" type="text/html" title="Importance of Alignment" />
                <link href="https://jessemcdowell.ca/2023/05/Polyglot-Unconference-2023/?utm_source=atom_feed" rel="related" type="text/html" title="Polyglot Unconference 2023" />
                <link href="https://jessemcdowell.ca/2023/05/Sustainable-Errors/?utm_source=atom_feed" rel="related" type="text/html" title="Sustainable Errors" />
            
                <id>https://jessemcdowell.ca/2023/07/Horizontal-One-on-Ones-and-Talking-Practice/</id>
            
            
            <published>2023-07-18T15:35:11-07:00</published>
            <updated>2023-07-18T15:35:11-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>When I was promoted to the role of architect it was a new role in the organization. The stakeholders I had to work with were not used to talking to an architect, and weren&rsquo;t sure what I did or when I should be involved in a conversation. I started using recurring one-on-one meetings with each stakeholder separately. It worked great. It&rsquo;s also made me a much better communicator.</p>
<p>One of the first and most important lessons I learned as an architect is that you can&rsquo;t design a good architecture without a good understanding of its requirements. You can design a system in a vacuum, it&rsquo;s also much easier to do it this way, but it&rsquo;s far less likely to serve the organization. Gathering, validating, and documenting technical requirements is tough work, but an essential part of being an architect.</p>
<p>The best way to discover needs and requirements is to talk to people who know them. There is quite a lot of ways to approach this, and a lot has been written on this subject. For me, I knew I needed to chat with my stakeholders early and often, and private meetings was the simplest technique that could possibly work.</p>
<p>In my first one-on-one meetings I explained my role, and more importantly, why my role would be beneficial to each of them. For the product manager I explained that if I was good at my job I would help the development team go faster (because of less rework) and to reduce the technical risks of higher value projects before they were scheduled. For the product owner and development leads I explained that I would find and encourage new technologies that helped the team go faster, and help to keep projects moving smoothly.</p>
<p>I didn&rsquo;t have any difficulty getting time on a regular schedule. Even with a 3-month release cycle, there was always enough to fill a meeting every two weeks with each of my main stakeholders. I always came prepared with questions, and after a while my stakeholders starting bringing questions for me too.</p>
<p>I think I have pretty good communication skills, but when you need to explain the value of technical work to people who are primarily non-technical it can be a lot more difficult. It&rsquo;s even more daunting because the people I was meeting with were far, far better presenters and public speakers than I am.</p>
<p>I started noticing that I was getting very similar questions from different stakeholders, and so I was able to re-use my answers. And with time, I started getting better at it. Regular conversations certainly helped, but talking about the same topics over and over seemed to make an even bigger difference. Every time I answered a similar question I had a chance to refine my best points and try out new ones.</p>
<p>I was fortunate to work in an environment with a lot of trust: I could speak my mind honestly, and I didn&rsquo;t really need to be persuasive to make sure my ideas were heard. Even still, the better I got, the more confident I felt. Portraying confidence is important in a leadership position - if a leader looks worried it can generate more stress for those around them.</p>
<p>After a few years of these meetings, I am convinced that they were a big part of my success as an architect. I&rsquo;m also convinced that they&rsquo;ve helped me improve as a communicator.</p>
<p>Repeating similar conversation has been so valuable, that I&rsquo;ve started having practice conversations just for the sake of it. Rough notes and research is still my preferred place to start, but this type of preparation starts to have diminishing returns after a while. If I really need to nail a conversation, a practice conversation is an efficient way to improve my messages. All it takes is a bit of time from a trusted colleague, or if that&rsquo;s not possible, a rubber duck can do the job too.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/architecture" term="architecture" label="architecture" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/career" term="career" label="career" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/practices" term="practices" label="practices" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Case of the Appearing Users]]></title>
            <link href="https://jessemcdowell.ca/2023/07/Case-of-the-Appearing-Users/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/05/Case-of-the-Disappearing-Users/?utm_source=atom_feed" rel="related" type="text/html" title="Case of the Disappearing Users" />
                <link href="https://jessemcdowell.ca/2023/05/Is-the-Bug-Fun/?utm_source=atom_feed" rel="related" type="text/html" title="Is the Bug Fun?" />
                <link href="https://jessemcdowell.ca/2023/04/How-to-Fix-a-Bug/?utm_source=atom_feed" rel="related" type="text/html" title="How to Fix a Bug" />
                <link href="https://jessemcdowell.ca/2023/03/How-to-Report-a-Bug/?utm_source=atom_feed" rel="related" type="text/html" title="How to Report a Bug" />
                <link href="https://jessemcdowell.ca/2013/06/reading-server-graphs-connected-users/?utm_source=atom_feed" rel="related" type="text/html" title="Reading Server Graphs: Connected Users" />
            
                <id>https://jessemcdowell.ca/2023/07/Case-of-the-Appearing-Users/</id>
            
            
            <published>2023-07-10T13:18:09-07:00</published>
            <updated>2023-07-10T13:18:09-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>A couple of years after solving <a href="/2023/05/Case-of-the-Disappearing-Users">The Case of The Disappearing Users</a>, I was assigned another high profile bug where new users were being spontaneously created. They were being generated without a name or any profile information, but still filling up space in lists and appearing on schedules. A couple of other developers had tried fixing it but had no luck, so it was assigned to me.</p>
<p>I went through my usual bag of tricks: searched recent changes, searched for insert statements, tried to create empty users manually (and couldn&rsquo;t). Nothing worked, and it was looking pretty hopeless.</p>
<p>I knew I wasn&rsquo;t going to make any progress if I couldn&rsquo;t narrow down the cause. The problem was only occurring for one customer, but it was also only occurring around once a week. I ended up making a patched version of our application that regularly scanned the number of users and threw up a notification if a new one had appeared. I also added some custom logging in a few conspicuous areas, and sent it to the customer.</p>
<p>The issue appeared again, but this time we were able to collect the logs and analyze them. I was able to determine that the problem was occurring for a specific user, and I was able to figure out which application had caused it. The weird thing: this application didn&rsquo;t have a feature for creating users! We were able to question the user, and it turned out they had a new computer, and it had a brand new mouse.</p>
<p>It&rsquo;s important to understand that this application was build using Microsoft Access (hey, don&rsquo;t judge). One of the best features of Access in the data binding, saving your from writing a lot of boilerplate code. All you have to do is write a query and bind the controls on the form to the desired fields.</p>
<p>One of the (many) challenges with Access is that it is sometimes a bit too clover. It could convert a select statement into insert / update / delete statements automatically. If you navigated the form past the last row of the query (even if your query only selected a single row) it would go into creation mode.</p>
<p>The form where the row was created had all the keyboard navigation shortcuts blocked, as was the standard practice, but the user was still able to trigger it. This is where the new mouse comes in. This problem occurred around the time when mice just started shipping with scroll wheels, and in Access it automatically triggered row navigation.</p>
<p>We weren&rsquo;t able to disable this behavior, but I was able to rewrite the query and modify the form so that a new row couldn&rsquo;t be generated any more. We also updated our installation instructions to ban mice with scroll wheels in case the same problem could be triggered in any of our hundreds of other forms.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/debugging" term="debugging" label="debugging" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/war-stories" term="war-stories" label="war stories" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Automating Non-Non-Downtime Upgrades in Kubernetes with ArgoCD]]></title>
            <link href="https://jessemcdowell.ca/2023/06/Automating-Non-Non-Downtime-Upgrades-in-Kubernetes-with-ArgoCD/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2013/06/reading-server-graphs-connected-users/?utm_source=atom_feed" rel="related" type="text/html" title="Reading Server Graphs: Connected Users" />
                <link href="https://jessemcdowell.ca/2012/04/installutil-and-badimageformatexception/?utm_source=atom_feed" rel="related" type="text/html" title="InstallUtil and BadImageFormatException - Facepalm" />
                <link href="https://jessemcdowell.ca/2011/01/working-together-and-having-fun/?utm_source=atom_feed" rel="related" type="text/html" title="Working Together and Having Fun" />
                <link href="https://jessemcdowell.ca/2010/12/teaching-it/?utm_source=atom_feed" rel="related" type="text/html" title="Teaching IT" />
                <link href="https://jessemcdowell.ca/2010/11/64-bit-iis-vs-32-bit-assemblies/?utm_source=atom_feed" rel="related" type="text/html" title="64-bit IIS vs. 32-bit Assemblies" />
            
                <id>https://jessemcdowell.ca/2023/06/Automating-Non-Non-Downtime-Upgrades-in-Kubernetes-with-ArgoCD/</id>
            
            
            <published>2023-06-26T10:55:24-07:00</published>
            <updated>2023-06-26T10:55:24-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>I recently worked on a project to move a complicated legacy application onto Kubernetes. It was quite an undertaking, but in the end we were successful. One of the biggest challenges was figuring out how to automate our legacy deployment process, one where the whole application has to be stopped completely for schema upgrades to run.</p>
<p>The normal &ldquo;Kubernetes way&rdquo; to upgrade an application is by changing the <a href="https://kubernetes.io/docs/concepts/workloads/controllers/deployment/">Deployment resource</a>. With its default <code>RollingUpdate</code> strategy it will delete a pod with the old definition, start a pod with the new definition, wait for it to be healthy, then repeat continuously until the change is fully applied.</p>
<p>This process wouldn&rsquo;t work for us, and it was not obvious how we could automate one that did. Our application is tied to its schema version; new versions of the app won&rsquo;t run on the old schema, old versions of the app can&rsquo;t run on the new schema, and the schema migrator won&rsquo;t start if it detects any running applications. We would have preferred to use a rolling update without downtime, but it wasn&rsquo;t possible to make our application support this in our timelines. I expect it will eventually be implemented, but it will require several changes and a significant testing effort.</p>
<p>The process we wanted to automate was:</p>
<ol>
<li>Shut down the old version of the application</li>
<li>Run the schema migrator</li>
<li>Start the new version of the application</li>
</ol>
<p>Or putting it into Kubernetes terms:</p>
<ol>
<li>Delete all the pods</li>
<li>Run the schema migrator job</li>
<li>Create new pods (with new image tag)</li>
</ol>
<p>We tried a few different approaches, but the solution we ultimately chose was using <a href="https://argoproj.github.io/cd/">ArgoCD</a> with its <a href="https://argo-cd.readthedocs.io/en/stable/user-guide/sync-waves/">sync phases and waves feature</a>. There were a few unexpected challenges, but we were still happy with the results.</p>
<div class="table-of-contents">
    <nav id="TableOfContents">
  <ul>
    <li><a href="#what-is-argocd-and-what-are-sync-phases-and-waves">What is ArgoCD and What Are Sync Phases and Waves?</a></li>
    <li><a href="#how-we-automated-our-deployment-process">How We Automated our Deployment Process</a></li>
    <li><a href="#branching-strategy">Branching Strategy</a></li>
    <li><a href="#the-good-our-upgrade-process">The Good: Our Upgrade Process</a></li>
    <li><a href="#the-bad-scaling-pods-without-downtime">The Bad: Scaling Pods Without Downtime</a></li>
    <li><a href="#the-ugly-automatic-synchronization">The Ugly: Automatic Synchronization</a></li>
    <li><a href="#conclusion">Conclusion</a></li>
  </ul>
</nav>
</div>

<h2 id="what-is-argocd-and-what-are-sync-phases-and-waves">What is ArgoCD and What Are Sync Phases and Waves?</h2>
<p><a href="https://argoproj.github.io/cd/">ArgoCD</a> is a powerful open source tool that lets you deploy Helm charts to a Kubernetes cluster. The charts and their settings are pulled from a configurable source, in our case GitHub. This allowed us to store all our Kubernetes configuration as code. We wanted better visibility and consistency in our infrastructure and this tool makes that possible. Being able to add &ldquo;GitOps&rdquo; is an added bonus.</p>
<p>ArgoCD is based on a custom resource called an <a href="https://argo-cd.readthedocs.io/en/stable/operator-manual/declarative-setup/#applications">Application</a>. An application represents a single installation of a Helm chart. Its resource includes a source (where to retrieve the chart), a destination (where to install the chart), and any parameters to apply to the chart. You can automate more complicated scenarios with charts containing Applications, resulting in Applications containing Applications. You can also use the <a href="https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/">ApplicationSet</a> resource to generate multiple Application objects. We combine all of these to push out a lot of similar but not exactly the same applications in several environments.</p>
<p>ArgoCD periodically compares the source (code) and destination (cluster state). If there are any differences, the application gets marked as out of sync. ArgoCD can synchronize all the changes in an application automatically or with the press of a button. It also has a nice user interface that shows all the applications, their state, and some other useful information.</p>
<p>If your application can be deployed all at once as a simple Helm chart, ArgoCD can easily do this. For a more complicated deployment process like ours, we used the sync phases and waves feature; we added special annotations in a few our resource definitions to control the order ArgoCD applies their changes. It looks like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">metadata</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">annotations</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">argocd.argoproj.io/hook</span><span class="p">:</span><span class="w"> </span><span class="l">PreSync</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">argocd.argoproj.io/sync-wave</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;5&#34;</span><span class="w">
</span></span></span></code></pre></div><h2 id="how-we-automated-our-deployment-process">How We Automated our Deployment Process</h2>
<p>Our process uses these steps:</p>
<ol>
<li><code>PreSync</code> / <code>-1</code> - Create necessary secrets, service accounts, etc.</li>
<li><code>PreSync</code> / <code>10</code> - Create job: run a script that sets the replica count to 0</li>
<li><code>PreSync</code> / <code>20</code> - Create job: run the schema migrator docker image</li>
<li><code>Sync</code> (default) - Create the deployment, services, and everything else</li>
</ol>
<p>The <code>PreSync</code> / <code>10</code> step ensures the application is stopped before continuing. It checks that the deployment exists so it won&rsquo;t fail on the very first run, then it sets the replica count to 0. The pods get deleted pretty quickly after this change is applied.</p>
<p>The schema migrator job runs next. It can upgrade the schema of an existing database or create a new one if one doesn&rsquo;t already exists. Once it completes, all the rest of the resources are created in the <code>Sync</code> step. The deployment resource sets the new application version and restores the replica count. Pods start getting created, and pretty soon we have a fully working application.</p>
<p>If the schema migrator job fails, Kubernetes will execute any retries as per the job definition. If the job can&rsquo;t complete, the synchronization cycle stops and gets marked as failed in ArgoCD. A human can then make any necessary changes and trigger another synchronization cycle.</p>
<p>We ran this process hundreds of times in our development environment and several more in our production environments. The ArgoCD part of it always worked correctly. Since some of our applications had an installation per tenant, we also ran several in parallel with no issues. We did have a few deployments fail, but they were all caused by infrastructure issues or application bugs. That won&rsquo;t be different from any other Kubernetes system.</p>
<h2 id="branching-strategy">Branching Strategy</h2>
<p>The GitOps approach to managing our environments brought some significant benefits, but it also made it more challenging to test changes to our charts. For example, some application changes would require add or dropping startup parameters in the pod definition. We had to be especially careful that it wouldn&rsquo;t break an environment if the new chart was applied before the application version was updated. It was possible to deal with simple changes like this using conditional blocks in the Helm templates, but it got a lot more messy when we were updating community charts for our logging or monitoring infrastructure, or changing the shared ingress definitions.</p>
<p>To ensure we didn&rsquo;t have any accidents we moved to a branching strategy. We now use three branches:</p>
<ul>
<li>develop - for our development environment. This is where most of our chart changes occur. It&rsquo;s also where we test daily builds of our applications</li>
<li>staging - for our staging environment. We use this to test new charts and applications before a production release</li>
<li>production - for all of our production environments</li>
</ul>
<p>Changes to the charts get tested in the development environment. They then get merged to the staging branch just before a major release. As part of the production release cycle we merge the same changes from the staging branch to the production branch, making sure that only tested changes get deployed.</p>
<p>To ensure there is no configuration drift, we also occasionally merge changes from the staging and production branches back to the development branch.</p>
<p>We did find branching a bit difficult to use, especially for parts of our team that had less experience with Git. Even with this difficulty, we found the added safety worthwhile.</p>
<h2 id="the-good-our-upgrade-process">The Good: Our Upgrade Process</h2>
<p>Our upgrade run list was beautifully simple:</p>
<ol>
<li>Make sure the environment is healthy and all the Application resources are in a healthy state</li>
<li>Merge any changes from the previous branch to the target branch (ex: develop to staging, or staging to production).</li>
<li>For each installation, modify the version (docker image tag) in the appropriate configuration yaml files and merge those.</li>
<li>Find the applications marked as out of sync in ArgoCD and trigger synchronization cycles. Wait for them to finish.</li>
</ol>
<p>Once we started the synchronization cycle, ArgoCD would start applying the changes. A few minutes later the new version would start up and the web services would start responding to requests again.</p>
<h2 id="the-bad-scaling-pods-without-downtime">The Bad: Scaling Pods Without Downtime</h2>
<p>The biggest drawback of this approach was the inability to make minor configuration tweaks to our production system through the code. ArgoCD uses a synchronization cycle to apply changes from the source. This was great when we were changing the version of the application and the schema migrator needed to run, but it wasn&rsquo;t so great when we needed to add a little memory or increase the replica count to keep everything working smoothly.</p>
<p>In these cases we had to make changes to the Kubernetes resources directly, bypassing ArgoCD. This meant the Application resources would be marked as out of sync until we made the same changes in the code. If we forgot this step, the changes would get stomped during the next synchronization cycle.</p>
<p>ArgoCD has a feature to ignore certain state differences in a resource. This is great when you&rsquo;re using autoscaling or other Kubernetes automation features. We couldn&rsquo;t use it though because it also sometimes prevented ArgoCD from applying the changes to raise the replica count from 0 in the <code>Sync</code> phase.</p>
<p>This is only an issue when deploying applications that need downtime while the chart is being applied. Many applications that are designed to run it Kubernetes will keep working throughout this process, and ArgoCD can handle this just fine.</p>
<h2 id="the-ugly-automatic-synchronization">The Ugly: Automatic Synchronization</h2>
<p>ArgoCD is capable of triggering synchronization cycles automatically when it detects changes. This is helpful if you want to ensure your cluster&rsquo;s state is always identical to your committed code, which is the ultimate in a GitOps workflow. The drawback is that it also means a synchronization cycle can be triggered whenever changes are detected. Since our process involves downtime, we didn&rsquo;t want this to happen unintentionally in our production environments.</p>
<p>The other problem we ran into with automatic synchronization was that it made it harder to test minor configuration changes in our development environment. If we added or removed a bit of memory to measure the impact, ArgoCD would quickly reset it back. We could add parameters to allow these things to be configured, but that increased the complexity of the charts and made them harder to read. It also meant we had to remember te remove the same changes again later.</p>
<p>The setting for automatic synchronization is specified on a per-application basis via the resource definition, so you can make this behavior optional for some of your applications. We decided to use manual synchronization in our staging and production environments for everything but the top-level charts. This allowed us to control when changes were applied.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Using ArgoCD for automating the more complicated upgrade process worked well for us, even with its challenges. I would recommend this solution to others.</p>
<p>Another strong reason to use ArgoCD is that it is an excellent tool to use even if you don&rsquo;t need to control the synchronization process. It was a great platform for us to deploy newer Kubernetes-native applications, and it was convenient to use the same tool for everything. It also left us in a position where we could iterate gradually to a simpler deployment process with our legacy applications.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/kubernetes" term="kubernetes" label="kubernetes" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/argocd" term="argocd" label="argocd" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/devops" term="devops" label="devops" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Importance of Alignment]]></title>
            <link href="https://jessemcdowell.ca/2023/06/Importance-of-Alignment/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2021/01/breaking-past-senior-developer/?utm_source=atom_feed" rel="related" type="text/html" title="Breaking Past Senior Developer" />
                <link href="https://jessemcdowell.ca/2023/05/Polyglot-Unconference-2023/?utm_source=atom_feed" rel="related" type="text/html" title="Polyglot Unconference 2023" />
                <link href="https://jessemcdowell.ca/2016/08/infiltrating-an-organization/?utm_source=atom_feed" rel="related" type="text/html" title="Infiltrating an Organization (or: Joining a New Team)" />
                <link href="https://jessemcdowell.ca/2012/03/choosing-priorities/?utm_source=atom_feed" rel="related" type="text/html" title="Choosing Priorities" />
                <link href="https://jessemcdowell.ca/2011/10/sharpening-the-saw/?utm_source=atom_feed" rel="related" type="text/html" title="Sharpening The Saw" />
            
                <id>https://jessemcdowell.ca/2023/06/Importance-of-Alignment/</id>
            
            
            <published>2023-06-12T13:56:44-07:00</published>
            <updated>2023-06-12T13:56:44-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>Unless you work entirely alone, alignment is a big deal. When you are well aligned with your company&rsquo;s goals you are a more valuable employee. When you are well aligned with your manager they can keep you on the right track and be an ally against obstacles. When you are well aligned with your peers you can keep each other focused on the most important work.</p>
<p>On the other hand, when you have poor alignment you can see all sorts of problems. When teams are misaligned they can undermine each other&rsquo;s efforts. When you are misaligned with your manager you can find yourself being over managed or left out to dry when things get rough. When you are not aligned with the goals of your organization you miss opportunities to demonstrate your skills and advance your career.</p>
<p>Alignment is such an important part of being successful in an organization that it&rsquo;s important to understand what it looks like and regularly assess how healthy it is. If it starts to degrade, you should work to fix it, or you will find it gets harder and harder to be successful.</p>
<p>Ensuring good alignment with your manager is the most important. A good manager should help you find work that is rewarding, encourages development, and furthers the company&rsquo;s goals. This is a hard job, and even the best manager in the world can&rsquo;t do this if they don&rsquo;t know what you enjoy and what your goals are. Make sure you are talking regularly with your manager and you have a good relationship. I can&rsquo;t overstate how important it is to have a manager you trust that is helping you grow.</p>
<p>A manager that is honest with you when you&rsquo;re doing poorly is also important. It&rsquo;s impossible to grow without pushing yourself, and when you tackle new challenges it&rsquo;s almost certain that you will make mistakes. If you don&rsquo;t get negative feedback when you need it you can end up wasting a lot of time and energy, and ultimately damage your reputation.</p>
<p>In Vancouver / Canadian culture a lot of managers (myself included) find it difficult to deliver negative feedback, but the alternative is so much worse. A manager that doesn&rsquo;t communicate honestly and candidly may seem like they are supporting you and agree with your direction. This is like wearing a fake seat belt; when things go bad you will have no protection.</p>
<p>If you have a manager that can&rsquo;t give you guidance because they don&rsquo;t understand what you&rsquo;re doing, you need to make sure they at least agree with the choices you are making. Be very careful here though, as some managers will say they agree with you without really meaning it, especially when the choices are complicated. You have to have make sure they really understand what you are asking and the implications of it. If they aren&rsquo;t invested in the decisions, they may not back them up when you need it.</p>
<p>Another challenging situation is when your manager is too far removed from your work to have an opinion about it. In these cases you have to depend on the other kinds of alignment and do your best to manage and sell yourself. This becomes more likely the higher up you go in an organization.</p>
<p>Good alignment with your organization&rsquo;s goals is also important, but this is impossible without a culture that supports it. People at all levels of an organization have to make decisions every day, communicate with customers and partners, and are constantly representing the brand. You and everyone else should know what the organization is trying to achieve and how you are personally contributing to it. When everyone isn&rsquo;t working in the same direction it is easier to have miscommunications and disputes between teams.</p>
<p>The best teams I&rsquo;ve worked on update everyone with recurring company or department-level meetings. Even if some of the information isn&rsquo;t immediately or personally relevant, it all still soaks into the subconscious. Even just the exercise of gathering and presenting to the broader audience is valuable; it forces team leaders to understand the value they provide and makes sure they are measuring and delivering it.</p>
<p>Some companies don&rsquo;t see the value of keeping everyone informed. If that&rsquo;s the case where you work, you will have to take matters into your own hands. At the very least, make sure you read the company emails. Ask questions when you can, and try to always understand the direction your company is going.</p>
<p>Even when you have good alignment with your manager and a clear understanding of the organization&rsquo;s goals, you may sometimes find yourself under a manager that isn&rsquo;t well aligned themselves. Even if you are a perfect employee, the success of your team will reflect on you, and the team won&rsquo;t be seen as successful if it isn&rsquo;t helping the organization. The teams that are doing the best from this point of view tend to have the best bonuses and the best growth opportunities.</p>
<p>It&rsquo;s not always possible to control the team you work on, sometimes the only way to improve things is to leave your company entirely. Of course there are lots of factors that go into a decision like that&hellip; but if you don&rsquo;t feel that you, your team, and your company are going in the same direction, or if you can&rsquo;t tell, you should at least be aware that it could be hurting your opportunities to grow and advance.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/people-management" term="people-management" label="people management" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/career" term="career" label="career" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Polyglot Unconference 2023]]></title>
            <link href="https://jessemcdowell.ca/2023/05/Polyglot-Unconference-2023/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2011/10/sharpening-the-saw/?utm_source=atom_feed" rel="related" type="text/html" title="Sharpening The Saw" />
                <link href="https://jessemcdowell.ca/2021/01/breaking-past-senior-developer/?utm_source=atom_feed" rel="related" type="text/html" title="Breaking Past Senior Developer" />
                <link href="https://jessemcdowell.ca/2016/08/infiltrating-an-organization/?utm_source=atom_feed" rel="related" type="text/html" title="Infiltrating an Organization (or: Joining a New Team)" />
                <link href="https://jessemcdowell.ca/2012/03/choosing-priorities/?utm_source=atom_feed" rel="related" type="text/html" title="Choosing Priorities" />
                <link href="https://jessemcdowell.ca/2011/10/managing-priorities-outside-of-work/?utm_source=atom_feed" rel="related" type="text/html" title="Managing Priorities Outside of Work" />
            
                <id>https://jessemcdowell.ca/2023/05/Polyglot-Unconference-2023/</id>
            
            
            <published>2023-05-31T22:22:23-07:00</published>
            <updated>2023-05-31T22:22:23-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>I recently had the pleasure of attending the 2023 Polyglot Unconference in Vancouver, put on by the <a href="https://www.polyglotsoftware.com/">Polyglot Software Association</a>. I&rsquo;ve been attending these for years. It is my favourite local conference.</p>
<p>An <a href="https://en.wikipedia.org/wiki/Unconference">unconference</a>, sometimes called an open spaces conference, is a participant-driven event where attendees choose the topics of discussion and provide the content themselves. They are meant to be open and inviting, and build interpersonal relationships. This year was no exception.</p>
<p>The event started with a brief introduction and some ground rules, and then attendees began pitching sessions. Anyone could pitch a session, and then organize it however they wanted. This year I pitched a session about how to start a software company. I got on stage, gave my name, and explained what I wanted to talk about.</p>
<p>After all the sessions were pitched, attendees voted on the sessions they want to attend. Organizers then assigned them to rooms. All the sessions can usually be accommodated, so the voting is only used to make sure that the number of interested attendees can fit in the rooms they are assigned to.</p>
<p>For my session, I used a <a href="https://en.wikipedia.org/wiki/Fishbowl_(conversation)">fishbowl format</a>. I put 5 chairs on the stage. Only people in chairs could ask or answer questions, but anyone in the audience could take a chair at any time. The group on the stage had to ensure that one chair was always empty. What you end up with is an intimated discussion with an audience.</p>
<p>A few experienced founders attended my session, and quite a few people who wanted to or had already started software companies were present too. I asked my questions, other people asked their questions, and we got a lot of great answers. I took 3 pages of notes that will absolutely be helpful in my endeavors.</p>
<p>The best part of this event is learning about what other companies in town are doing, what&rsquo;s working for them, and where they&rsquo;ve had problems. Traditional software conferences tend to have more vendor-sponsored presentations where everything is a sales pitch. These are valuable too, but the unconference is a better way to get a balanced opinion.</p>
<p>I can&rsquo;t wait to attend again next year.
`</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/career" term="career" label="career" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/learning" term="learning" label="learning" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/community" term="community" label="community" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Sustainable Errors]]></title>
            <link href="https://jessemcdowell.ca/2023/05/Sustainable-Errors/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/04/How-to-Fix-a-Bug/?utm_source=atom_feed" rel="related" type="text/html" title="How to Fix a Bug" />
                <link href="https://jessemcdowell.ca/2023/03/How-to-Report-a-Bug/?utm_source=atom_feed" rel="related" type="text/html" title="How to Report a Bug" />
                <link href="https://jessemcdowell.ca/2011/01/when-to-add-an-orm-tool/?utm_source=atom_feed" rel="related" type="text/html" title="When to Add an ORM Tool" />
                <link href="https://jessemcdowell.ca/2023/05/Is-the-Bug-Fun/?utm_source=atom_feed" rel="related" type="text/html" title="Is the Bug Fun?" />
                <link href="https://jessemcdowell.ca/2016/08/infiltrating-an-organization/?utm_source=atom_feed" rel="related" type="text/html" title="Infiltrating an Organization (or: Joining a New Team)" />
            
                <id>https://jessemcdowell.ca/2023/05/Sustainable-Errors/</id>
            
            
            <published>2023-05-29T09:57:05-07:00</published>
            <updated>2023-05-29T09:57:05-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>Making a program work for the happy path is not always easy, but given enough time I believe pretty much anyone could do it. When a professional takes on the task however they will make it work for more than just the happy path, and do it with code that is easy to debug, and easy for others to understand and change. Since so much of what we end up dealing with are exceptional flows, we need a concise way to deal with them. Fortunately we have the aptly named exception pattern.</p>
<p>When the pattern is used well it is almost invisible, and yet we should be thinking about it all the time. Sample code and simple apps often show exception handling as rote boilerplate that writes out stack traces and swallows errors. This is not a good example to be setting.</p>
<p>Most modern languages have an exception type and a throw statement. I&rsquo;ll be using C#/.Net terminology for this post, but the same or similar terms and patterns exist in Java, JavaScript, TypeScript, Python, and many other languages.</p>
<p>Most scripting languages (shell script, Windows batch, and sometimes PowerShell) and some low level languages like C don&rsquo;t have exceptions. In these cases you have to check return codes every time you call a function or an external application, and it sucks. You don&rsquo;t have to look far to find scripts filled primarily with error handling code. For the rest of us, there is something much easier and better.</p>
<div class="table-of-contents">
    <nav id="TableOfContents">
  <ul>
    <li><a href="#the-exception-pattern">The Exception Pattern</a></li>
    <li><a href="#global-error-handling">Global Error Handling</a></li>
    <li><a href="#throwing-good-exceptions">Throwing Good Exceptions</a></li>
    <li><a href="#what-about-performance">What About Performance?</a></li>
    <li><a href="#what-about-leaking-security-sensitive-information">What About Leaking Security Sensitive Information?</a></li>
  </ul>
</nav>
</div>

<h2 id="the-exception-pattern">The Exception Pattern</h2>
<p>Here is an example of the exception pattern being used well:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="cl"><span class="kd">public</span> <span class="kt">string</span> <span class="n">GetMagicString</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">using</span> <span class="nn">var</span> <span class="n">reader</span> <span class="p">=</span> <span class="k">new</span> <span class="n">StreamReader</span><span class="p">(</span><span class="s">&#34;magic.txt&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="kt">var</span> <span class="n">magicString</span> <span class="p">=</span> <span class="n">reader</span><span class="p">.</span><span class="n">ReadLine</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="p">(</span><span class="n">String</span><span class="p">.</span><span class="n">IsNullOrEmpty</span><span class="p">(</span><span class="n">magicString</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">        <span class="k">throw</span> <span class="k">new</span> <span class="n">InvalidOperationException</span><span class="p">(</span><span class="s">&#34;magic.txt did not contain any text&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">magicString</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>If you don&rsquo;t already know how exceptions work you can read about them in <a href="https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/exceptions/">Microsoft&rsquo;s documentation</a>.</p>
<p>You may have noticed that no exceptions are being handled in the function, which is the point. This depends on the built-in exception that will be thrown being good enough. This is because I expect the file to be created by the installer in normal deployments. I could wrap this in a try/catch block and do something, but what would I do? If you were about to say &ldquo;return null&rdquo; then you lose 10 points. You should always favour exposing problems quickly and clearly rather than ignoring them or pushing them down the line.</p>
<p>If I couldn&rsquo;t expect the file to exist, if say <code>magic.txt</code> was a special override file that was only created in certain circumstances, I would write the function like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="cl"><span class="kd">public</span> <span class="kt">string</span> <span class="n">GetMagicString</span><span class="p">()</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">using</span> <span class="nn">var</span> <span class="n">reader</span> <span class="p">=</span> <span class="k">new</span> <span class="n">StreamReader</span><span class="p">(</span><span class="s">&#34;magic.txt&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="kt">var</span> <span class="n">magicString</span> <span class="p">=</span> <span class="n">reader</span><span class="p">.</span><span class="n">ReadLine</span><span class="p">();</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="n">String</span><span class="p">.</span><span class="n">IsNullOrEmpty</span><span class="p">(</span><span class="n">magicString</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">            <span class="k">throw</span> <span class="k">new</span> <span class="n">InvalidOperationException</span><span class="p">(</span><span class="s">&#34;magic.txt did not contain any text&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">magicString</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">catch</span> <span class="p">(</span><span class="n">FileNotFoundException</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">DefaultMagicString</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Notice that I am only catching the specific file not found exception. I could have caught any exception, but that has its own dangers. Another possible exception is the <code>UnauthorizedAccessException</code>. If that happens, I&rsquo;d rather notify the user than silently ignore the file.</p>
<p>I added a bit of guard code that can throw the <code>InvalidOperationException</code>. Even if it&rsquo;s an unlikely problem, I prefer to surface problems like this earlier. They typically save time later when debugging. I&rsquo;m keeping this code in the second example even while I have access to a default value because I think an empty file is more likely a mistake than intentional. It could be intentional for some use case I don&rsquo;t know about, but it is always easier to ease restrictions than it is to add new ones in a deployed application, so I&rsquo;d rather be more strict early.</p>
<p>The <code>using var</code> declaration is a relatively new C# feature that generates an implicit using block. It causes <code>reader</code> to be disposed before the function exits, be it successfully or a because of a thrown exception. Because the stream is opening and potentially locking a file, we want to make sure it is cleaned up quickly. Cleaning up resources in all cases is something we should always be mindful of as well, but in most modern languages you can use features like <code>using</code> to make this task similarly invisible.</p>
<h2 id="global-error-handling">Global Error Handling</h2>
<p>So where do exceptions get handled? Generally you should have an error handler somewhere, as high up the stack as is reasonable. You only have to write it once in one place, and reducing repetition has a lot of advantages. Your error handler can do any fancy stuff you want it to, and you can change the behavior of your error handling without making sweeping changes across your application.</p>
<p>Most of our applications are iterating on some atomic unit of work. Web servers iterate over web requests. Background processors iterate over messages in a queue or scheduled tasks. It makes sense to implement error handlers at the level where these are invoked, especially if there is a good chance that another similar unit of work could succeed.</p>
<p>In a web api the error handler can usually be added as a wrapper around every request through some hook in the framework. Common implementations will log the exception and return a 500-level status code. Fancier implementations will return more nuanced codes depending on the type of the exception (like return a <code>400</code> for <code>ArgumentException</code> or anything derived from it). Your web framework of choice probably already does this for you.</p>
<p>Some errors, however, aren&rsquo;t recoverable. For example, without complicated retry logic, many applications will be in a bad state if their startup logic fails somehow. You might have one global handler for all your startup code, but its behavior should probably be to output some useful diagnostic information and exit. There may be other errors that indicate poor application health beyond startup, but this is often pretty challenging to deal with reliably.</p>
<p>.Net also has a few special exceptions that can&rsquo;t be caught normally. Most exceptions mean that the operation you attempted failed, but these uncatchables indicate that the runtime environment could now be in a bad state. The dreaded <code>StackOverflowException</code> is an example of this. In these cases exiting and getting restarted is the only safe thing to do. The framework is going to do that regardless of your error handling so you don&rsquo;t need to add special logic for it.</p>
<h2 id="throwing-good-exceptions">Throwing Good Exceptions</h2>
<p>The best thing to do when you detect unexpected conditions is often throwing an exception. The exception message should clearly describe what the problem is. Remember that you or one of your peers will be reading this message about 6 months from now when a test fails or a bug gets reported. Sometimes I like to include advice about fixing the problem in the exception, but use this sparingly; it&rsquo;s much less helpful when the advice has become outdated and sends users on wild goose chases.</p>
<p>It sometimes makes sense to create a new exception type for your error cases. I can&rsquo;t remember where I once read that exceptions should have the same level of abstraction as the interface that throws them&hellip; It&rsquo;s not terrible advice, but I don&rsquo;t think it&rsquo;s worth the trouble for most cases. If you&rsquo;re building a framework it might make sense. If you&rsquo;re checking for an empty file after reading its contents it may not.</p>
<p>One case where I do create custom exceptions is when I&rsquo;m writing a unit test. For really trivial stuff (like argument null) you probably don&rsquo;t need a unit test, but for anything that should be tested, use a custom exception. If you&rsquo;re asserting on any exception being thrown your test could be marked as passing when it should be failing. If you derive your custom exception from a built-in or another more common exception, your callers won&rsquo;t normally need to handle your specific type, but they could if they wanted to.</p>
<p>If you are not creating your own type, try to use a specific and appropriate exception type, either built-in or custom. <code>InvalidOperationException</code> and <code>ArgumentException</code> give better hints the developer in the future than a simple <code>Exception</code>.</p>
<p>It&rsquo;s also a good practice to include the inner exception whenever you generate a new exception in response to a failure lower in the stack. These details can be very helpful if the new exception is ever thrown unexpectedly.</p>
<h2 id="what-about-performance">What About Performance?</h2>
<p>One common argument against throwing exceptions is that it causes a performance hit. It&rsquo;s true, it does takes time to capture the stack trace, and allocating a new object on the heap isn&rsquo;t free. It depends on your circumstance of course, but I think in 99% of scenarios this cost is too miniscule to matter, and certainly far cheaper than the wasted time of developers who can&rsquo;t find bugs.</p>
<p>If your exceptional case is something you expect to hit often inside a tight loop though, it may actually matter. The <code>TryFunction</code> pattern is the common alternative in .Net. The drawback is that because your return value is typically boolean you can&rsquo;t easily add new failure cases as safely as you can with exceptions. You could include more information in another output parameter, but that can also impact your callers whenever the list changes. It also means every caller needs to check the function response, or even worse, they can forget to and the app can continue running in a weird state without realizing the call failed.</p>
<h2 id="what-about-leaking-security-sensitive-information">What About Leaking Security Sensitive Information?</h2>
<p>Another argument I&rsquo;ve heard against good error handling is to prevent leaking information to attackers, but I think this is mostly bad advice too. For any software that is distributed, a malicious user can easily find tools to look inside and see how it&rsquo;s wired. For server-side code you should at least share the error details with yourself in your own log files. If you need to withhold information from potential hackers then do that in your global error handler.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/design" term="design" label="design" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/c" term="c" label="c#" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/.net" term=".net" label=".net" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/quality" term="quality" label="quality" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/practices" term="practices" label="practices" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Is the Bug Fun?]]></title>
            <link href="https://jessemcdowell.ca/2023/05/Is-the-Bug-Fun/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/04/How-to-Fix-a-Bug/?utm_source=atom_feed" rel="related" type="text/html" title="How to Fix a Bug" />
                <link href="https://jessemcdowell.ca/2023/03/How-to-Report-a-Bug/?utm_source=atom_feed" rel="related" type="text/html" title="How to Report a Bug" />
                <link href="https://jessemcdowell.ca/2013/06/reading-server-graphs-connected-users/?utm_source=atom_feed" rel="related" type="text/html" title="Reading Server Graphs: Connected Users" />
                <link href="https://jessemcdowell.ca/2023/05/Case-of-the-Disappearing-Users/?utm_source=atom_feed" rel="related" type="text/html" title="Case of the Disappearing Users" />
                <link href="https://jessemcdowell.ca/2012/04/installutil-and-badimageformatexception/?utm_source=atom_feed" rel="related" type="text/html" title="InstallUtil and BadImageFormatException - Facepalm" />
            
                <id>https://jessemcdowell.ca/2023/05/Is-the-Bug-Fun/</id>
            
            
            <published>2023-05-15T10:47:52-07:00</published>
            <updated>2023-05-15T10:47:52-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>There are many things about producing video games that are surprising, but one of the weirdest has to be the approach to bugs. Like any piece of software, bugs are found through testing or user reports, triaged, then assigned to developers. Unlike normal business software they also ask the question, &ldquo;is the bug fun?&rdquo;</p>
<p>There are plenty of unintended features (bugs) in games that became beloved. Attack combos were an accident in Street Fighter II, but they became so popular that they are a part of basically every fighting game now. Rocket jumps are another example. The internet is full of examples.</p>
<p>Sometimes very glitchy games can be fun too, especially for a certain audience. Speed runners sometimes use glitches to lower their times. People love games for all sorts of reasons beyond just beating them and getting high scores. At the end of the day the goal of a game is to entertain more than be correct.</p>
<p>The next time a bug comes across your desk, maybe ask yourself if fixing it would make your app less fun.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/debugging" term="debugging" label="debugging" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/bugs" term="bugs" label="bugs" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/video-games" term="video-games" label="video games" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/quality" term="quality" label="quality" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Case of the Disappearing Users]]></title>
            <link href="https://jessemcdowell.ca/2023/05/Case-of-the-Disappearing-Users/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/04/How-to-Fix-a-Bug/?utm_source=atom_feed" rel="related" type="text/html" title="How to Fix a Bug" />
                <link href="https://jessemcdowell.ca/2023/03/How-to-Report-a-Bug/?utm_source=atom_feed" rel="related" type="text/html" title="How to Report a Bug" />
                <link href="https://jessemcdowell.ca/2013/06/reading-server-graphs-connected-users/?utm_source=atom_feed" rel="related" type="text/html" title="Reading Server Graphs: Connected Users" />
                <link href="https://jessemcdowell.ca/2012/04/installutil-and-badimageformatexception/?utm_source=atom_feed" rel="related" type="text/html" title="InstallUtil and BadImageFormatException - Facepalm" />
                <link href="https://jessemcdowell.ca/2011/08/doubling-data-for-performance-testing/?utm_source=atom_feed" rel="related" type="text/html" title="Doubling Data for Performance Testing" />
            
                <id>https://jessemcdowell.ca/2023/05/Case-of-the-Disappearing-Users/</id>
            
            
            <published>2023-05-01T08:26:25-07:00</published>
            <updated>2023-05-01T08:26:25-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>Many years ago I worked on a program that had a serious problem: the users in one customer&rsquo;s system were getting deleted periodically. When a user was deleted, any data linked with them was also deleted. We could restore the data from backups, but it was a difficult process, and having a system that loses data wasn&rsquo;t great for our reputation, so we wanted to resolve it quickly. Our VP of development tried to find the issue first, but after a day without any progress he assigned the issue to me.</p>
<p>I asked some questions and tried my usual tricks. There wasn&rsquo;t an error message or anything helpful in the logs. There didn&rsquo;t seem to be any obvious place to start, so I did something crazy: I searched the entire codebase for the world <code>DELETE</code>.</p>
<p>It seemed likely to me that the user was getting deleted by the application, so there had to be a delete statement somewhere that was the cause.</p>
<p>One of the biases I&rsquo;ve noticed in myself is that I tend to drastically over-estimate the time it will take to perform mundane tasks. In reality, even if there were a few thousand delete statements in the code (and there weren&rsquo;t), it would still be possible to review them all in an afternoon. Using a good IDE that has a preview for search results and a bit of care with search terms it was possible to get through them even faster.</p>
<p>I found the offending delete statement. The bug was actually a feature!</p>
<p>The customer had re-installed the software at some point and couldn&rsquo;t find their licence key. The software wouldn&rsquo;t work without a licence unless it was put into &ldquo;demonstration mode.&rdquo; Demonstration mode caused users to be deleted after the trial period ended.</p>
<p>We issued them a new licence key and changed the demonstration mode feature so it wouldn&rsquo;t be so destructive if it ever got turned on in the future.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/debugging" term="debugging" label="debugging" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/war-stories" term="war-stories" label="war stories" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[How to Fix a Bug]]></title>
            <link href="https://jessemcdowell.ca/2023/04/How-to-Fix-a-Bug/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2023/03/How-to-Report-a-Bug/?utm_source=atom_feed" rel="related" type="text/html" title="How to Report a Bug" />
                <link href="https://jessemcdowell.ca/2013/06/reading-server-graphs-connected-users/?utm_source=atom_feed" rel="related" type="text/html" title="Reading Server Graphs: Connected Users" />
                <link href="https://jessemcdowell.ca/2016/08/infiltrating-an-organization/?utm_source=atom_feed" rel="related" type="text/html" title="Infiltrating an Organization (or: Joining a New Team)" />
                <link href="https://jessemcdowell.ca/2012/04/installutil-and-badimageformatexception/?utm_source=atom_feed" rel="related" type="text/html" title="InstallUtil and BadImageFormatException - Facepalm" />
                <link href="https://jessemcdowell.ca/2011/10/sharpening-the-saw/?utm_source=atom_feed" rel="related" type="text/html" title="Sharpening The Saw" />
            
                <id>https://jessemcdowell.ca/2023/04/How-to-Fix-a-Bug/</id>
            
            
            <published>2023-04-24T08:50:00-07:00</published>
            <updated>2023-04-24T08:50:00-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>Building applications can be tricky, and it&rsquo;s inevitable that mistakes will be made. As a result, we programmers spend a lot of time fixing bugs. Sometimes they are easy, but sometimes they can be pretty tough to figure out.</p>
<p>I&rsquo;ve fixed a lot of bugs in my career, and to be honest with you, I usually enjoy the process. These days I am typically assigned the super urgent bugs that nobody else can figure out, and I kind of like it that way. I don&rsquo;t get me wrong, I don&rsquo;t like the bugs being there, but I enjoy being helpful and figuring out tough problems. I also think my successes have helped improve my reputation which is always a good thing.</p>
<p>This can take a lot of hard work, but I&rsquo;ve made the job considerably easier through a set of tricks and a process I&rsquo;ve developed over my career. I never thought of these as particularly special, or as a secret weapon, but I realize now that they aren&rsquo;t really taught. They are important though, so I wrote them down.</p>
<p>This is the process I generally follow when I&rsquo;m approaching a bug. I will obviously not go through a formal process for the super simple and obvious ones, but for the toughest nuts, all the steps here will help me get it over the finish line.</p>
<div class="table-of-contents">
    <nav id="TableOfContents">
  <ul>
    <li><a href="#step-1-read-the-bug-report">Step 1: Read the Bug Report</a></li>
    <li><a href="#step-2-reproduce-the-problem">Step 2: Reproduce the Problem</a></li>
    <li><a href="#step-3-find-the-problem">Step 3: Find the Problem</a></li>
    <li><a href="#step-4-test-my-theory">Step 4: Test My Theory</a></li>
    <li><a href="#step-5-fix-the-problem">Step 5: Fix the Problem</a></li>
    <li><a href="#step-6-look-for-similar-bugs">Step 6: Look for Similar Bugs</a></li>
    <li><a href="#step-7-test-my-fix">Step 7: Test My Fix</a></li>
    <li><a href="#step-8-understand-the-bugs-impact">Step 8: Understand the Bug&rsquo;s Impact</a></li>
    <li><a href="#step-9-bug-retrospective-post-mortem">Step 9: Bug Retrospective (Post-mortem)</a></li>
  </ul>
</nav>
</div>

<h2 id="step-1-read-the-bug-report">Step 1: Read the Bug Report</h2>
<p>When a bug report comes across my desk, I read it and make sure that I understand it. Fixing a bug properly takes time and carries its own risks, so I want to be sure I&rsquo;m fixing the right bug.</p>
<p>If I can&rsquo;t understand the bug report, this is also a good place to stop and request more information. I wrote <a href="/2023/03/How-to-Report-a-Bug/">another post about writing good bug reports</a>.</p>
<h2 id="step-2-reproduce-the-problem">Step 2: Reproduce the Problem</h2>
<p>The most important step to fixing a bug is reproducing it. Even if the problem seems obvious, even if I am feeling particularly lazy, I force myself to do it anyway. If I can&rsquo;t, I have no way to test my fix later. It also means I won&rsquo;t be able to use a debugger to figure out what is happening when it fails.</p>
<p>Sometimes it&rsquo;s possible to write an automated test before finding the problem, and if I can, I will. It makes debugging and testing considerably faster. In complicated software this can even be faster than starting up all the required pieces to test an issue manually.</p>
<p>It can be difficult to reproduce an intermittent issue, but I still put in the effort for the same reasons. I&rsquo;ve sometimes had luck wrapping unit tests in a for loop, but it won&rsquo;t help if the causes are environmental.</p>
<p>There have been rare occasions when I&rsquo;ve had to attempt fixing a bug before I can reproduce it, but I will only let this happen in extreme circumstances. When doing this kind of thing I also make sure to communicate it clearly with everyone involved.</p>
<h2 id="step-3-find-the-problem">Step 3: Find the Problem</h2>
<p>This can be the most infuriating part of fixing bugs, but it is essential. There are a bunch of techniques that can work here. For this post I&rsquo;ll stick to the tricks I use the most.</p>
<p>The first thing I do is read the error details again. If there is an error message in the bug report it can carry a lot of clues. It&rsquo;s tempting to skim over it, and I have made this mistake plenty of times myself. Now I make sure I not only read the error message, but also understand it. This has saved me a lot of time. System errors can be especially helpful since they tend to be specific. Stack traces are also incredibly valuable. If I don&rsquo;t understand what an error code means I look it up.</p>
<p>In the absence of a stack trace, I like to narrow down where the problem is occurring. I start by visualizing all the steps through the system that are involved in the malfunctioning operation, then I choose some point in the middle. Using my trusty debugger, I test if it&rsquo;s failed at that point. I continue along in a binary search pattern until I have narrowed down the problem to a specific spot.</p>
<p>For example: if I&rsquo;m fixing a bug in a web application where a user&rsquo;s name is getting saved incorrectly I can start by checking the web request from my browser&rsquo;s developer tool. If the request is wrong, I know the bug is on the client side. If the request is correct, the bug will be somewhere in the server. Assuming it&rsquo;s in the server, I might set a breakpoint between my business layer and repository layer to narrow it down further. Continuing in this way I can find the exact location of the bug quickly and reliably.</p>
<p>It works for more than just software problems too. I&rsquo;ve used the same approach to diagnose load balancer problems, problems with components in Kubernetes, computer hardware, even electrical wiring.</p>
<p>In some cases it&rsquo;s easier to figure out what change introduced the bug instead. I will use the same kind of binary search pattern but checking out commits between releases. A quick build time and a unit test makes this a lot less painful. I&rsquo;ve never had a bug where it was a good fit, but you can also try the <code>git bisect</code> command to automate the process fully.</p>
<p>If I ever get stuck in my investigation, I go back to the beginning and re-check all my assumptions. Did I read the bug right? Was the process in a healthy state when it occurred? Am I looking at the right version of the code? Did the feature ever work? Did I misclassify a success or failure when I did the binary search? Even if all my assumptions were correct, going through the problem again can sometimes spark new theories for investigation. This is also where I start when someone else asks me for help with their bugs.</p>
<p>If all else fails, or even if it hasn&rsquo;t, searching on the internet can help. This is especially true for third party components or services. Be careful though, I am finding internet resources to be increasingly less helpful but your results may vary. Even though I&rsquo;ve wasted a lot of time following red herrings from random internet threads, this is still sometimes the best option available.</p>
<h2 id="step-4-test-my-theory">Step 4: Test My Theory</h2>
<p>I find it easy to jump to conclusions when I&rsquo;m debugging, but my experience has taught me to approach bugs with a scientific kind of skepticism. Once I am pretty sure I know what the problem is, if it&rsquo;s not a trivial change, I like to isolate it and prove to myself that I understand it.</p>
<p>I will write an automated test that reproduces the problem if at all possible. It might seem quicker to fix the bug first, but starting with the test will make the process simpler. Writing a failing test proves that you understand the bug, and it also proves that the test will fail if you don&rsquo;t successfully fix it.</p>
<p>If you can&rsquo;t write a test because that isn&rsquo;t your team&rsquo;s practice then you have my sympathies. On the bright side, you&rsquo;ll be getting a lot more experience fixing bugs!</p>
<h2 id="step-5-fix-the-problem">Step 5: Fix the Problem</h2>
<p>I will remove some bad code and / or put some more good code in.</p>
<h2 id="step-6-look-for-similar-bugs">Step 6: Look for Similar Bugs</h2>
<p>Sometimes a bug indicates a pattern of bugs. Before I fling my fix back out into the world I like to do a little research to see if the same mistake has been made in other places.</p>
<p>For example: if I was fixing a bug caused by a query operator that isn&rsquo;t supported by an older database server version, I can do a quick search to see if the operator was used anywhere else. Since I&rsquo;ve already figured out how to fix it, I can fix them all at the same time and eliminate a whole bunch of bugs.</p>
<h2 id="step-7-test-my-fix">Step 7: Test My Fix</h2>
<p>Since I almost always have automated tests, I can check if I&rsquo;ve fixed the problem pretty easily.</p>
<p>I&rsquo;ll usually do a manual test as well to make sure I really have fixed the issue. Sometimes a bug has more than one cause, or is repeated in more than one place. To be honest, I often find this step tedious, so I have to remind myself why it&rsquo;s important. A lot of time can get wasted if there was some aspect I missed. Also, if I&rsquo;m going to put my name on a fix, I want people to be able to depend on it actually being fixed.</p>
<h2 id="step-8-understand-the-bugs-impact">Step 8: Understand the Bug&rsquo;s Impact</h2>
<p>It depends on the bug, but if there is some damage left behind, I make sure to consider its impact. Sometimes this takes a bit of experimentation, but it is important. Sometimes a cleanup script is necessary, or sometimes manual steps can be provided to correct the issue. At the very least I want to make sure I can tell my stakeholders what the impact was.</p>
<h2 id="step-9-bug-retrospective-post-mortem">Step 9: Bug Retrospective (Post-mortem)</h2>
<p>Once in a while I take a bit of time to reflect on the bugs I&rsquo;ve encountered. How did the bug escape in the first place? Is it likely that similar bugs will be introduced again? Can I introduce tools or change processes to make this class of bug less likely to occur?</p>
<p>Some organizations have a formal post mortem process for impactful issues. This is a great way to ensure a team is learning from its mistakes. I have introduced this process in a few of my teams and highly recommend it.</p>
<p>Even for bugs with less impact it can be worth spending a bit of time thinking about this. It&rsquo;s not always feasible to prevent some types of bugs, but as craftspeople we should be trying!</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/debugging" term="debugging" label="debugging" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/bugs" term="bugs" label="bugs" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/quality" term="quality" label="quality" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/practices" term="practices" label="practices" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[How to Report a Bug]]></title>
            <link href="https://jessemcdowell.ca/2023/03/How-to-Report-a-Bug/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2013/06/reading-server-graphs-connected-users/?utm_source=atom_feed" rel="related" type="text/html" title="Reading Server Graphs: Connected Users" />
                <link href="https://jessemcdowell.ca/2016/08/infiltrating-an-organization/?utm_source=atom_feed" rel="related" type="text/html" title="Infiltrating an Organization (or: Joining a New Team)" />
                <link href="https://jessemcdowell.ca/2012/04/installutil-and-badimageformatexception/?utm_source=atom_feed" rel="related" type="text/html" title="InstallUtil and BadImageFormatException - Facepalm" />
                <link href="https://jessemcdowell.ca/2011/10/sharpening-the-saw/?utm_source=atom_feed" rel="related" type="text/html" title="Sharpening The Saw" />
                <link href="https://jessemcdowell.ca/2011/08/doubling-data-for-performance-testing/?utm_source=atom_feed" rel="related" type="text/html" title="Doubling Data for Performance Testing" />
            
                <id>https://jessemcdowell.ca/2023/03/How-to-Report-a-Bug/</id>
            
            
            <published>2023-03-10T21:40:00-08:00</published>
            <updated>2023-03-11T18:03:00-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>Nobody likes bugs, least of all programmers. No matter how hard we try to catch them early, some will always escape into circulation. Until computers are smart enough to do what we meant instead of what we said, users are going to keep finding bugs, and we&rsquo;re going to keep fixing them.</p>
<p>Before a bug is fixed, it needs to be reported. Unfortunately it&rsquo;s not uncommon to receive incomplete reports. We can spend a lot of time hunting and making guesses, and sometimes that&rsquo;s enough, but if we can&rsquo;t figure out the problem it&rsquo;s pretty hard to fix it. This can be especially unfortunate when the stakes are high, and oddly, this is when it also seems to be the most common.</p>
<p>So what does a bad bug report look like?</p>
<blockquote>
<p>The website crashed.</p></blockquote>
<p>I get this kind of report now and then, even from experienced members of our support teams. I almost always need to send it back to get more information.</p>
<p>If I got this report instead, I wouldn&rsquo;t (usually) need more information:</p>
<blockquote>
<p>When I open the store website I get a 502 Bad Gateway error.</p></blockquote>
<p>When I&rsquo;m trying to understand a bug, I need to know four things:</p>
<ul>
<li>How did you encounter the bug? (Steps to Reproduce)</li>
<li>What happened? (Actual)</li>
<li>What should have happened? (Expected)</li>
<li>Where did you encounter the bug? (Location)</li>
</ul>
<p>Here is another example of a bug that would be hard to figure out:</p>
<blockquote>
<p>I can&rsquo;t delete a user.</p></blockquote>
<p>It only answers one of the questions (Expected). I might be able to guess what the user tried to do (Steps to Reproduce), but there could be multiple ways to do this. It doesn&rsquo;t tell me what application was being used (Location) or what happened (Actual). Was there an error message? Did the application close unexpectedly? Did the computer explode in a ball of fire? I can help with most of these, but they all get approached differently.</p>
<p>If you want a gold star, this is a more formal way to report the same bug, and the format I typically use myself:</p>
<blockquote>
<p>Steps to Reproduce:
1. Go to the user list
2. Click a user
3. Click delete
* Receive successfully deleted notification
4. Return to user list
Actual:
* User appears in the list
* When I click on the user again, I get the error &ldquo;User not found&rdquo;
Expected:
* User does not appear in the list
Location:
* Admin site: <code>https://{someserver}/admin/users</code></p></blockquote>
<div class="table-of-contents">
    <nav id="TableOfContents">
  <ul>
    <li><a href="#how-did-you-encounter-the-bug-steps-to-reproduce">How did you encounter the bug? (Steps to Reproduce)</a></li>
    <li><a href="#what-happened-actual">What happened? (Actual)</a></li>
    <li><a href="#what-should-have-happened-expected">What should have happened? (Expected)</a></li>
    <li><a href="#where-did-you-encounter-the-bug-location">Where did you encounter the bug? (Location)</a></li>
  </ul>
</nav>
</div>

<h2 id="how-did-you-encounter-the-bug-steps-to-reproduce">How did you encounter the bug? (Steps to Reproduce)</h2>
<p>The first step to fixing a bug is trying to reproduce it. Exact steps can help a lot with this. Sometimes it isn&rsquo;t enough, but it&rsquo;s even more helpful when there are other factors involved.</p>
<h2 id="what-happened-actual">What happened? (Actual)</h2>
<p>A detailed, unemotional description of what happened is the most important part of a bug report. If there in an error message, give us the exact text. It may not make sense to you, but this can tell us a lot about what is going on. If possible, copy and paste the exact text of the error. A screenshot can also be really helpful.</p>
<p>Expressions like &ldquo;crashed&rdquo; or &ldquo;failed&rdquo; or &ldquo;doesn&rsquo;t work&rdquo; sound good, but they don&rsquo;t tell us what happened. You don&rsquo;t need to use technical words if you don&rsquo;t know them, but you should avoid interpreting the information. For example &ldquo;an error message appeared that said &hellip;&rdquo; is much more helpful than &ldquo;it failed&rdquo;.</p>
<h2 id="what-should-have-happened-expected">What should have happened? (Expected)</h2>
<p>Sometimes this is obvious. Generally when someone reports an error message we understand they expect not to get an error. Sometimes the expected behavior is not so obvious.</p>
<p>Keep in mind that programmers are trained in programming, and are usually not experts in the fields where our programs are being used. For example, if there&rsquo;s a problem with the way tax is being calculated in an accounting application, the people looking at the bug may know less than you do on the subject. For technical bugs and miscalculations it can also be helpful to include how you determined the result you expected.</p>
<h2 id="where-did-you-encounter-the-bug-location">Where did you encounter the bug? (Location)</h2>
<p>It&rsquo;s not always obvious, but sometimes companies have more than one piece of software, or more than one version in circulation.</p>
<p>If you&rsquo;re using a website, include the URL. If it&rsquo;s a visual bug, maybe include the browser you&rsquo;re using, it&rsquo;s version, and the operating system you&rsquo;re using it on. If it&rsquo;s a mobile app, include the kind of phone you have. If the app has a version number include that too.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/debugging" term="debugging" label="debugging" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/bugs" term="bugs" label="bugs" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/quality" term="quality" label="quality" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/practices" term="practices" label="practices" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Breaking Past Senior Developer]]></title>
            <link href="https://jessemcdowell.ca/2021/01/breaking-past-senior-developer/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2016/08/infiltrating-an-organization/?utm_source=atom_feed" rel="related" type="text/html" title="Infiltrating an Organization (or: Joining a New Team)" />
                <link href="https://jessemcdowell.ca/2011/01/working-together-and-having-fun/?utm_source=atom_feed" rel="related" type="text/html" title="Working Together and Having Fun" />
                <link href="https://jessemcdowell.ca/2012/03/choosing-priorities/?utm_source=atom_feed" rel="related" type="text/html" title="Choosing Priorities" />
                <link href="https://jessemcdowell.ca/2011/10/sharpening-the-saw/?utm_source=atom_feed" rel="related" type="text/html" title="Sharpening The Saw" />
                <link href="https://jessemcdowell.ca/2011/10/managing-priorities-outside-of-work/?utm_source=atom_feed" rel="related" type="text/html" title="Managing Priorities Outside of Work" />
            
                <id>https://jessemcdowell.ca/2021/01/breaking-past-senior-developer/</id>
            
            
            <published>2021-01-29T19:45:00-08:00</published>
            <updated>2021-01-29T23:01:00-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>Developing software is an excellent career. Software has touched almost every aspect of our world, and its impact is always expanding. Many new things have become possible because of software, things that couldn&rsquo;t have been dreamed of even ten years ago. The industry is continuing to expand. Tools are getting better. New opportunities are appearing everywhere&hellip; So why haven&rsquo;t you gotten a promotion in ten years?</p>
<p>In the early days of my career, I got new responsibilities, promotions, and raises fairly regularly. It took a bit of luck, a lot of hard work, and a few years (but not very many years), to work my way up to a senior developer position. Senior means different things at different places, but eventually I got to a place where there was no easy next step, and I had a good number of peers in exactly the same position.</p>
<p>I can now proudly tell you that I have broken past the wall. About a year ago I achieved my goal and got promoted to the role of system architect. The following are the things that made the difference for me.</p>
<div class="table-of-contents">
    <nav id="TableOfContents">
  <ul>
    <li><a href="#choose-the-career-path-you-want">Choose the career path you want</a></li>
    <li><a href="#develop-your-hard-skills-and-your-technical-breadth">Develop your hard skills and your technical breadth</a></li>
    <li><a href="#find-the-right-company-and-manager-to-help-you-advance">Find the right company and manager to help you advance</a></li>
    <li><a href="#develop-your-soft-skills">Develop your soft skills</a></li>
    <li><a href="#develop-your-personal-brand">Develop your personal brand</a></li>
  </ul>
</nav>
</div>

<h2 id="choose-the-career-path-you-want">Choose the career path you want</h2>
<p>There seems to be three distinct paths forward from senior developer:</p>
<ul>
<li>people and project management</li>
<li>technical leadership</li>
<li>focused technical expert</li>
</ul>
<p>The focused technical expert path only exists in companies that need experts focused in very specific areas such as hardware manufacturers or OS / platform companies. If you are at the top of one of these specialties, you probably already work for the company where you&rsquo;ll be getting promoted, and this post won&rsquo;t really apply to you.</p>
<p>Both management and technical leadership are fine choices, but you should decide which is right for you. Here are some important differences to help you make your choice:</p>
<ul>
<li>Technical leadership positions are harder to get. There aren&rsquo;t as many of them, and it seems they are more sought after.</li>
<li>Management positions will eventually strangle out all time for development. At some point you may not be able to go back.</li>
<li>Technical leadership positions require making harder technical choices based on research and experimentation.</li>
<li>Management positions require making harder business decisions based on company and customer priorities.</li>
</ul>
<p>Either way, they are both big changes from full-time development. You will be leaning heavily on your soft skills, spending more of your time in meetings and writing a lot of documents. You will typically have less control over the things you focus on, and your success will depend more heavily on the work of others.</p>
<p>If all this sounds awful to you, it is entirely fine to stay where you are. You can spice things up by changing projects or companies. You may want to scale up your impact some day, but until then, do what you love. The only unfortunate consequence is that you will likely be stuck in your current salary range.</p>
<h2 id="develop-your-hard-skills-and-your-technical-breadth">Develop your hard skills and your technical breadth</h2>
<p>Both leadership and management positions will favour generalists because you&rsquo;ll typically have responsibility for far more parts of your system. If you&rsquo;re after a management position, a basic understanding of different problem areas will be enough. If you want to be a technical leader, make sure you have some success stories in a few areas. If you&rsquo;ve only ever worked in the back end, for example, see if you can get some time working in the front of the house.</p>
<p>Technical leaders are going to need to be up to date on all the latest technologies and be able to discuss them intelligently. If learning and working with new things isn&rsquo;t possible where you work, it may be time to consider changing companies. Contracting helped me expand my portfolio dramatically in just a few years, but it&rsquo;s not a path I&rsquo;d necessarily recommend to anyone.</p>
<h2 id="find-the-right-company-and-manager-to-help-you-advance">Find the right company and manager to help you advance</h2>
<p>It&rsquo;s always important to have a good relationship with your manager, but this is especially true if you want to advance your career. Make sure your manager knows what your goal is, and make sure they are supportive. A good manager will tell you what areas you need to improve, represent you when the right opportunity appears internally, or vouch for you if you find one outside.</p>
<p>Make sure your company has the role you want. Very small companies will probably not need a software architect, and may not have many openings for new managers. If you&rsquo;re growing quickly it may appear in the future, but ask your manager. If you&rsquo;re valued enough, they might even create the position for you when it makes sense.</p>
<p>If you&rsquo;re in a place where advancing isn&rsquo;t possible, or you aren&rsquo;t getting the support you need, you may have to make the difficult choice and move on. When considering new roles, think about how they can help you work toward your goal. It&rsquo;s also totally fine, maybe even advantageous, to tell a potential employer what you&rsquo;re working toward, and ask if it&rsquo;s possible with them.</p>
<h2 id="develop-your-soft-skills">Develop your soft skills</h2>
<p>In your new role you&rsquo;re going to need to juggle more priorities, build consensus with more people, and convince everyone that it&rsquo;s all under control. You won&rsquo;t need to be amazing at this on your first day, but you won&rsquo;t get the job if they don&rsquo;t think you can do it.</p>
<p>Here are some things to work on:</p>
<ul>
<li>Find your own personal organization system and get comfortable with it.</li>
<li>Study development process and team management. There are lots of great books, blogs, videos, and classes.</li>
<li>Give more presentations, and put effort into improving them.</li>
<li>If you aren&rsquo;t already, see if you can become someone&rsquo;s manager.</li>
<li>Try coaching / mentoring someone who is struggling.</li>
<li>Run a study group or a book club.</li>
</ul>
<h2 id="develop-your-personal-brand">Develop your personal brand</h2>
<p>I think a lot of us developers lack awareness of our personal brand, but it&rsquo;s an important part of your career, and it&rsquo;s importance increases the higher up you go. Make sure people want to work with you, and try to get some high-profile successes under your belt. These will all help you earn a promotion, and give you essential credibility once you get it.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/career" term="career" label="career" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/leadership" term="leadership" label="leadership" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/people-management" term="people-management" label="people management" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Brewing Your Own Iced Tea]]></title>
            <link href="https://jessemcdowell.ca/2018/06/brewing-your-own-iced-tea/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2014/06/why-i-only-drink-loose-tea/?utm_source=atom_feed" rel="related" type="text/html" title="Why I Only Drink Loose Tea" />
                <link href="https://jessemcdowell.ca/2011/01/working-together-and-having-fun/?utm_source=atom_feed" rel="related" type="text/html" title="Working Together and Having Fun" />
            
                <id>309</id>
            
            
            <published>2018-06-13T21:29:59-07:00</published>
            <updated>2018-06-13T21:29:59-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>There are few things more refreshing than a cold glass of good iced tea. I&rsquo;ve tried iced tea from a lot of places, but the best I&rsquo;ve had to this day is my own recipe. It&rsquo;s so easy that I can&rsquo;t in good conscience keep it secret. It&rsquo;s also far cheaper than anything you can buy in a can or bottle, and a lot healthier because it doesn&rsquo;t require any kind of sweetener. The only down side is that it takes a few hours to cool.</p>
<p>The short version: Make good tea with boiling water, then let it cool slowly before serving.</p>
<p>The long version:</p>
<p>Any decent black tea should work, but my favourite is loose Twinings Earl Grey (<a href="https://www.amazon.com/Twinings-Classics-Earl-Grey-Loose/dp/B0005ZXX1S/ref=sr_1_42?s=grocery&amp;ie=UTF8&amp;qid=1375601004&amp;sr=1-42">Amazon.com</a>). The loose version tastes quite different than their tea bags, so do put in the effort to find it.</p>
<p>You will also need a fine strainer to remove the tea. I use a cloth tea sock (<a href="https://www.amazon.com/dp/B002U77176/ref=sxts_k2p-hero-vn_lp_3?pf_rd_m=ATVPDKIKX0DER&amp;pf_rd_p=8011851592090061987&amp;pd_rd_wg=wF4KQ&amp;pf_rd_r=THGQ3HKM51EH9K3JW6Y2&amp;pf_rd_s=desktop-sx-top-slot&amp;pf_rd_t=301&amp;pd_rd_i=B002U77176&amp;pd_rd_w=e9sIM&amp;pf_rd_i=tea+sock&amp;pd_rd_r=63759cf2-6f82-4d78-840b-edb729e1d77b&amp;ie=UTF8&amp;qid=1527138358&amp;sr=3">Amazon.com</a>).</p>
<p>Also, be careful that you brew the tea in a container that can handle boiling water. A heat-safe glass pitcher is nice, but a regular metal pot from your kitchen will do just as well. Once it&rsquo;s down to room temperature, you can transfer it to any container you like.</p>
<p>I add 25 mL of loose tea for every 1 L of boiling water. Make sure the water is actually at a rolling boil. The hot tap on your water cooler is not going to work for this.</p>
<p>Set a timer for 5 minutes. At about 2 minutes, stir the tea. At 5 minutes, remove the tea strainer gently without stirring. The tea seems to produce the most flavour about two minutes in, and the most bitterness at the end of the steeping. Stirring only the once yields the smoothest flavour for me.</p>
<p>Let the tea cool slowly to room temperature, then put it in the fridge to cool the rest of the way. Adding ice while it&rsquo;s hot can disturb the flavour, and dilutes the tea.</p>
<p>Once it&rsquo;s cold, pour a glass, add ice if you like, and enjoy.</p>
<p>You can experiment with different teas. Green or white tea should work, but follow their proper brewing instructions. Only black tea should be brewed with boiling water. My second favourite mix is using a plain black orange pekoe, inserting a couple springs of fresh mint after removing the tea.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/food" term="food" label="food" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/recipes" term="recipes" label="recipes" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/tea" term="tea" label="tea" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Infiltrating an Organization (or: Joining a New Team)]]></title>
            <link href="https://jessemcdowell.ca/2016/08/infiltrating-an-organization/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2011/10/sharpening-the-saw/?utm_source=atom_feed" rel="related" type="text/html" title="Sharpening The Saw" />
                <link href="https://jessemcdowell.ca/2012/03/choosing-priorities/?utm_source=atom_feed" rel="related" type="text/html" title="Choosing Priorities" />
                <link href="https://jessemcdowell.ca/2011/10/managing-priorities-outside-of-work/?utm_source=atom_feed" rel="related" type="text/html" title="Managing Priorities Outside of Work" />
                <link href="https://jessemcdowell.ca/2011/05/consulting-a-brave-new-world/?utm_source=atom_feed" rel="related" type="text/html" title="Consulting - A Brave New World" />
                <link href="https://jessemcdowell.ca/2011/01/working-together-and-having-fun/?utm_source=atom_feed" rel="related" type="text/html" title="Working Together and Having Fun" />
            
                <id>290</id>
            
            
            <published>2016-08-31T22:23:42-07:00</published>
            <updated>2021-01-29T19:45:23-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>It takes some time to integrate into a new team. I always feel like an outsider at first. As I build friendships and trust, I&rsquo;m able to contribute with increasing effectiveness. Having noticed some patterns, I&rsquo;ve been able to make the process faster and smoother using a few simple tricks.</p>
<p><a href="https://en.wikipedia.org/wiki/Tuckman%27s_stages_of_group_development">Tuckman&rsquo;s Stages of Group Development</a> describe what happens when a team is formed. His theory has four stages: forming, storming, norming, and performing. As I stared writing this post, I noticed that the stages I was describing lined up fairly well with his. It&rsquo;s important to note that I&rsquo;m talking about joining an existing team, where he talks about a team being formed entirely from new people.</p>
<p>I will relate the stages I&rsquo;ve noticed using his labels, but leave it as an exercise for the reader to compare to his more detailed descriptions.</p>
<h2 id="1-forming">1) Forming</h2>
<p>When I first join a team, everyone is as clueless about me as I am about them. Anything I can do to facilitate interaction here is valuable. I like to set out a couple trinkets on my desk, things that show my personality and interests. This creates openings for others to start conversations, which can help to break the ice. It can be awkward and uncomfortable as the &ldquo;new guy&rdquo;, but getting through this sooner really helps speed things up.</p>
<p>I also tend to ask a lot of questions while I&rsquo;m getting my bearings. The hard part here is gathering information without judging or commenting too much. Honesty is important, but this early in the process, it can be more damaging than helpful. It&rsquo;s easy to forget that things were built with constraints and assumptions I wasn&rsquo;t there to experience.</p>
<p>That being said, looking things over with a fresh set of eyes can uncover many interesting things. So that I don&rsquo;t lose track of them, I write every idea and observation down and revisit them later.</p>
<h2 id="2-storming">2) Storming</h2>
<p>After a month or two, I start getting used to what&rsquo;s going on, and the problems the team is solving. The things that seemed strange before may now be routine, but some will still get in my way. My productivity improves, but I don&rsquo;t feel like a full member of the team yet.</p>
<p>This is when I start going through my list of suggestions. I remove the items that no longer make sense, and then prioritize the remainder. I work through my list slowly, applying gentle pressure, trying to ask questions of various people on the team. Sometimes the questions receive insightful answers, other times they provoke good changes. Occasionally they uncover issues with team dynamics or lost political battles. All of these outcomes are valuable in different ways.</p>
<p>Essentially, I am trying to develop my voice. By waiting until I&rsquo;ve been in the team a bit, I have a much firmer foundation to launch from. Being honest, and being able to disagree respectfully are necessary to fully contributing to the team. Just remember to take it slowly. If you come on too strong too suddenly you can alienate your coworkers.</p>
<p>With time, everyone gets used to me and my style. Once they realize that I can challenge opinions without judging their source, things become a lot smoother.</p>
<h2 id="3-norming">3) Norming</h2>
<p>After a while, I run out of questions. At this point, I will have a pretty good understanding of the architecture, the history and opinions that shaped it, and have a rough idea where people stand on the important issues. Others should have a good idea where I stand too.</p>
<p>This is when I start fine-tuning my personal processes, and trying to resolve anything that&rsquo;s still holding me back. I expect to have a good relationship with my manager by this point. I would be bringing up more serious issues earlier, but now I want to start bringing up everything else. If our relationship strong enough, this is when I would try to fix that also.</p>
<p>Sometimes I never make it to this stage. I might be spending too much energy arguing, or feel like my contributions are not appreciated. If I don&rsquo;t make it here within a few months, and have no clear path to improve things, I know it&rsquo;s time to brush up my resume.</p>
<h2 id="4-performing">4) Performing</h2>
<p>With good working relationships, enough context about the problem space, and all my major issues resolved, I can now focus on getting some work done.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/career" term="career" label="career" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/leadership" term="leadership" label="leadership" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/practices" term="practices" label="practices" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Why I Only Drink Loose Tea]]></title>
            <link href="https://jessemcdowell.ca/2014/06/why-i-only-drink-loose-tea/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2011/01/working-together-and-having-fun/?utm_source=atom_feed" rel="related" type="text/html" title="Working Together and Having Fun" />
            
                <id>279</id>
            
            
            <published>2014-06-01T15:47:35-07:00</published>
            <updated>2024-11-01T13:30:30-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>When I was a child, I drank tea because my parents wouldn&rsquo;t let me drink coffee. I would soak a tea bag in hot water until it made a dark, bitter liquid, then dump in milk and sugar until it was overly sweet, and mostly flavourless. I would sip it to fit in with adults, but I wouldn&rsquo;t say that it was something I enjoyed.</p>
<p>As a young adult, I tried loose tea on the advice of a friend. It was a totally different drink. Black tea tasted rich and warming. Earl grey had a wonderfully soothing aroma. Green tea had a nourishing earthy taste that made me feel good when I drank it. More importantly, there was very little bitterness, so I could skip the milk and sugar, and enjoy the flavours even more.</p>
<p>The reason why loose tea tastes better is basic chemistry. Tea is damaged by exposure to air. The tea in tea bags is ground to a fine dust, then spread out into thin mesh pouches. The larger surface area accelerates the ageing process, robbing the flavour and aroma.</p>
<p>It may be true that tea bags are easier than loose tea, but with a little equipment they aren&rsquo;t much easier. Most of the time, I use a simple strainer that sits in the cup like <a href="https://www.amazon.ca/s?k=finum+basket">this one</a> (<a href="https://www.amazon.ca/s?k=finum+basket">amazon.ca</a>, <a href="https://www.amazon.com/s?k=finum+basket">amazon.com</a>). Measure some loose tea into the basket, pour boiling water over it, and remove the basket once it&rsquo;s finished steeping. The lid can be flipped to hold the wet basket, so you don&rsquo;t even need to stay near the sink.</p>
<p>At work, or when I&rsquo;m travelling, I use <a href="https://www.libretea.com/">a tea thermos with a built-in strainer made by a local company</a>. I put dark teas in the strainer part, steep with the container upside down, then flip and remove both lids to drink. It&rsquo;s easy to use, and it makes it easy to carry hot tea around without spilling or making a mess.</p>
<p>Loose teas can be harder to find, but there are many great sources online. If there is a tea store in your neighbourhood, going in and selecting a few teas can be a lot of fun. Except for the very highest grades, most good teas aren&rsquo;t too expensive, especially when compared to premium tea bags.</p>
<p>Once awakened, I started taking a lot of pleasure in hunting down and trying new teas and brewing equipment. I have been researching and experimenting for more than a decade now, and I still feel like I&rsquo;ve barely scratched the surface.</p>
<p>Sometimes I am forced to drink tea from tea bags while I&rsquo;m in a restaurant or visiting family. Some tea bags that are better than others, but all of them seem lacking compared to my own stash at home. It&rsquo;s enough that I&rsquo;ll never forget why I put in the extra effort to enjoy my cup of tea.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/food" term="food" label="food" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/tea" term="tea" label="tea" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Controller Led Navigation in Angular]]></title>
            <link href="https://jessemcdowell.ca/2014/04/controller-led-navigation-in-angular/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2010/11/64-bit-iis-vs-32-bit-assemblies/?utm_source=atom_feed" rel="related" type="text/html" title="64-bit IIS vs. 32-bit Assemblies" />
            
                <id>264</id>
            
            
            <published>2014-04-01T15:59:35-07:00</published>
            <updated>2024-11-01T13:01:01-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>I recently tried <a href="https://angularjs.org/">AngularJS</a> for a pet project. I watched <a href="https://www.youtube.com/watch?v=i9MHigUZKEM" title="a great tutorial">a great tutorial about the platform</a>, then dove in head first. You can see what I built here: <a href="https://jessemcdowell.ca/mysterysolver" title="Mystery Solver">MysterySolver</a></p>
<p>I enjoyed Angular. It was straightforward to use, and allowed me to bang out a lot of functionality without much cumbersome boilerplate code. Jasmine, the testing framework set up in the bootstrap source, was also pretty slick. I really liked how I could nest a bunch of test blocks inside of each other to reuse common setup code.</p>
<p>The only serious bump I ran into was getting multiple controllers to work together.</p>
<p>My goal was to build a wizard-style flow. A user enters a bit of info, hits a button, then enters more info. The answers in one step affect the questions in future steps, or might cause steps to be added or taken away. I wanted the controller to trigger the navigation, and I wanted to pass state when it did.</p>
<p>View-led navigation would have been easy: add a link whenever you like. A user clicks and the new controller is loaded. This kind of navigation is great for keeping controllers ignorant of each other. I suspect this approach is better for search engine indexers as well. It wasn&rsquo;t ideal for me.</p>
<p>I searched the Internet and read a bunch of documentation hoping to find an easy answer. What I found was a bunch of other people asking the same questions. When I finally decided to build it myself, the solution was easier than I expected:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="s1">&#39;use strict&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">// var module = angular.module(&#39;...&#39;, []);
</span></span></span><span class="line"><span class="cl"><span class="c1"></span><span class="nx">module</span><span class="p">.</span><span class="nx">service</span><span class="p">(</span><span class="s1">&#39;navigation&#39;</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">$location</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="kd">var</span> <span class="nx">storage</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">return</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="nx">navigate</span><span class="o">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">path</span><span class="p">,</span> <span class="nx">data</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">storage</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="nx">path</span><span class="o">:</span> <span class="nx">path</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="nx">data</span><span class="o">:</span> <span class="nx">data</span>
</span></span><span class="line"><span class="cl">      <span class="p">};</span>
</span></span><span class="line"><span class="cl">      <span class="nx">$location</span><span class="p">.</span><span class="nx">path</span><span class="p">(</span><span class="nx">path</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">    <span class="p">},</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nx">getNavigationData</span><span class="o">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="p">((</span><span class="nx">storage</span> <span class="o">==</span> <span class="kc">null</span><span class="p">)</span> <span class="o">||</span> <span class="p">(</span><span class="nx">storage</span><span class="p">.</span><span class="nx">path</span> <span class="o">!=</span> <span class="nx">$location</span><span class="p">.</span><span class="nx">path</span><span class="p">()))</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="k">throw</span> <span class="s1">&#39;navigated without passing data&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="kd">var</span> <span class="nx">data</span> <span class="o">=</span> <span class="nx">storage</span><span class="p">.</span><span class="nx">data</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="nx">storage</span> <span class="o">=</span> <span class="kc">null</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">      <span class="k">return</span> <span class="nx">data</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">};</span>
</span></span></code></pre></div><p>To navigate, I used code like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">module</span><span class="p">.</span><span class="nx">controller</span><span class="p">(</span><span class="s1">&#39;Page1Controller&#39;</span><span class="p">,</span> <span class="p">[</span><span class="s1">&#39;$scope&#39;</span><span class="p">,</span> <span class="s1">&#39;navigation&#39;</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">$scope</span><span class="p">,</span> <span class="nx">navigation</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">$scope</span><span class="p">.</span><span class="nx">navigate</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">navigationParameter</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nx">navigation</span><span class="p">.</span><span class="nx">navigate</span><span class="p">(</span><span class="s1">&#39;/Page2&#39;</span><span class="p">,</span> <span class="nx">navigationParameter</span><span class="p">);</span>
</span></span><span class="line"><span class="cl">  <span class="p">};</span>
</span></span><span class="line"><span class="cl"><span class="p">}]);</span>
</span></span></code></pre></div><p>In the destination controller, I fetched the navigation parameter like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="cl"><span class="nx">module</span><span class="p">.</span><span class="nx">controller</span><span class="p">(</span><span class="s1">&#39;Page2Controller&#39;</span><span class="p">,</span> <span class="p">[</span><span class="s1">&#39;$scope&#39;</span><span class="p">,</span> <span class="s1">&#39;navigation&#39;</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">$scope</span><span class="p">,</span> <span class="nx">navigation</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="nx">$scope</span><span class="p">.</span><span class="nx">navigationParameter</span> <span class="o">=</span> <span class="nx">navigation</span><span class="p">.</span><span class="nx">getNavigationData</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}]);</span>
</span></span></code></pre></div><p>It was easy to test this code. Whenever I expected a controller to navigate, I checked the value of <code>$location.path()</code>. To pass navigation parameters into controllers, I just called the navigate method before the controller was created in the setup block.</p>
<p>Unfortunately this solution breaks the back button. Because the browser triggers backward navigation, the navigation parameter won&rsquo;t be set when the controller tries to load. This wasn&rsquo;t something I needed, so I left alone.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/angular" term="angular" label="angular" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/js" term="js" label="js" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/web" term="web" label="web" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Reading Server Graphs: Connected Users]]></title>
            <link href="https://jessemcdowell.ca/2013/06/reading-server-graphs-connected-users/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2012/04/installutil-and-badimageformatexception/?utm_source=atom_feed" rel="related" type="text/html" title="InstallUtil and BadImageFormatException - Facepalm" />
                <link href="https://jessemcdowell.ca/2011/08/doubling-data-for-performance-testing/?utm_source=atom_feed" rel="related" type="text/html" title="Doubling Data for Performance Testing" />
                <link href="https://jessemcdowell.ca/2010/12/teaching-it/?utm_source=atom_feed" rel="related" type="text/html" title="Teaching IT" />
                <link href="https://jessemcdowell.ca/2010/11/64-bit-iis-vs-32-bit-assemblies/?utm_source=atom_feed" rel="related" type="text/html" title="64-bit IIS vs. 32-bit Assemblies" />
                <link href="https://jessemcdowell.ca/2011/01/working-together-and-having-fun/?utm_source=atom_feed" rel="related" type="text/html" title="Working Together and Having Fun" />
            
                <id>232</id>
            
            
            <published>2013-06-14T22:30:44-07:00</published>
            <updated>2013-06-14T22:30:44-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>I&rsquo;ve spent the last several years working on multi-user server systems in two different companies. Both those companies had a giant monitor hanging off a wall showing a graph of connected users. It won&rsquo;t give you detailed diagnostic information, but it is a good indicator for the health of your servers, and your product generally. If you learn to notice certain patterns in your user graph, it can also save you precious time when things go wrong.</p>
<div class="table-of-contents">
    <nav id="TableOfContents">
  <ul>
    <li><a href="#assumptions">Assumptions</a></li>
    <li><a href="#healthy">Healthy</a></li>
    <li><a href="#central-component-malfunction--failure">Central Component Malfunction / Failure</a></li>
    <li><a href="#distributed-component-malfunction">Distributed Component Malfunction</a></li>
    <li><a href="#distributed-server-failure">Distributed Server Failure</a></li>
    <li><a href="#application-overloaded">Application Overloaded</a></li>
    <li><a href="#central-component-performance">Central Component Performance</a></li>
    <li><a href="#denial-of-service-attack">Denial-of-Service Attack</a></li>
    <li><a href="#television-ad">Television Ad</a></li>
    <li><a href="#television-event">Television Event</a></li>
    <li><a href="#monitoring-failure">Monitoring Failure</a></li>
  </ul>
</nav>
</div>

<h2 id="assumptions">Assumptions</h2>
<p><img src="NetworkDiagram.jpg" alt="A hand-drawn network diagram with a load balancer in front of three web nodes and a single backing database."></p>
<p>The patterns I&rsquo;m describing assume a common architecture: you have some boxes in the front that have your traffic balanced between them, and some stuff behind them that&rsquo;s not balanced. For the purposes of this discussion, it doesn&rsquo;t matter if it&rsquo;s one or more apps in the front, if you have a service tier, or what kind of data storage you use. Anything that&rsquo;s redundant is going to be called distributed, and anything that&rsquo;s not is going to be called central. You need some way to track user sessions, and the ability to detect disconnects within a few minutes. Some of these graphs also depend on a load balancer that&rsquo;s configured to keep a single user session on the same distributed server.</p>
<h2 id="healthy">Healthy</h2>
<p><img src="Normal.png" alt="Two smooth sine waves being added to make a third smooth sine wave."></p>
<p><img src="NormalZoomed.png" alt="A zoomed in look at part of the total sine wave. The line is making a very smooth curve."></p>
<p>This is how a healthy system should look. I&rsquo;m showing two views, one broken into a couple regions, and another just showing the total within a shorter time span. It&rsquo;ll be a lot easier to show these patterns on the zoomed in graph, so I&rsquo;ll use that as a base line for the following examples.</p>
<p>I was surprised when I first saw the smooth wave-like pattern a connected user graph makes. These examples are a pure sine wave because it was easy to produce, but it&rsquo;s pretty close to what I&rsquo;ve seen on real systems. The waves might get a little higher and wider on weekends, but it&rsquo;s always a smooth line when things are normal.</p>
<p>The numbers and data shown are totally fabricated, and do not represent any of the systems I&rsquo;ve worked on. My focus here is on the disruptions to the lines. I have observed all of the patterns I&rsquo;m showing in real production environments, some of them numerous times.</p>
<h2 id="central-component-malfunction--failure">Central Component Malfunction / Failure</h2>
<p><img src="CentralComponentMalfunction.png" alt="Line has sudden spikes up and down. The down spikes almost reach the 0 line."></p>
<p>This is the worst case for a server system, and as you can see, the results are drastic. You can tell it&rsquo;s a central component because the number of connected users drops very close to zero. You&rsquo;ll also note that I show the connected users shooting back above the norm. This happens when users try to reconnect once or multiple times when the system becomes unresponsive. This is a pattern you will notice during most malfunctions.</p>
<h2 id="distributed-component-malfunction">Distributed Component Malfunction</h2>
<p><img src="DistributedComponentMalfunction.png" alt="Line has sudden spikes up and down but they stay relatively close to the trend line."></p>
<p>This is a much more common occurrence in a server system; a server starts to malfunction without losing the ability to respond to network traffic. The load balancer doesn&rsquo;t detect a failure, but users have serious trouble using the app. You will see a serious fluctuation in the graph as users start disconnecting and reconnecting, slowly getting pushed to new working servers. This is one of the reasons why it&rsquo;s important to have sticky sessions on your load balancer.</p>
<h2 id="distributed-server-failure">Distributed Server Failure</h2>
<p><img src="DistributedServerFailure.png" alt="Line has a sudden drop down (a portion of it&rsquo;s load), a very small bump up, and then things return to normal."></p>
<p>When one of your distributed servers fails outright, you see a much more sudden gouge, but proportional to the number of servers you have. The graph returns to normal fairly quickly once the load balancer detects the bad server, and users finish logging back in.</p>
<h2 id="application-overloaded">Application Overloaded</h2>
<p><img src="ApplicationOverloaded.png" alt="At the peak of a curve the line goes from smooth to choppy with spikes downward from the trend line."></p>
<p>Performance limits can be distinguished because they get worse as the number of connecting users increases. This example shows a hard wall, but the severity you observe will depend on how your system is breaking down. The key indicator is the subtle twitching that gets progressively worse as the pressure builds. The deep downward spikes occur as various parts of the system start throwing large quantities of errors.</p>
<h2 id="central-component-performance">Central Component Performance</h2>
<p><img src="CentralComponentPerformance.png" alt="The line goes from smooth to choppy with spikes down higher than the spikes up. It is displayed off the peak of the normal curve."></p>
<p>This is what it looks like when a central component starts to have a performance issue. When it occurs off peak, it&rsquo;s a good clue that some critical system is acting up. If it&rsquo;s not obvious what&rsquo;s wrong, here are a few things you can check: failed hard drives in your storage system, hardware errors in your system logs, unusual latency with a heavily-used APIs, or perhaps someone is running an ad-hoc query on the production database.</p>
<h2 id="denial-of-service-attack">Denial-of-Service Attack</h2>
<p><img src="DosAttack.png" alt="The trend line suddenly drops near zero. There are a few spikes of retrying users before the line goes flat."></p>
<p>Denial of Service attacks are awful, and unfortunately effective things. They look different than network gear failures because attackers have trouble ramping up load generators quickly. Once they do get going though, your networking gear will usually start failing, and nothing will get through until they stop. Most DOS attacks are network-level, so you shouldn&rsquo;t see increased activity or connections before or during the attack.</p>
<h2 id="television-ad">Television Ad</h2>
<p><img src="TvAd.png" alt="The trend line has a slight bump and then slowly trails back down to the trend line."></p>
<p>Advertising should increase your number of connected users. If your ad hits lot of people at the same time, such as a TV ad, you&rsquo;ll see a bump like this. There will be a spike just as the ad airs, a bit of hang time, then it starts to trickle down to normal. The size of the bump will depend on the effectiveness and reach of your ad.</p>
<h2 id="television-event">Television Event</h2>
<p><img src="TvEvent.png" alt="The trend line takes a noticeable (but proportional) dip, then raises slightly above the trend line before returning to normal."></p>
<p>This is one of my favourite patterns. This is what happens if there is a big event that your audience is interested in. An example would be a sports site during the Super bowl. You see a dip while it airs, then you go back to normal when it ends. If people don&rsquo;t like the event, the line might start returning to normal sooner.</p>
<h2 id="monitoring-failure">Monitoring Failure</h2>
<p><img src="MonitoringFailure.png" alt="The trend line falls instantly to 0 and stays there until it returns instantly to normal."></p>
<p>A flat line like this is almost never real, except maybe during deliberate maintenance windows. If it&rsquo;s not obvious why you&rsquo;re flat, you should check that your monitoring and graphing systems are collecting data correctly.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/performance" term="performance" label="performance" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/devops" term="devops" label="devops" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/debugging" term="debugging" label="debugging" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/quality" term="quality" label="quality" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[InstallUtil and BadImageFormatException - Facepalm]]></title>
            <link href="https://jessemcdowell.ca/2012/04/installutil-and-badimageformatexception/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2010/11/64-bit-iis-vs-32-bit-assemblies/?utm_source=atom_feed" rel="related" type="text/html" title="64-bit IIS vs. 32-bit Assemblies" />
                <link href="https://jessemcdowell.ca/2010/12/teaching-it/?utm_source=atom_feed" rel="related" type="text/html" title="Teaching IT" />
                <link href="https://jessemcdowell.ca/2011/08/doubling-data-for-performance-testing/?utm_source=atom_feed" rel="related" type="text/html" title="Doubling Data for Performance Testing" />
                <link href="https://jessemcdowell.ca/2011/01/working-together-and-having-fun/?utm_source=atom_feed" rel="related" type="text/html" title="Working Together and Having Fun" />
                <link href="https://jessemcdowell.ca/2011/01/when-to-add-an-orm-tool/?utm_source=atom_feed" rel="related" type="text/html" title="When to Add an ORM Tool" />
            
                <id>225</id>
            
            
            <published>2012-04-14T23:09:18-07:00</published>
            <updated>2012-04-14T23:09:18-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>I had a frustrating issue at work this week: one that was easy to fix, but embarrassingly difficult to find. I came pretty close to giving up, which is not a solution I often explore, but in the end we figured it out and got everything working.</p>
<p>A member of our operations team was installing a Windows service I&rsquo;d built to monitor some stuff in our production environment. I&rsquo;ve made a few windows services in my day, and installed them many times on many machines. I&rsquo;d even installed this one on my development machine with no issue. In our staging environment, however, this is what we got:</p>
<blockquote>
<p>C:\Install\TheService&gt;C:\Windows\Microsoft.NET\Framework64\v4.0.30319\InstallUtil.exe TheService.exe Microsoft (R) .NET Framework Installation utility Version 4.0.30319.1 Copyright (c) Microsoft Corporation. All rights reserved.</p>
<p>Exception occurred while initializing the installation: System.BadImageFormatException: Could not load file or assembly &lsquo;file:///C:\Monitoring\Service\TheService.exe&rsquo; or one of its dependencies. An attempt was made to load a program with an incorrect format.</p></blockquote>
<p>We checked the likely things: the framework version, the platform the app was built for, even re-copying the files in case they somehow got corrupted. When these didn&rsquo;t work, we started trying more radical things: forcing all assemblies to 32 bit, even running the service as an executable to see if there was some error in the app.</p>
<p>In my defence, we are both experienced engineers, and I&rsquo;m not the only person who missed it. Look closely at the command line we used:</p>
<blockquote>
<p>C:\Windows\Microsoft.NET\Framework<strong>64</strong>\v4.0.30319\InstallUtil.exe</p></blockquote>
<p>Long version: Service applications in Visual Studio 2010 are 32 bit by default, and this is a reasonable default for them to have. We were trying to install the 32 bit service with the 64 bit version of InstallUtil. InstallUtil loads the target assembly to access it&rsquo;s installation instructions, but you can&rsquo;t load a 32 bit assembly from a 64 bit application (or vice versa). If you try to, you get a BadImageFormatException.</p>
<p>Short version: Two numbers derailed my entire afternoon.</p>
<p>It would have been nice if the error message from InstallUtil was a little more specific, but I suppose this isn&rsquo;t a common problem. At least I got a good reminder about the importance of checking the small details when the big ones aren&rsquo;t bearing fruit.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/.net" term=".net" label=".net" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/devops" term="devops" label="devops" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/debugging" term="debugging" label="debugging" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Choosing Priorities]]></title>
            <link href="https://jessemcdowell.ca/2012/03/choosing-priorities/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2011/10/sharpening-the-saw/?utm_source=atom_feed" rel="related" type="text/html" title="Sharpening The Saw" />
                <link href="https://jessemcdowell.ca/2011/10/managing-priorities-outside-of-work/?utm_source=atom_feed" rel="related" type="text/html" title="Managing Priorities Outside of Work" />
                <link href="https://jessemcdowell.ca/2011/05/consulting-a-brave-new-world/?utm_source=atom_feed" rel="related" type="text/html" title="Consulting - A Brave New World" />
            
                <id>221</id>
            
            
            <published>2012-03-08T21:50:05-08:00</published>
            <updated>2024-11-01T13:01:01-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>During the summer I started a difficult but important journey to reorganize my life. The first step was <a href="https://jessemcdowell.ca/2011/10/managing-priorities-outside-of-work" title="organizing my daily tasks">organizing my daily tasks</a>. I was successful, but becoming productive again has created new issues.</p>
<p>When you change from trying to do everything to doing what&rsquo;s most important, you need to decide what important means. Figuring this out for myself has proven difficult. I still don&rsquo;t have all the answers, but I am constantly making progress.</p>
<p>Deciding what isn&rsquo;t important has been easier. I am abandoning Themis, my attempt at an open source project. I no longer have a need for it, and it will take a lot more effort to get it finished than I originally expected. I would be delighted if someone wanted to take it on, but there isn&rsquo;t enough there for it to be likely.</p>
<p>Writing in this blog has also been pushed down the priority list. I intend to keep going, but instead of forcing out a steady pace of content, I&rsquo;ll wait until I have ideas that I have a strong desire to share.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/career" term="career" label="career" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Sharpening The Saw]]></title>
            <link href="https://jessemcdowell.ca/2011/10/sharpening-the-saw/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2011/10/managing-priorities-outside-of-work/?utm_source=atom_feed" rel="related" type="text/html" title="Managing Priorities Outside of Work" />
                <link href="https://jessemcdowell.ca/2011/05/consulting-a-brave-new-world/?utm_source=atom_feed" rel="related" type="text/html" title="Consulting - A Brave New World" />
            
                <id>218</id>
            
            
            <published>2011-10-24T18:49:26-07:00</published>
            <updated>2021-01-29T19:45:23-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>One of the best things about working in the software business is the high rate of change. Not only do we work with machines that radically change in capability every few years, but new techniques and technologies are constantly appearing on the scene. It is a fascinating new frontier that never gets dull.</p>
<p>One of the worst things about working in the software business is the high rate of change. If you only focus on your job and stop paying attention, you could wake up one day unemployed with an obsolete skill set. Keeping up to date is a lot of work, and it becomes harder every year.</p>
<p>Some skills hold up better over time. Solid work habits are priceless, and can help you advance your career even where technical skills are lacking. Work management, software design, and coding practices are good subjects to study as well; they don&rsquo;t age nearly as fast as specific technologies, and they can make a big impact on the quality of your work. Unless you become a manager however, you&rsquo;re still going to need to study technical stuff.</p>
<p>The only way to really learn a tool is to use it, so study at home isn&rsquo;t always enough. Some employers will deliberately favour new technologies to keep skills fresh and employees happy, but this is not the standard. If your company starts to lag behind industry trends, you might consider changing jobs while it&rsquo;s still possible.</p>
<p>There are some tricks that help me stay up to date: I listen to podcasts while exercising or doing simple chores. Public transit is a great opportunity to read while commuting. A smart phone helps me squeeze in a few extra blogs and tweets when I&rsquo;m waiting for something. This is primarily how I follow industry trends.</p>
<p>To learn specific technologies, I&rsquo;ll spend an hour or two a few times a week reading or watching videos. I need to play with something to understand it, so I spend an odd evening banging away at some pet project.</p>
<p>My study used to be limited to an occasional book or seminar, but increasing my study time, and opening myself to podcasts, blogs, and twitter has made a huge difference in my career. I have more to contribute in design meetings. I have opinions on various technologies I haven&rsquo;t even used. It takes more energy for sure, but I feel like I am a better developer for it, and I don&rsquo;t regret it one bit.</p>
<p>This business is a challenging one, but exciting as well. If you are the kind of person that loves to learn, explore new technologies, and push yourself to constantly look at problems in new ways, then embrace the change and you will do well.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/career" term="career" label="career" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/learning" term="learning" label="learning" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/practices" term="practices" label="practices" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Managing Priorities Outside of Work]]></title>
            <link href="https://jessemcdowell.ca/2011/10/managing-priorities-outside-of-work/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2011/05/consulting-a-brave-new-world/?utm_source=atom_feed" rel="related" type="text/html" title="Consulting - A Brave New World" />
            
                <id>209</id>
            
            
            <published>2011-10-06T19:55:07-07:00</published>
            <updated>2011-10-06T19:55:07-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>I&rsquo;ve been fortunate to take some serious down time during the summer. After working pretty much non-stop for ten years, things have piled up. I took some time to rest, then got to work on some personal projects that have been neglected far too long.</p>
<p>What I&rsquo;ve learned, however, is that it takes more than time to get things done. After about a month, pretty much all I&rsquo;d achieved was completing a couple video games, and putting all the things I wanted to do into one very long list.</p>
<p>My first thought was that I wasn&rsquo;t focusing, so after listening to <a href="https://hanselminutes.com/268/personal-systems-of-organization-rey-bango-interviews-scott-hanselman">a podcast by Scott Hanselman about organization</a>, I decided to try the <a href="https://www.pomodorotechnique.com/">Pomodoro Technique</a>. This is a fairly simple system where you try to focus completely on a task for 25 minutes at a time, shutting out all distractions.</p>
<p>After a bit of Internet searching with no results, I started making my own pomodoro timer application. I had something working in a couple hours, barely holding myself back from adding sounds and animations. It would have been a lot faster, but I wasn&rsquo;t completely focused on the task.</p>
<p>Armed with my timer, ready to experience my first burst of productivity, my wife came home from work. I told her excitedly how I&rsquo;d built this magical thing, and how I&rsquo;m going to start getting so much done. Then she asks me one simple question, &ldquo;Why didn&rsquo;t you use the timer from the kitchen?&rdquo; This is when I realized that my problem was focus on a whole other level. I was doing work, but I wasn&rsquo;t doing the right work.</p>
<p>This is a common problem in the software world: getting overwhelmed by a giant list of tasks, doing a little bit of everything, and completing nothing. I&rsquo;ve experienced this first hand, and have been using effective tools for dealing with it for years, but I had never considered applying them to my personal life.</p>
<p>I tried a few things next, but the best help came from the book <a href="https://www.gettingresults.com/">Getting Results the Agile Way</a>, which was also mentioned in the podcast. It combines agile philosophy with some ideas about balancing life priorities and energy levels. I&rsquo;m not committed to the entire process, but the parts I am trying have already made a big difference. By focusing on three goals a day, I&rsquo;m now managing to plow through even dauntingly large tasks. Though I&rsquo;m not doing everything on my list, I&rsquo;m getting the most important things done, and because I feel that I&rsquo;m making progress again it&rsquo;s motivating me to work harder.</p>
<p>Taking the time off has been really good for me. Beyond organizing my personal priorities, I&rsquo;ve also been organizing my main workspaces: my office, and my kitchen. You could say the experience is like rebooting a computer; it took a bit of time, but now I feel fresh and full of life, ready to tackle something new.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/career" term="career" label="career" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Doubling Data for Performance Testing]]></title>
            <link href="https://jessemcdowell.ca/2011/08/doubling-data-for-performance-testing/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2011/01/when-to-add-an-orm-tool/?utm_source=atom_feed" rel="related" type="text/html" title="When to Add an ORM Tool" />
                <link href="https://jessemcdowell.ca/2010/12/teaching-it/?utm_source=atom_feed" rel="related" type="text/html" title="Teaching IT" />
                <link href="https://jessemcdowell.ca/2010/11/64-bit-iis-vs-32-bit-assemblies/?utm_source=atom_feed" rel="related" type="text/html" title="64-bit IIS vs. 32-bit Assemblies" />
                <link href="https://jessemcdowell.ca/2010/10/sqlite-vs-sql-ce-3-5-with-entity-framework-4/?utm_source=atom_feed" rel="related" type="text/html" title="SQLite vs. SQL CE 3.5 with Entity Framework 4" />
            
                <id>200</id>
            
            
            <published>2011-08-11T21:22:26-07:00</published>
            <updated>2021-01-29T19:45:23-08:00</updated>
            
            
            <content type="html"><![CDATA[<p><strong>Or: The Most Impressive T-SQL Script I&rsquo;ve Ever Written</strong></p>
<p>I was recently working on a new application. After three months in the field, users were starting to complain about performance issues. We had done some limited performance tuning for the first release, and more as part of the second release, but new issues were popping up as more data got entered into the system. We could have continued fixing issues as they came up, one release at a time, but we wanted to get ahead of the problem, and the client wanted to know that the system would remain usable without developer intervention for a few years at least.</p>
<p>The nature of the business was such that the rate of data entry and number of concurrent users shouldn&rsquo;t change much over time. This meant that increasing the amount of data would be a good approximation of how the system would look in the future. How do you increase the amount of data in a normalized relational database? It would be difficult to enter realistic test data manually, and unrealistic test data can cause unrealistic test results. Bad results means we waste time fixing issues that wouldn&rsquo;t appear, and never get a chance to detect issues that will.</p>
<p>I proposed creating a T-SQL script that would double all the existing data. Running it once would simulate six months of usage, running it again simulates a year, twice more and we&rsquo;re simulating four years of usage. Once it was approved, it took me a couple days to finish, about half of that going to testing and debugging. The result is the most impressive T-SQL script I&rsquo;ve ever written.</p>
<p>The script I wrote didn&rsquo;t do the actual duplication, it combined hand-entered metadata with data from sys.tables and sys.columns to generate a much bigger script. Not only was it faster to write this way, but it made it easier to fix issues, and it allowed the script to be reused after columns were added to the database.</p>
<p>I had one table variable with a list of tables to copy, and another defining the relationships between foreign keys and their source tables. Most foreign keys were named the same in all tables that they appeared in, so a single mapping was often enough for all of them. Most of the mappings could be determined from the list of tables itself, so I only had a few other relationships and special column rules that had to be entered manually.</p>
<p>Another factor that helped was our use of guids for all primary keys. Because the can be determined before inserting a row, it was possible to generate the mapping from old to new key at the start of the script. I could also use a single insert statement for each table, and the order of execution only mattered where foreign key constraints existed.</p>
<p>The results were tremendous. We found a bunch of issues we wouldn&rsquo;t have found otherwise, and had a fairly solid indication of how the application would behave years in the future.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/database" term="database" label="database" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/performance" term="performance" label="performance" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/debugging" term="debugging" label="debugging" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[The Critical Path]]></title>
            <link href="https://jessemcdowell.ca/2011/06/the-critical-path/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
            
                <id>192</id>
            
            
            <published>2011-06-25T09:58:08-07:00</published>
            <updated>2021-01-29T19:45:23-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>For the last ten years, if you had asked, I would have told you with some conviction that a &ldquo;critical path&rdquo; is a kind of test script that tests the essential functionality of a system. If the app passes it&rsquo;s critical path test, I would have said, it may not delight users, but it&rsquo;s unlikely that they&rsquo;ll storm your basement with torches and pitchforks.</p>
<p>It came up at work during a discussion about interview questions. My colleague was both proud and surprised to have been the only person interviewed for his position that had known what it really means.</p>
<p>A critical path is a project management term that refers to the chain of dependant tasks that will take the longest amount of time in a project plan. For example, if I were to make a project plan for my breakfast on Sunday morning, it would look like this:</p>
<p><img src="BreakfastOnSunday.png" alt="The plan for my Sunday morning breakfast."></p>
<p>In this plan, making pancakes is the critical path. With an unlimited amount of people, and an unlimited amount of kitchen space, and all the resources and tools we would need, the fastest that we could produce my Sunday morning breakfast is the time it takes to make pancakes.</p>
<p>I can steam milk, flip pancakes, and toss a couple eggs in the pan all at the same time, but grinding coffee beans and mixing the pancake batter has to be done one at a time. This breakfast takes me just a few minutes longer than the critical path, so it&rsquo;s not worth hiring a sous chef.</p>
<p>The time of a software projects is usually shaped more by the number of developers. We usually have a lot of features to deliver, and most of them are not directly dependant on each other. What you can see is a part at the beginning and end of a project where it&rsquo;s hard to keep multiple people working, but that&rsquo;s a whole other thing.</p>
<p><img src="GenericSoftwareProject.png" alt="A generic project plan showing a chain of blocking tasks at the beginning and end of a project."></p>
<p>When a critical path is defining the time line for your project, it&rsquo;s important to identify it; any step that gets delayed affects the entire chain. You should start these tasks as soon as possible, and do what you can to stop them from getting blocked.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/project-management" term="project-management" label="project management" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Consulting - A Brave New World]]></title>
            <link href="https://jessemcdowell.ca/2011/05/consulting-a-brave-new-world/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
            
                <id>184</id>
            
            
            <published>2011-05-23T22:20:41-07:00</published>
            <updated>2024-11-01T13:30:30-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>I&rsquo;ve taken a big step in my career. I&rsquo;ve become a software consultant.</p>
<p>The decision to leave my job of three and a half years wasn&rsquo;t an easy one. I was working with some fantastic people, building the best software we could, but at the end of the day I wasn&rsquo;t learning. The work was challenging, but the challenges were mostly non-technical. In this business, if you&rsquo;re not moving forward you may as well lay down and die. I couldn&rsquo;t change my situation, so I said my goodbyes and left.</p>
<p>Instead of settling in somewhere else for another long haul, I felt it was time for a change of pace. Now I get to hop from organization to organization working on new problems and broadening my skill set in the most efficient way possible. I might settle down if a good opportunity comes along, but for now I&rsquo;m enjoying the freedom.</p>
<p>I&rsquo;m currently working on a fun little contract, but I&rsquo;ll be looking for my next opportunity soon.</p>
<p>If you want help designing or building awesome software that delights users, we should talk. I&rsquo;d be happy to put my experience to work building you exactly what you need.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/career" term="career" label="career" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/consulting" term="consulting" label="consulting" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[The Average Test]]></title>
            <link href="https://jessemcdowell.ca/2011/04/the-average-test/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
            
                <id>177</id>
            
            
            <published>2011-04-30T16:08:30-07:00</published>
            <updated>2024-08-07T05:00:00+00:00</updated>
            
            
            <content type="html"><![CDATA[<p>I was conducting technical interviews recently for a senior developer position. I like to talk about a bunch of things, but there is one question I particularly enjoy asking each candidate. Write an implementation for this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">Average</span><span class="p">(</span><span class="kt">int</span> <span class="n">a</span><span class="p">,</span> <span class="kt">int</span> <span class="n">b</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>It&rsquo;s a great question because it&rsquo;s a simple problem, and anyone that comes through the door should know the formula without having to ask. It&rsquo;s interesting how people react to it sometimes, but I chalk it up to interview jitters. About half the people interviewed answer like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">SimpleAverage</span><span class="p">(</span><span class="kt">int</span> <span class="n">a</span><span class="p">,</span> <span class="kt">int</span> <span class="n">b</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">(</span><span class="n">a</span> <span class="p">+</span> <span class="n">b</span><span class="p">)</span> <span class="p">/</span> <span class="m">2</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>It&rsquo;s a good first answer too, except that it doesn&rsquo;t always work.</p>
<p>&ldquo;What will be the result of your function if it&rsquo;s called with the values Int32.MaxValue and Int32.MaxValue?&rdquo;</p>
<p>The answer I want to hear is that it won&rsquo;t work. Some people think (like I did), that it will throw a System.OverflowException, but it actually returns -1. The default for C# projects is to skip overflow checking for arithmetic.</p>
<p>This is another answer that I get from time to time:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">SystemMathAverage</span><span class="p">(</span><span class="kt">int</span> <span class="n">a</span><span class="p">,</span> <span class="kt">int</span> <span class="n">b</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">System</span><span class="p">.</span><span class="n">Math</span><span class="p">.</span><span class="n">Average</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>I respect this answer; it indicates that a developer wants to leave low-level details to the framework, which is a perfectly reasonable attitude when developing business-class software. Unfortunately there are two problems with this answer: it defeats the purpose of the test, and it doesn&rsquo;t exist in the framework. There is the Linq average statement which can be used like this:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">LinqAverage</span><span class="p">(</span><span class="kt">int</span> <span class="n">a</span><span class="p">,</span> <span class="kt">int</span> <span class="n">b</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kt">int</span><span class="p">[]</span> <span class="n">inputArray</span> <span class="p">=</span> <span class="k">new</span> <span class="kt">int</span><span class="p">[]</span> <span class="p">{</span> <span class="n">a</span><span class="p">,</span> <span class="n">b</span> <span class="p">};</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">(</span><span class="kt">int</span><span class="p">)</span><span class="n">inputArray</span><span class="p">.</span><span class="n">Average</span><span class="p">();</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>After the first attempt, I indicate that I want a function which will work correctly for all values of a and b. The way candidates tackle it tells you a lot about them as a developer. Here are some behaviours I&rsquo;ve noticed:</p>
<ul>
<li>Very few candidates stop to consider a solution before answering.</li>
<li>About 3/4 of candidates try to use an if statement.</li>
<li>About 1/4 of candidates write the &ldquo;if&rdquo; on the board before deciding what to use it for.</li>
<li>Occasional candidates will challenge the necessity for handling high values.</li>
</ul>
<p>A clean correct answer is hard to pull off on the spot, especially under pressure. I&rsquo;ve never seen it, and I wouldn&rsquo;t be surprised if I never do. This doesn&rsquo;t bother me either. The key thing I&rsquo;m looking for is the ability to understand questions and discuss the source code. These skills are essential to working in a team.</p>
<p>How did I come up with such a brilliant interview question? I didn&rsquo;t. Once upon a time in a company far far away, someone asked me the very same thing. I was pretty proud of my answer too, until I tested it. This was what I came up with:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">JessesAverage</span><span class="p">(</span><span class="kt">int</span> <span class="n">a</span><span class="p">,</span> <span class="kt">int</span> <span class="n">b</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">(</span><span class="n">a</span> <span class="p">/</span> <span class="m">2</span><span class="p">)</span> <span class="p">+</span> <span class="p">(</span><span class="n">b</span> <span class="p">/</span> <span class="m">2</span><span class="p">)</span> <span class="p">+</span> <span class="p">(((</span><span class="n">a</span> <span class="p">%</span> <span class="m">2</span><span class="p">)</span> <span class="p">+</span> <span class="p">(</span><span class="n">b</span> <span class="p">%</span> <span class="m">2</span><span class="p">))</span> <span class="p">/</span> <span class="m">2</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>I wrote <a href="https://gist.github.com/949410" title="TestAverageMethod()">a test harness</a> in some spare time after the last round of interviews. Here&rsquo;s how each of the answers measure up:</p>
<p>Results for SimpleAverage():
Near Zero: 121 of 121 passed.
End of Range: 0 of 3 passed.
Failed Values:</p>
<ul>
<li>SimpleAverage(2147483647, 2147483647) returns -1, but should return 2147483647</li>
<li>SimpleAverage(-2147483648, -2147483648) returns 0, but should return -2147483648</li>
<li>SimpleAverage(2147483647, 2147483645) returns -2, but should return 2147483646</li>
</ul>
<p>Results for LinqAverage():
Near Zero: 121 of 121 passed.
End of Range: 3 of 3 passed.</p>
<p>Results for JessesAverage():
Near Zero: 109 of 121 passed.
End of Range: 3 of 3 passed.
Failed Values:</p>
<ul>
<li>JessesAverage(-4, 1) returns -2, but should return -1</li>
<li>JessesAverage(-4, 3) returns -1, but should return 0</li>
<li>JessesAverage(-3, 4) returns 1, but should return 0</li>
<li>JessesAverage(-2, 1) returns -1, but should return 0</li>
<li>JessesAverage(-1, 2) returns 1, but should return 0</li>
<li>JessesAverage(-1, 4) returns 2, but should return 1</li>
<li>JessesAverage(1, -4) returns -2, but should return -1</li>
<li>JessesAverage(1, -2) returns -1, but should return 0</li>
<li>JessesAverage(2, -1) returns 1, but should return 0</li>
<li>JessesAverage(3, -4) returns -1, but should return 0</li>
<li>JessesAverage(4, -3) returns 1, but should return 0</li>
<li>JessesAverage(4, -1) returns 2, but should return 1</li>
</ul>
<p>This was the best answer I could come up with after some experimentation. I&rsquo;m not sure why, but the need for type conversion bugs me.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="cl"><span class="kt">int</span> <span class="n">ConversionAverage</span><span class="p">(</span><span class="kt">int</span> <span class="n">a</span><span class="p">,</span> <span class="kt">int</span> <span class="n">b</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">(</span><span class="n">Int32</span><span class="p">)(((</span><span class="n">Int64</span><span class="p">)</span><span class="n">a</span> <span class="p">+</span> <span class="p">(</span><span class="n">Int64</span><span class="p">)</span><span class="n">b</span><span class="p">)</span> <span class="p">/</span> <span class="m">2</span><span class="p">);</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>Results for ConversionAverage():
Near Zero: 121 of 121 passed.
End of Range: 3 of 3 passed.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/c" term="c" label="c#" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/interviewing" term="interviewing" label="interviewing" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Working Together and Having Fun]]></title>
            <link href="https://jessemcdowell.ca/2011/01/working-together-and-having-fun/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2010/12/teaching-it/?utm_source=atom_feed" rel="related" type="text/html" title="Teaching IT" />
                <link href="https://jessemcdowell.ca/2010/11/64-bit-iis-vs-32-bit-assemblies/?utm_source=atom_feed" rel="related" type="text/html" title="64-bit IIS vs. 32-bit Assemblies" />
                <link href="https://jessemcdowell.ca/2010/09/ie9-try-and-fail/?utm_source=atom_feed" rel="related" type="text/html" title="IE9 Try and Fail" />
                <link href="https://jessemcdowell.ca/2010/08/ssmse-with-vs2010-express/?utm_source=atom_feed" rel="related" type="text/html" title="SQL Management Studio Express with VS 2010 Express" />
            
                <id>172</id>
            
            
            <published>2011-01-30T11:46:45-08:00</published>
            <updated>2024-10-29T00:00:00-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>We did one of our monthly releases at work this week. Releases can be stressful and frustrating, and take a lot of methodical preparation to get right. It can be thankless work too; the only time a user notices a release is when it goes badly. We do our releases early on a week day to minimize impact, so if anything does go wrong, there&rsquo;s not many bodies around to help out. It&rsquo;s not much fun, but it&rsquo;s important work that needs to be done.</p>
<p>One thing that makes the experience considerably more enjoyable for me is the team of coworkers that come in to help. There&rsquo;s a handful of us, each with our own responsibilities. I deploy the applications while someone else monitors the database and another person tests the system to make sure it&rsquo;s running normally. We back each other up, help out where we can, and make decisions together when they need to be made quickly.</p>
<p>Pressure is never a desirable thing in a work environment, but one benefit is that it quickly builds trust between anyone facing it together. The people I work with are really fantastic: smart, dedicated, and fun. We come from different cultures, like different kinds of food, have different hobbies and different tastes in music, but we still find things to talk about, reasons to laugh and smile.</p>
<p>I&rsquo;ve been trying ways to make releases more fun. We had a pot luck breakfast once, and a release soundtrack from team favourites another time. This time I made breakfast burritos, and as a joke, a doughnut salad topped with an espresso sauce. We played a few songs during the waiting periods, and had as much fun as anyone could that early in the morning.</p>
<p><img src="DoughnutSalad.jpg" alt="A plate of chopped doughnuts topped with an espresso jelly."></p>
<p>Thanks to the people involved, this release went well despite a few hiccups.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/devops" term="devops" label="devops" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/food" term="food" label="food" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/leadership" term="leadership" label="leadership" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/people-management" term="people-management" label="people management" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[When to Add an ORM Tool]]></title>
            <link href="https://jessemcdowell.ca/2011/01/when-to-add-an-orm-tool/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2010/10/sqlite-vs-sql-ce-3-5-with-entity-framework-4/?utm_source=atom_feed" rel="related" type="text/html" title="SQLite vs. SQL CE 3.5 with Entity Framework 4" />
                <link href="https://jessemcdowell.ca/2010/12/invitations-and-the-vcard-format/?utm_source=atom_feed" rel="related" type="text/html" title="Invitations and the VCard Format" />
                <link href="https://jessemcdowell.ca/2010/11/64-bit-iis-vs-32-bit-assemblies/?utm_source=atom_feed" rel="related" type="text/html" title="64-bit IIS vs. 32-bit Assemblies" />
                <link href="https://jessemcdowell.ca/2010/09/themis-system-design/?utm_source=atom_feed" rel="related" type="text/html" title="Themis: System Design" />
                <link href="https://jessemcdowell.ca/2010/08/shared-resource-service-requirements/?utm_source=atom_feed" rel="related" type="text/html" title="Shared Resource Service Requirements" />
            
                <id>167</id>
            
            
            <published>2011-01-03T12:33:03-08:00</published>
            <updated>2011-01-03T12:33:03-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>I&rsquo;m working on the code that parses VCalendar data so that it can be processed. I&rsquo;m copying the data I care about into a simple data structure that can represent a calendar request in any format. Any logic that interacts with calendar requests would use this internal structure. I want it to be simple, only having the stuff that I need, but I don&rsquo;t want to completely re-invent the wheel either, so I will use the VCalendar format as a guideline.</p>
<p>I&rsquo;ve used this pattern of processing an established or complex data format into a simple proprietary one before with good results. It allows you to isolate the complexities of a data format in the code that processes it, keeping the rest of your code clean. Like everything, there are cases where I wouldn&rsquo;t use it, but for this kind of scenario it should work well.</p>
<p>Now that I&rsquo;m building this neutral representation, my next thought is how it will be saved to the database. I don&rsquo;t need a database at this stage of the project, so part of me wants to ignore the decision until later, but the whole point of the system is to persist and interact with these types, so it&rsquo;s important that I get it right. If I make simple POCO classes now, and start writing a bunch of code that uses them, I might have to change a lot of code later if I want to switch to types generated by Entity Framework. I could write my own custom code to read and write my own types from the database, then I can use any type I want without restrictions, but it would be a waste to write this plumbing code myself when an ORM can do it faster and better.</p>
<p>Creating a table design now wouldn&rsquo;t be a simple matter either. I don&rsquo;t know enough about the needs of the scheduling service to know which parts of the VCalendar format to bring over. If I try to guess now I know I&rsquo;ll bring over a bunch of stuff that I don&rsquo;t need, but starting with a simple table and adding fields every time I need one is no fun either. Adding a database to a system is like attaching a ball and chain, and I want to wait until I can be sure I have my model correct before I do it.</p>
<p>I need to make a decision, so here it is: I&rsquo;m going to keep building my own types, and try to keep them. When it comes time to hook up a database, I&rsquo;ll play around with the new POCO support in Entity Framework 4, and if that fails I&rsquo;ll try another ORM tool. I may need to change my model a bit to suit the ORM, but I&rsquo;m hoping that it wont need to change much.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/database" term="database" label="database" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/design" term="design" label="design" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/entity-framework" term="entity-framework" label="entity framework" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/.net" term=".net" label=".net" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Teaching IT]]></title>
            <link href="https://jessemcdowell.ca/2010/12/teaching-it/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2010/11/64-bit-iis-vs-32-bit-assemblies/?utm_source=atom_feed" rel="related" type="text/html" title="64-bit IIS vs. 32-bit Assemblies" />
                <link href="https://jessemcdowell.ca/2010/09/ie9-try-and-fail/?utm_source=atom_feed" rel="related" type="text/html" title="IE9 Try and Fail" />
                <link href="https://jessemcdowell.ca/2010/08/ssmse-with-vs2010-express/?utm_source=atom_feed" rel="related" type="text/html" title="SQL Management Studio Express with VS 2010 Express" />
            
                <id>163</id>
            
            
            <published>2010-12-24T22:28:12-08:00</published>
            <updated>2010-12-24T22:28:12-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>My company runs a 24/7 site with a substantial number of users and connections to partner systems all over the world. We do what we can to make the system fault tolerant, but problems can still appear at any time of day or night. Ideally we would have a technical support team that&rsquo;s staffed around the clock, but that not in the cards for now.</p>
<p>We used to have a system where anyone that could fix a problem would get a text message, and whoever was closest to a computer would respond. It worked most of the time, but there were some issues. The team creating the tickets would often be unsure of which component was broken, so the issues would hit lots of people who would be unable to help, and it also exposes <a href="https://en.wikipedia.org/wiki/Diffusion_of_responsibility">a quirk of human behaviour</a> that makes us less likely to help when more people are available.</p>
<p>Thankfully the whole plan is being reworked. Now a single volunteer will carry a phone that gets all the support tickets. They will respond to issue within a reasonable amount of time, and if they can&rsquo;t solve them themselves, find other people to help. The company has also added decent incentives to be on call, and is investing in tools to make the job easier.</p>
<p>As part of the preparation, I&rsquo;ve been asked to do presentations and create supporting documentation for all of the first responders. I enjoy doing the presentations, and everyone seems to be benefiting from the content, but it&rsquo;s a lot more work than you might expect. The group of people I&rsquo;m working with is comprised mostly of heads-down developers. They&rsquo;re smart people, and all wanting to learn, but most don&rsquo;t have any IT experience.</p>
<p>My first session covered these topics:</p>
<ul>
<li>
<p>how to think and act like a seasoned IT pro</p>
</li>
<li>
<p>a brief description of our hosting environment, servers , and where the various applications are deployed</p>
</li>
<li>
<p>how to recognize network problems, hardware problems, and OS problems</p>
</li>
<li>
<p>the symptoms to expect when any of our key systems fail</p>
</li>
<li>
<p>how to safely restart our key systems</p>
</li>
</ul>
<p>It took me about an hour and a half to prepare, and two hours to present. I&rsquo;ve spent about a day of effort writing it down, but would guess that I&rsquo;ve covered less than half of the material I talked about. To be fair, the written material is laid out quite differently from the presentations; I am trying to capture a series of troubleshooting guides that can be used in an emergency.</p>
<p>Are you curious about the difference between a developer and an IT pro? The most significant difference I see is that we developers are used to an environment where we can test changes quickly and safely: change a line of code, hit F5, and see what happens. IT pros take the opposite approach because the cost of a bad change in a live production environment can be devastating. They need to be certain what any change will do before it&rsquo;s made, and be prepared to roll it back if there&rsquo;s any hint of trouble. It can be frustrating for a developer to work this way because it&rsquo;s a serious effort to make even simple changes, but it&rsquo;s this mentality that keeps the world&rsquo;s servers running.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/devops" term="devops" label="devops" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/debugging" term="debugging" label="debugging" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/people-management" term="people-management" label="people management" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Invitations and the VCard Format]]></title>
            <link href="https://jessemcdowell.ca/2010/12/invitations-and-the-vcard-format/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2010/09/themis-system-design/?utm_source=atom_feed" rel="related" type="text/html" title="Themis: System Design" />
                <link href="https://jessemcdowell.ca/2010/08/shared-resource-service-requirements/?utm_source=atom_feed" rel="related" type="text/html" title="Shared Resource Service Requirements" />
            
                <id>153</id>
            
            
            <published>2010-12-16T12:59:22-08:00</published>
            <updated>2021-01-29T19:45:23-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>My next goal for the Themis project is to parse an invitation from an email. I am starting with invitations generated by MS Outlook because that&rsquo;s my target audience, but a peak inside of a Google Calendar invitation gives me hope that I&rsquo;ll be able to support multiple calendars without much trouble.</p>
<p>Outlook invitations are sent in the VCalendar format, content type &ldquo;text/calendar&rdquo;. The standard was published as <a href="https://www.w3.org/2002/12/cal/rfc2445.html">RFC 2445</a> in 1998. It describes a standard layout for calendar data in the VCard format, which is described in <a href="https://www.w3.org/2002/12/cal/rfc2425.html">RFC 2425</a>.</p>
<p>VCard is a simple text based format with a nested structure that&rsquo;s similar to XML. It uses colons and semicolons instead of angle brackets, supports attributes, and has standard representations for several common data types. Here is an example of the same data represented in both formats (content from RFC 2425):</p>
<p><img src="vcard-vs-xml.png" alt="An example of VCard data beside an XML representation of the same data."></p>
<p>I looked around for a library to do the job,but decided to build my own. Writing this kind of code isn&rsquo;t a good way to deliver business value, but it sure is fun. A bit of time with a white board and a couple of evenings coding, and I have a mostly functional VCard parser ready to go.</p>
<p>When I am setting out to build something like a parser, I like to consider the designs of other established libraries that perform a similar function. As I&rsquo;ve already mentioned, VCard is a lot like XML in structure, so XML parsers are a great place to look for inspiration. I built a structure that loosely mimics the XmlDocument model, an object tree that holds the entire document in memory. It won&rsquo;t perform as fast as using something like an XmlReader, but it makes it easier to handle variations in document format with polymorphism. Since the VCard documents I&rsquo;m processing should never be too big, it won&rsquo;t be much of a performance burden anyway.</p>
<p>The object structure that holds VCard data looks like this:</p>
<p><img src="VCardObjectModel1.jpg" alt="A UML diagram showing the class structure for holding VCard data."></p>
<p>At first I wanted to have a property like Value, but there is no reliable way to know the type of a VCard value without knowing the structure of the document, and at this level I don&rsquo;t want to be coupled to the structure of the document. Instead, I will be adding a series of methods to get and set the EscapedValue as a specific type.</p>
<p>The VCardSimpleValue class was a late addition to the model. I needed a way to hold parameter values (the equivalent of attributes in XML), but since they can&rsquo;t have parameters of their own I didn&rsquo;t want to pick up the Parameters collection. I also considered making a type separate from VCardValue for the parameters, but both these classes will need the ability to read and write the escaped value, and I don&rsquo;t want to write that code twice.</p>
<p>I&rsquo;ve added the first unit tests to the solution, checking the parser against a number of examples in the specifications and real snippets extracted from emails sent by Outlook. I&rsquo;ve also added tests for a number of failure cases in the parser such as groups without an ending or values lines without a value delimiter.</p>
<p>My next addition will be parsers for the value types and a stub in the test harness that replies to emails with some info about the original request.</p>
<p>The code at the time of this post is available here: <a href="https://github.com/jessemcdowell/themis/tree/8e0c428df0e33145df0323d6e9ae617114dd0f85">https://github.com/jessemcdowell/themis/tree/8e0c428df0e33145df0323d6e9ae617114dd0f85</a></p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/design" term="design" label="design" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[64-bit IIS vs. 32-bit Assemblies]]></title>
            <link href="https://jessemcdowell.ca/2010/11/64-bit-iis-vs-32-bit-assemblies/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2010/10/sqlite-vs-sql-ce-3-5-with-entity-framework-4/?utm_source=atom_feed" rel="related" type="text/html" title="SQLite vs. SQL CE 3.5 with Entity Framework 4" />
                <link href="https://jessemcdowell.ca/2010/09/ie9-try-and-fail/?utm_source=atom_feed" rel="related" type="text/html" title="IE9 Try and Fail" />
                <link href="https://jessemcdowell.ca/2010/08/ssmse-with-vs2010-express/?utm_source=atom_feed" rel="related" type="text/html" title="SQL Management Studio Express with VS 2010 Express" />
            
                <id>136</id>
            
            
            <published>2010-11-11T16:45:38-08:00</published>
            <updated>2021-01-29T19:45:23-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>I found my first 64-bit bug at work. I was moving a windows service built for the &lsquo;Any CPU&rsquo; to a 64-bit server. It started fine on the new server, and gave no indication of poor health in the logs, but one key function was malfunctioning. I&rsquo;m not exactly sure what the cause is, but I know that the hash of any binary file was resulting in the same value. The service does some direct memory manipulation which is a likely culprit.</p>
<p>Fixing the problem was easy: Set all the assemblies to compile for the 32-bit platform, check it in, build it, ship it.</p>
<p>Except that my web service now looks like this:</p>
<p><em>Server Error in &lsquo;/MyApp&rsquo; Application.</em></p>
<p><em>Could not load file or assembly &lsquo;MyCompany.MyApp.MyAssembly&rsquo; or one of its dependencies. An attempt was made to load a program with an incorrect format.</em></p>
<p>This application has two parts on the server: a windows service that processes emails, and a web service for interacting with them. The two parts share a database and a bunch of business logic, so they also share some assemblies. The windows service works great now that I&rsquo;ve converted all the assemblies to 32-bit, but the web service wont start.</p>
<p>Although the error message is a bit misleading, it wasn&rsquo;t that hard to find the problem. When IIS is running in 64-bit mode (as it tends to do on 64-bit servers), all of its application pools will be 64-bit, and a 64-bit application pool cant load a 32 bit assembly. These are all the ways I can think of to fix it:</p>
<ul>
<li>IIS 7 can run an application pool in 32-bit mode</li>
<li>IIS 6 can be configured to run in 32-bit mode, or you can run a second instance in 32-bit mode, though I don&rsquo;t imagine both instances could share the same ports</li>
<li>Write 32-bit and 64-bit versions for any platform-specific operations using conditional compilation, then build two versions of your assemblies</li>
<li>Rewrite the malfunctioning parts of your app so you don&rsquo;t have any platform-specific code</li>
<li>Compile most of your assemblies for &lsquo;Any CPU&rsquo;, but compile entry-point assemblies (such as my service executable) for 32-bit</li>
</ul>
<p>A configuration-based solution isn&rsquo;t ideal based on the number and management practices of our environments. Finding and removing platform dependence would be the best technical solution, but it would be risky and time consuming. Switching some of my assemblies back to &lsquo;Any CPU&rsquo; did the job with a minimal amount of impact.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/.net" term=".net" label=".net" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/devops" term="devops" label="devops" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/debugging" term="debugging" label="debugging" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/web" term="web" label="web" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[SQLite vs. SQL CE 3.5 with Entity Framework 4]]></title>
            <link href="https://jessemcdowell.ca/2010/10/sqlite-vs-sql-ce-3-5-with-entity-framework-4/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
            
                <id>123</id>
            
            
            <published>2010-10-31T22:19:23-07:00</published>
            <updated>2024-11-01T13:30:30-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>My wife and I have been taking a Japanese class. I&rsquo;m enjoying it quite a bit, even though learning languages has never been easy for me. I need to work hard to memorize all the new words, and that means lots and lots of practice.</p>
<p>My wife bought a pack of index cards. This is what a normal person would do. To me, it seems like defeat. Why use paper when there&rsquo;s a computer in the room? While she&rsquo;s at the store, I grab a white board and sketch out a data model.</p>
<p>I used WPF for a UI, two simple view models, and Entity Framework 4 to store the lists of characters, words, and phrases. I started out with SQL Compact Edition 3.5 as a database because that was the one that was there. That didn&rsquo;t work out so well, so on the advice of a colleague I switched my app to SQLite.</p>
<p>This is what I found:</p>
<p>SQL CE 3.5</p>
<p>SQLite</p>
<p>Pros</p>
<ul>
<li>
<p>Can be created easily from the VS designer</p>
</li>
<li>
<p>Can view and modify data inside Visual Studio</p>
</li>
<li>
<p>Supports Identity Columns</p>
</li>
<li>
<p>Easy to deploy</p>
</li>
<li>
<p>Lots of good tools available for managing databases</p>
</li>
<li>
<p>Can be used from non .Net and even non Windows applications</p>
</li>
</ul>
<p>Cons</p>
<ul>
<li>
<p>Doesn&rsquo;t support Identity Columns in EF</p>
</li>
<li>
<p>Requires extra installation on client computers</p>
</li>
<li>
<p>Generating tables from a model is painful</p>
</li>
<li>
<p>Many model changes require regenerating the database</p>
</li>
<li>
<p>Doesn&rsquo;t map Integer type correctly</p>
</li>
<li>
<p>Doesn&rsquo;t support foreign key constraints</p>
</li>
<li>
<p>Generating tables from a model is somewhat painful</p>
</li>
</ul>
<h1 id="sql-compact-edition-35">SQL Compact Edition 3.5</h1>
<p>Before I start dumping on SQL CE, I should say that I was trying the 3.5 version, not the fancy new 4.0 version. Maybe they&rsquo;ve fixed all these issues.</p>
<p>Creating the model was easy, but as I went through a few iterations of the data model I quickly became frustrated with the inability to change the database to match the new model. I&rsquo;m sure it&rsquo;s possible to change it, but you have to write the scripts yourself and hope that you&rsquo;ve named everything exactly right. Entity Framework created a handy database-generation script, but it won&rsquo;t run directly on as SQL CE database because it doesn&rsquo;t support the GO statement. Copy and pasting each section of the script is annoying, but still better than typing all the names manually.</p>
<p>The real shock was adding data from inside my app. Entity Framework can&rsquo;t generate data in a SQL CE database if the primary key is an identity. The best workaround I could find was to use a regular old integer for a primary key and search the model for the highest key value every time an entity is created. Not fun at all.</p>
<h1 id="sqlite">SQLite</h1>
<p>It took a bit of fooling around to switch my database to SQLite. Most of the creation script ran on the database, though the types had to be changed a bit to match the database. All integers in SQLite are actually Int64 in .Net, but the Entity Framework maps them to Int32 in the model. Modifying the mapping files manually is annoying, but not hard to figure out.</p>
<p>Once the tables were set up and working, it was easy to copy the data over. The database tool I&rsquo;m using for SQLite can import CSV files, even the multi-byte Japanese characters came across correctly.</p>
<p>The only thing left is to study.</p>
<p><del>You can get a .Net library for SQLite here: <code>https://sqlite.phxsoftware.com/</code></del></p>
<p>It&rsquo;s able to integrate with Visual Studio, even the Express version, but it didn&rsquo;t work well for me. With a good database management tool you don&rsquo;t need it. I&rsquo;ve been using SQLite Administrator, which you can get here: <a href="https://sqliteadmin.orbmu2k.de/">https://sqliteadmin.orbmu2k.de/</a></p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/database" term="database" label="database" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/entity-framework" term="entity-framework" label="entity framework" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/.net" term=".net" label=".net" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[IE9 Try and Fail]]></title>
            <link href="https://jessemcdowell.ca/2010/09/ie9-try-and-fail/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2010/08/ssmse-with-vs2010-express/?utm_source=atom_feed" rel="related" type="text/html" title="SQL Management Studio Express with VS 2010 Express" />
            
                <id>110</id>
            
            
            <published>2010-09-22T20:07:53-07:00</published>
            <updated>2021-01-29T19:45:23-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>I decided to give IE9 a try. What&rsquo;s not to like about a browser that&rsquo;s supposed to be faster, cleaner, and support new HTML features?</p>
<p>They really nailed the faster and cleaner parts. I didn&rsquo;t find the user interface in IE8 too busy, but seeing the improvement I&rsquo;m happy for the extra space.</p>
<p>Plunking a site onto my task bar like it were an application is pretty awesome. There are a couple of websites that I&rsquo;ll do this for without hesitation.I wonder how long it&rsquo;ll take to see adoption of the magic html jump list task extensions. I&rsquo;m not terribly optimistic myself; I&rsquo;m hoping someone will find a way to add tasks and maybe an overlay to sites that don&rsquo;t have them built in.</p>
<p>In spite of all of that, sadly, I had to remove it.</p>
<p>The most severe issue I found was with the Live Mesh Remote Desktop service. My mouse cursor kept flicking to the the busy cursors and back again a few times a second no matter what application I was using.Also, myCPU was running at about 26% where it normally hovers closer to 2%. Closing the browser doesn&rsquo;t fix it either.</p>
<p>It took a bit of digging around in Resource Monitor, but I was able to find the problem. It wasn&rsquo;t a single app running the CPU, it was dozens of processes starting and terminating every second. That explains the mouse cursor. The two apps in question were the Windows error reporting service and the Live Mesh Remote Desktop service.</p>
<p>The solution was easy: set the Mesh Remove Desktop service to disabled. You won&rsquo;t be able to access the desktop, but using everything else won&rsquo;t suck. I don&rsquo;t use Mesh desktop sharing enough that it&rsquo;s a problem for me, but anyone who does may want to try it&rsquo;s new replacement.</p>
<p>I could have lived with the disabled desktop sharing if it were the only problem, but a few hideous rendering problems in Google Calendar, and WordPress failing to save my posts was a deal breaker. The compatibility mode doesn&rsquo;t seem to be compatible enough, and tweaking the settings in the developer console (F12) is too much of a nuisance to do every day.</p>
<p>IE9 came off pretty easy once I figured out how to do it. It&rsquo;s listed as an update, not as an application, so you need to click on the the &ldquo;View installed updates&rdquo; thingy of thePrograms and Features menu.</p>
<p>I hope they get the bugs worked out soon; it&rsquo;s only been a day and I&rsquo;m starting to miss it already.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/devops" term="devops" label="devops" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Themis: System Design]]></title>
            <link href="https://jessemcdowell.ca/2010/09/themis-system-design/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2010/08/shared-resource-service-requirements/?utm_source=atom_feed" rel="related" type="text/html" title="Shared Resource Service Requirements" />
            
                <id>79</id>
            
            
            <published>2010-09-14T22:29:40-07:00</published>
            <updated>2010-09-14T22:29:40-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>I&rsquo;m charging forward on the Shared Resource Schedule Service. There are a lot of things I&rsquo;m getting into place before I start writing code. Don&rsquo;t try to tell me that I&rsquo;m not being agile either, you always need to design a little to write good code. The key to being agile is designing only as much as you need, and being prepared to change your mind later.</p>
<h1 id="the-name">The Name</h1>
<p>The first order of business was to choose a name. It helps to know the name before you create a project. I&rsquo;m also hoping to put the source in a public repository right away so that I can talk about the coding process while I build it.</p>
<p>Themis comes from Greek mythology. <a href="https://en.wikipedia.org/wiki/Themis">According toWikipedia</a>, she &ldquo;is the embodiment of divine order, law, and custom.&rdquo; That seems somewhat applicable to the subject at hand. I know thatGreek mythology isn&rsquo;t too creative, but we can always change the name later if anyone has a better idea.</p>
<h1 id="application-structure">Application Structure</h1>
<p><img src="Themis-Application-Architecture.jpg" alt="A diagram showing the two main applications and the database that they both reference."></p>
<p>This system should be very simple. It&rsquo;ll have a windows service to check for invitation emails, a web page to view schedules and configure the service, and a database to hold all the information so that both apps can access it. For simplicity, I want to put most of the code into a single assembly.I want to the web UI and the service runner to be a thin veneer over the main assembly.</p>
<h1 id="application-design">Application Design</h1>
<p>Although this is just a home project I do want it to be good quality, even if my name weren&rsquo;t going to be pasted into every source file with the license. To achieve this, I will be using unit tests in as many areas as I can add them; not only to catch my mistakes but to help others understand and maintain the system after I&rsquo;m gone. To make unit testing easier, I want to use an<a href="https://en.wikipedia.org/wiki/Dependency_injection">IoC / DI</a>coding style.</p>
<p>I&rsquo;m going to use Entity Framework to keep the application code ignorant about the database. I don&rsquo;t want to be tied to a particular database system, though I suspect most installations would use a sqlce database. I could use any ORM, but I&rsquo;ve been wanting to try EF.</p>
<h1 id="processing-mail">Processing Mail</h1>
<p>Now that enough of the design (and some of the non-coding elements) have been straightened out, I can start to really focus on the first part of the project:Processing email.</p>
<p><img src="Themis-Mail-Processing.jpg" alt="A sequence diagram showing the mail processing sequence."></p>
<p>I want to build the email handling logic as a group of small components,each responsible for a single aspect of the process. Aside from some shared types to hold email or request data, and the controller that knows how to orchestrate them, none of the components will know anything about each other. Even then the controller won&rsquo;t know which types contain the actual implementation thanks to the IoC container.</p>
<p>By building each part in isolation I get a few good advantages. First, I&rsquo;m able to test each component completely and separately from the rest of the system. Second, it becomes possible to replace any part of the chain with nothing more than a configuration change, which will be especially handy if anyone wants to fork the code. Third, I can reuse the individual components for other parts of the system, and they are likely to require less refactoring before they can be reused.</p>
<p>This is the new coding style: you build a system with complimentary building blocks loosely hooked together. You end up with a lot of classes, but you avoid the pain of complex inheritance trees.</p>
<h1 id="the-next-step">The Next Step</h1>
<p>Now that I&rsquo;ve figured out my approach, I see that my first step is too big for an afternoon. Instead, I&rsquo;m going to tackle it in three smaller steps:</p>
<ol>
<li>Receive any email and send a simple response back.</li>
<li>Receive event invitations by email and send a human-readable response listing the details of the invitation.</li>
<li>Receive event invitations by email and send an acceptance email. (The original requirement.)</li>
</ol>
<p>All I have left is to make a good cup of tea, turn up the music,and bang some keys.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/design" term="design" label="design" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/ioc" term="ioc" label="ioc" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/unit-testing" term="unit-testing" label="unit testing" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[My First Open Source Project]]></title>
            <link href="https://jessemcdowell.ca/2010/09/my-first-open-source-project/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
                <link href="https://jessemcdowell.ca/2010/07/my-first-post/?utm_source=atom_feed" rel="related" type="text/html" title="My First Post" />
            
                <id>53</id>
            
            
            <published>2010-09-04T19:52:29-07:00</published>
            <updated>2021-01-29T19:45:23-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>I&rsquo;ve been spending time on the first part of the shared resource service project: &ldquo;Receive event invitations by email and send an acceptance email&rdquo;. I hadn&rsquo;t considered the difficulty I would face receiving emails. This is the most important part of the application, and if I can&rsquo;t make it work, there&rsquo;s really no point to continue.</p>
<p>I don&rsquo;t want to write my own POP3 library, I&rsquo;ve done that before and it&rsquo;s much more complicated than it appears. Buying a commercial component isn&rsquo;t going to happen either. That left me with one option: To scour the Internet for an open source library.</p>
<p>Most of my career has been spent in companies that are paranoid about using third-party code. Finding something was easy enough (though I must say there are surprisingly few .Net POP3 libraries around), and testing a couple libraries didn&rsquo;t take long at all. The most difficult part was figuring out how to use an open source library properly.</p>
<p>My favourite of the contenders is released under the GNU LGPL license. This shouldn&rsquo;t impose any awkward restrictions. I have additional freedom because I&rsquo;m planning to release my project as open source, but I can see how this would be more difficult for a company building a commercial product.</p>
<p>Since I was reading about licenses, it seemed as good a time as any to pick one for my project. After a lot of reading, I decided to go with the GNU GPL license. I want my project to be freely available, and I don&rsquo;t want anyone to use it to make a profit without me being involved. If I were unsure this would be the safest choice anyway, because I always have the ability to switch to a less restrictive license later.</p>
<p>Now if I can just find an IoC container, a unit testing framework, and a mocking framework that are also compatible with GPL, Ill be ready to get started.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/blogging" term="blogging" label="blogging" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/open-source" term="open-source" label="open source" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Changing From an iPod to a Creative Zen]]></title>
            <link href="https://jessemcdowell.ca/2010/08/changing-from-an-ipod-to-a-creative-zen/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
            
                <id>41</id>
            
            
            <published>2010-08-31T14:22:15-07:00</published>
            <updated>2024-11-01T13:30:30-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>The time finally came for me to replace my iPod with something new. I was using an<a href="https://support.apple.com/kb/HT1353#clickwheel">iPod click wheel</a>that I got as a warranty replacement back in 2004.It was good to me, but I felt that it was time to look at other options.If iTunes ran a little better on my PC, I probably would have just bought another iPod without a second thought.</p>
<h2 id="hardware">Hardware</h2>
<p>It took me quite a while to choose a new device. I did look at the iPod Nano, but they couldn&rsquo;t store quite as much as I wanted.The iPod classic on the other hand is way more storage thanI need, and I prefer the lighter weight, longer battery life, and the improved durability of flash storage.</p>
<p>The Microsoft Zune is another device I considered, but the windows software for that is even worse than iTunes. For example,songs can have one of three ratings: heart, broken heart, or no rating. I like that they&rsquo;re trying to simplify the interface, but I really feel like I&rsquo;m not their target audience.That said, if Zune Pass worked in Canada I might have gone with them anyway.</p>
<p>The Creative Zen X-Fi gave me everything I wanted. It&rsquo;s small and light, has 32GB of flash storage and can be easily expanded, and it&rsquo;s priced competitively. It has an FM radio, and buttons on the outside. I know touch screen is in these days, but I can change songs without even taking it out of my pocket.</p>
<h2 id="software">Software</h2>
<p>Replacing iTunes was another big job. I&rsquo;ve built up a detailed system of smart play lists and ratings over the years and change scares me. I&rsquo;m willing to try something new, but I need an app with enough features to do the job.</p>
<p>My search was quickly reduced to two main contenders, <a href="https://www.winamp.com/">Winamp</a> and <a href="https://www.mediamonkey.com/">Media Monkey</a>. Both had automatic play lists, song ratings, and file type conversion features.</p>
<p>I ended up downloading both and doing several tests before choosing Media Monkey. Both applications were comparable for features, but Media Monkey was a little better with syncing, and a little more straightforward for organising songs and play lists.</p>
<p>Converting to Media Monkey was easy, and all the data (like ratings, play counts, album art) came over fine. I did have some trouble with the comments getting scrambled in several tracks, but it seems like iTunes was causing that well before copying the data over. All told, I had all the data over and fixed up after a half day of elbow grease.</p>
<h2 id="verdict">Verdict</h2>
<p>My only real problem so far has been with the Zen. I&rsquo;ve had to reboot the device a couple times when it&rsquo;s hiccupped during a huge sync operation or failed to come back out of sleep.The UI can be a bit difficult sometimes as well. I won&rsquo;t get into details, but let me just say that converting a WEP key to stars while typing with 9 buttons isn&rsquo;t making my life any easier.</p>
<p>All in all, I&rsquo;m pretty happy with the change so far. Just getting the weight of my old 20 GB monster out of my pocket has been nice.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/gadgets" term="gadgets" label="gadgets" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[SQL Management Studio Express with VS 2010 Express]]></title>
            <link href="https://jessemcdowell.ca/2010/08/ssmse-with-vs2010-express/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
            
                <id>34</id>
            
            
            <published>2010-08-10T20:16:53-07:00</published>
            <updated>2021-01-29T19:45:23-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>I had some fun trying to install SQL Management Studio on my home computer over the weekend. I was successful in the end, but it was a rough journey.</p>
<p>These seem to be the things that can lead you into my scenario:</p>
<ul>
<li>Windows 7</li>
<li>Visual Web Developer 2010 Express (which installs SQL Server 2008 Express)</li>
</ul>
<p>These are the things I ran into trying to install SQL Management Studio (both from an SQL 2008 standard disk, and a download of the express version from the Microsoft web site):</p>
<ul>
<li>A warning that the installer was not compatible with Windows 7. The warning leads you to the SQL 2008 SP1 installer, but that can&rsquo;t install management studio.</li>
<li>The installer fails to load with this error: &ldquo;Invoke or BeginInvoke cannot be called on a control until the window handle has been created.&quot;(If you keep retrying you can eventually get past this error, but it seems to be possible at three different stage of the install process.)</li>
<li>A reboot is required before proceeding, even when running the installer immediately after a reboot.</li>
<li>When given a list of modules to add, SQL Management Studio isn&rsquo;t an option on the list.</li>
</ul>
<p>What a frustrating evening!</p>
<p>The thing that finally worked turned out to be pretty simple though:</p>
<ol>
<li>Launch the Microsoft Web Platform Installer (which you need to install to have Visual Web Developer 2010 Express, as far as I know).</li>
<li>Go to the Web Platform tab, and click &ldquo;Customize&rdquo; on the Database option.</li>
<li>Check &ldquo;SQL Server 2008 R2 Management Studio Express&rdquo;, and click Install.</li>
<li>Stand back and let the web platform installer do it&rsquo;s thing.</li>
</ol>
<p>I wish I&rsquo;d tried that the first time.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/sql" term="sql" label="sql" />
                             
                                <category scheme="https://jessemcdowell.ca/tags/devops" term="devops" label="devops" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Shared Resource Service Requirements]]></title>
            <link href="https://jessemcdowell.ca/2010/08/shared-resource-service-requirements/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
            
                <id>22</id>
            
            
            <published>2010-08-08T21:12:53-07:00</published>
            <updated>2021-01-29T19:45:23-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>I want to start my project by dividing up the various things the system needs to do, then put them into the order I plan to approach them. I want to start with requirements that are the most risky and most important to success first, and work my way down the list until I finish with items that are neither risky nor important.</p>
<p>This approach gives me several advantages. It&rsquo;s not an issue in this case since I don&rsquo;t have any deadlines to meet, but if this were a professional project and a deadline had to be pushed up, I am more likely to have a product that does the most essential things. If I had a fixed deadline and the project starts running long, I&rsquo;m more likely to have a working product that&rsquo;s just missing a few less important features. In an ideal world those don&rsquo;t happen, but at the end of the project the parts that were risky and/or essential have had the most time to be tested and fine-tuned as the application matures.</p>
<p>I could just charge in and write some code, and sometimes the results can turn out okay, but in my experience it never saves time. If anything, it just increases the chance that the project will fail. On the other hand, I don&rsquo;t want to do too much design up front. I will know more and more about the problem space the further I get into the project, so the further back I can push parts of the design, the more likely I am to get them right, or at least be close,the first time.</p>
<p>After a couple days of thinking and fooling around with the list, here is what I end up with in the general order I plan to approach them:</p>
<ol>
<li>Receive event invitations by email and send an acceptance email.</li>
<li>Record accepted invitations, and send a rejection email when a new invitation conflicts with an existing one.</li>
<li>If an event is cancelled, remove it from the record.</li>
<li>If an event is rescheduled, try to accept the new time and remove the previous time.</li>
<li>Receive invitations for multiple resources.</li>
<li>Users can view the schedule for resources.</li>
<li>Administrators can view, add, edit, and remove resources or change other options for the service.</li>
<li>Save free/busy information for all resources in a format compatible with MS Outlook.</li>
<li>The schedule for a resource can be retrieved in the iCal format.</li>
</ol>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/design" term="design" label="design" />
                            
                        
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[Shared Resource Schedule Service]]></title>
            <link href="https://jessemcdowell.ca/2010/07/shared-resource-schedule-service/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
            
                <id>13</id>
            
            
            <published>2010-07-28T21:53:02-07:00</published>
            <updated>2021-01-29T19:45:23-08:00</updated>
            
            
            <content type="html"><![CDATA[<p>I think I&rsquo;ve got a new project to work on at home. We&rsquo;ve got this problem at work coordinating a few shared resources in Outlook (like a couple of meeting rooms and a projector), but we couldn&rsquo;t find any good solutions out there short of installing Exchange Server.</p>
<p>I&rsquo;ve been wanting to play around with Entity Framework a bit, so this should be a good candidate. Perhaps I can slap a UI on it using Silverlight and Ria Services.</p>
]]></content>
            
                 
                    
                
            
        </entry>
    
        
        <entry>
            <title type="html"><![CDATA[My First Post]]></title>
            <link href="https://jessemcdowell.ca/2010/07/my-first-post/?utm_source=atom_feed" rel="alternate" type="text/html" />
            
            
                <id>6</id>
            
            
            <published>2010-07-27T22:22:21-07:00</published>
            <updated>2010-07-27T22:22:21-07:00</updated>
            
            
            <content type="html"><![CDATA[<p>I&rsquo;ve finally gone and done it.</p>
<p>To be honest, creating my own blog wasn&rsquo;t foremost in my thoughts until fairly recently. Scott Hanselman had <a href="https://www.hanselman.com/blog/FoundVideoSocialNetworkingForDevelopersAndMakingYourBlogSuckLess.aspx">a couple of interesting videos</a> on his blog that finally convinced me to do it.</p>
<p>I hope you like it.</p>
]]></content>
            
                 
                    
                         
                        
                            
                             
                                <category scheme="https://jessemcdowell.ca/tags/blogging" term="blogging" label="blogging" />
                            
                        
                    
                
            
        </entry>
    
</feed>
