10 interesting stories served every morning and every evening.
Learning Zig is not just about adding a language to your resume. It is about fundamentally changing how you think about software.Ready to transform how you think about software?
...
Read the original on www.zigbook.net »
Coinbase Data Breach Timeline Doesn’t Add Up: I Have Recordings & Emails Proving Attacks Started Months Before Their ‘Discovery’
How I was targeted by a sophisticated phishing attack in January 2025—four months before Coinbase publicly disclosed they had been breached.
← Back to Blog
The Call That Changed Everything
On January 7, 2025, at 5:02 PM, I received an email with a subject line that immediately caught my attention:
“Order N54HJG3V: Withdrawal of 2.93 ETH initiated. A representative will be in touch shortly before we mark the payment completed”
Minutes later, my phone rang. The caller ID showed 1-805-885-0141. An American-sounding woman who identified herself as a Coinbase fraud prevention representative said someone had initiated a large transfer from my account and she was calling to confirm.
What happened next was chilling: She knew my social security number. She knew my Bitcoin balance down to the decimal point. She knew personal details that should have been impossible for a scammer to possess.
This wasn’t just another phishing attempt. This was something far more sophisticated.
January 7, 2025: I was attacked by scammers with detailed personal information
January 7, 2025 (same day): Brett Farmer, Head of Trust & Safety, responded: “This report is super robust and gives us a lot to look into. We are investigating this scammer now.”
January 13, 2025: I asked Coinbase: “How did the attacker know the balance of my bitcoin holdings?” (No response)
January 17, 2025: I followed up again, asking for a reply (No response)
January 22, 2025: Still no answer to my critical question (No response)
January 29, 2025: I asked again: “Could I please get a response?” (No response)
May 11, 2025: Coinbase says they became aware of the breach (when attackers demanded $20M ransom)
For four months, I had concrete evidence that attackers possessed detailed Coinbase customer data. For four months, I repeatedly asked Coinbase to explain how this was possible. And for four months, my questions went unanswered.
Coinbase never replied to a single follow-up email after Brett Farmer’s initial response. Despite his promise that they were “investigating this scammer,” the most important question—how the attacker obtained my private account data—was met with complete silence.
What Coinbase Disclosed in May 2025
In May 2025, Coinbase finally disclosed what had happened: cybercriminals had bribed overseas customer support contractors—particularly employees at TaskUs in India—to steal sensitive customer data.
Last four digits of Social Security numbers
Coinbase estimated the financial impact at $180-400 million, affecting less than 1% of their customer base. Over 200 TaskUs employees were ultimately terminated.
But here’s the crucial question: If attackers were actively using stolen data to target customers in January, when did the actual breach occur? And when did Coinbase first become aware that something was wrong?
What I Sent Coinbase on January 7, 2025
On January 7, 2025, immediately after recognizing the attack, I sent a comprehensive security report to Coinbase’s security team. This wasn’t a vague complaint—it was a detailed technical analysis that should have raised immediate red flags about a data breach.
Full email headers: Complete technical headers showing the email was routed through Amazon SES (a32-86.smtp-out.amazonses.com), not Coinbase’s own mail servers, despite appearing to come from commerce@coinbase.com
DKIM signature analysis: Documentation that while the email passed DKIM validation for both coinbase.com and amazonses.com, the actual sending infrastructure was suspicious
The phishing email content: The complete HTML email with its fake “suspicious activity” warning and fraudulent transaction details (Order N54HJG3V, 2.93 ETH withdrawal)
Phone number used: 1-805-885-0141 (later confirmed to be a Google Voice number)
Voice recording: An audio recording of my second call with the scammer, capturing the entire conversation where she demonstrated knowledge of my personal information
Specific data the attacker possessed: A detailed list of what the scammer knew, including:
The amount of the fabricated “suspicious transfer”
Attack methodology: Description of their social engineering tactics, including the attempt to get me to move funds to “a cold wallet” by downloading Coinbase Wallet
Red flags I identified: The inability of the caller to authenticate herself, the Google Voice callback number, the lack of any notifications in my actual Coinbase account
Post-call SMS flooding: Documentation that immediately after the call, I received hundreds of spam text messages for random service signups—a potential attempt to hide legitimate 2FA codes or security alerts in the noise
This wasn’t a typical phishing report. I specifically highlighted that the attacker had access to non-public account information that should have been impossible to obtain without either a device compromise on my end or a data breach at Coinbase.
Brett Farmer, Coinbase’s Head of Trust & Safety, responded the same day, calling it a “super robust” report. But when I followed up with the key question—“How did the attacker know the balance of my bitcoin holdings?“—the conversation ended. That critical question was never answered.
Here’s what made the email and call feel convincing in the moment—and what ultimately stopped me from going through with what the caller wanted.
The phishing email I received looked completely legitimate at first glance:
DKIM signatures: Passed validation for both coinbase.com and amazonses.com
Convincing narrative: “We detected suspicious activity on your account. A fraud prevention representative will be in touch shortly…”
The email even included what appeared to be a verification code (96841) and assigned me a specific case agent (“Sarah Schueler”). This level of detail gave it tremendous credibility.
But when I examined the email headers more carefully, I found something suspicious. Here are the key header fields from the phishing email:
From: Coinbase Commerce
To: [REDACTED]@[REDACTED]
Subject: Order N54HJG3V: Withdrawal of 2.93 ETH initiated
Date: Tue, 7 Jan 2025 17:02:14 +0000
Message-ID:
Return-Path:
Received: from a32-86.smtp-out.amazonses.com (a32-86.smtp-out.amazonses.com. [54.240.32.86])
by mx.google.com with ESMTPS id [REDACTED]
for
The email was sent through Amazon SES (Simple Email Service), not Coinbase’s own mail servers. Here’s what made this suspicious:
Return-Path mismatch: The return path used @amazonses.com, not @coinbase.com
Message-ID from Amazon: The Message-ID clearly shows @email.amazonses.com as the origin
Dual DKIM signatures: While both amazonses.com and coinbase.com DKIM checks passed, this is exactly how phishing works—attackers can configure Amazon SES to send “from” coinbase.com
SPF pass for wrong domain: SPF validated that Amazon SES was authorized to send for amazonses.com, not that it was authorized to send as coinbase.com
While Coinbase might legitimately use Amazon SES for some emails, a security-critical fraud alert should come through more controlled channels with stronger sender verification. The dual DKIM setup is a classic technique: attackers register with Amazon SES, configure it to “send as” the target domain (which Amazon allows), and rely on recipients not checking the actual sending infrastructure.
The Phone Call: Even More Convincing (And Recorded)
When the woman called me, she was professional, intelligent, and sounded exactly like a legitimate customer service representative. She had all the personal information I’d already mentioned, plus more details that seemed impossible for a scammer to possess.
During our conversation, I asked her to authenticate herself. Here’s where things got interesting.
How I Detected It Was a Scam
Despite the sophistication of the attack, several red flags eventually convinced me this was fraudulent:
I asked the caller to prove she was from Coinbase. She offered to read me my personal information—but I already knew she had that information. That’s not authentication; that’s just proving she has stolen data.
When I suggested she send me an email from a verified Coinbase address that I could reply to, she claimed she didn’t have access to personal email addresses and could only use generic support channels. A fraud prevention specialist without the ability to send verified emails? That didn’t add up.
When I asked if I could call her back, she said I couldn’t reach her because she was “in the fraud department.” After the call, I tried calling the number back: it was a Google Voice number.
Legitimate financial institutions always provide callback numbers that route to their main systems. A Google Voice number is a massive red flag.
When I challenged the authenticity of the email sender, the caller insisted that Amazon was just Coinbase’s “service provider” and that the DKIM signatures proved legitimacy. But when pressed, she couldn’t explain away the anomalies in a satisfactory way.
The conversation on the recorded call shows my growing skepticism:
Me: “I don’t think there is enough information provided for me to authenticate you.”
Caller: “I’m not sure what you would like me to do…”
Me: “There are just too many red flags.”
Red Flag #4: No Notifications in My Account
After the call, I logged into my actual Coinbase account. There were:
No notifications about the alleged transfer
No actual transaction matching what the caller described
If this were real, there would have been notifications everywhere.
Red Flag #5: The Pressure to Use Coinbase Wallet
The caller wanted me to move my cryptocurrency to “a cold wallet” and started walking me through downloading Coinbase Wallet. This is a classic social engineering tactic—get the victim to move funds to an address controlled by the attacker.
I didn’t follow through, so I never discovered exactly how they planned to steal the funds, but the intent was clear.
Here’s where things got even more concerning. Immediately after I ended the call with the scammer, my phone was bombarded with hundreds of text messages—random service signups, verification codes, newsletters, everything imaginable.
At first, I thought this was just a vindictive “FU” from the scammer. But the timing and volume suggest something more calculated: SMS flooding is a known technique to hide legitimate security alerts in noise.
The attack works like this:
While you’re overwhelmed, they attempt account takeovers on various services
Real 2FA codes and security alerts get buried in the flood
You miss the critical warnings because they’re hidden among hundreds of spam messages
This could have been an attempt to:
Hide real alerts from Coinbase or other services
Overwhelm me while they attempted unauthorized access to various accounts
Create confusion and distract from their next moves
This wasn’t just a phishing call—it was a coordinated, multi-vector attack.
Coinbase’s handling of this breach raises serious questions:
Customer support agents at third-party contractors had access to extremely sensitive data: Social Security numbers, account balances, transaction histories, and personal documents. Why were overseas contractors given such privileged access?
The economics of the bribery scheme tell the story: attackers likely paid relatively small amounts to contractors earning modest wages to access data they then used to attempt thefts worth potentially millions.
Coinbase claims they “discovered” the breach on May 11, 2025, when attackers attempted to extort $20 million. But my case—and likely many others—proves the breach was being actively exploited months earlier.
What monitoring systems failed to detect that customer data was being used in sophisticated phishing attacks? I reported this in January with specific details about how the attacker knew my information. That report should have triggered alarm bells.
Despite Brett Farmer’s initial acknowledgment that my report was “super robust” and warranted investigation, my follow-up emails asking how attackers obtained my account data went completely unanswered.
My question was specific and technical. It went to the heart of what should have been a massive red flag: How did attackers have access to non-public account data?
Had Coinbase investigated this seriously in January, they might have discovered the insider threat months earlier and prevented additional victims.
Coinbase says they “became aware” of the breach on May 11, 2025. But my January attack proves the breach was active at least four months earlier. This raises uncomfortable questions:
...
Read the original on jonathanclark.com »
TLDR: I built a portable step-sequencer synthesizer for my daughter’s third birthday. It has four sliders that control four notes in a looping sequence. Slide up = higher pitch, slide down = lower.
It’s a child-friendly, tactile music toy. Here’s the pink edition in action:
My daughter received a Montessori activity board full of switches and LEDs for her first birthday. Watching her twist knobs and flip the switches reminded me of the control surface of a synth, and I wondered if I could build a musical version - something simple, tactile, and creative that didn’t require holding down buttons to keep the sound going. A year later I finally decided to build it. I had no prior hardware experience, so this became an excuse to learn about microcontrollers, CAD, PCB design, and 3D printing.
I started the project with a 15 year old Arduino Inventors Kit and only a vague idea about how to use it. The first goal was simple: build a basic MIDI controller on a breadboard. If I could get some potentiometer readings, map them to 12 discrete values - one for each note in an octave - and emit MIDI messages, I would have taken a small step in the right direction. Adding an onboard synth module and designing a pretty box to put it in could wait until later.
Reading the potentiometer inputs and turning them into the MIDI messages using the Arduino MIDI library was easy enough. To hear the output, I wrote a small Python script that intercepted the MIDI messages and forwarded them to my Mac’s default MIDI device, which Logic Pro could pick up. That let me “play” the breadboard through software instruments.
Once I had the hang of wiring up potentiometers and rotary encoders, the next step was to move the audio synthesis from Logic to my breadboard. For this I used a little $12.95 SAM2695 synthesiser module with an integrated amplifier and speaker. Its inner workings remain a mystery to me but it does what I need it to and I was happy reduce the amount of time to get a functioning prototype into my daughter’s hands. I also moved to an Elegoo Nano here due to its low cost and increased number of analog pins.
Next, I added small OLED screen to provide some visual feedback and character and used the handy u8g2 graphics library. This was trickier than I expected: the Nano has so little RAM that I couldn’t buffer a full frame. I had to update the screen in small patches, and large updates were slow enough that they occasionally interfered with encoder reads, and caused laggy notes at faster tempos. I’ve still got some work to do to iron out blocking screen updates, but for now I pushed through and accepted a bit of lag. I added a little dancing panda that I adapted from one I found in a pixel art tutorial which I can no longer find - if you’re the original creator, please let me know so I can credit you!
For developing on-the-go, I discovered the Wokwimicrocontroller simulator. It let me build a virtual schematic and test code without luggin around the my fragile prototype. They have a free online simulator and a paid VS Code plugin that lets you create your diagrams in the IDE.
Once I had a functional circuit it was time to move on to designing an enclosure and assembling a complete version of the synthesiser that my daughter could play with.
After wiring up the breadboard, the next hurdle was figuring out how to build a proper enclosure. I looked for off-the-shelf cases, but nothing matched the size I needed, and everything seemed to come in either black or beige. So I decided to learn some basic CAD and 3D-print the enclosure on a friend’s Bambu Labs A1 Mini.
I downloaded Fusion 360 and started following tutorials. With only an hour or two to spare in the evenings, progress was slow at first. I’d never used any CAD software before, so I was constantly switching between learning the software and trying to make actual progress on the design. For other beginners, I highly recommend Product Design Online’s Learn Fusion 360 in 30 Days and this excellent video by wermy.
After a few weeks of trial-and-error, I finally had a design I could print:
Thank you Tom for printing these! A year’s supply of filament coming your way.
Moving the circuit to a proper PCB felt daunting, so for the first version I hand-wired everything on a solderable breadboard. The good: hanging out and drinking some wine with my friend, who kindly offered to help with the soldering. The bad: when the time finally came to close the two halves of the enclosure, stuffing the rats nest of wires inside ended up putting pressure on a bunch of the delicate soldered joints and breaking them. My daughter could play around a bit with it - enough for me to convince myself that she’d genuinely enjoy using it - but it was fragile. I also wanted to make a few units for friends, which meant I needed something more robust and faster to assemble. Time to design a PCB.
Romain, I definitely owe you a bottle or two…
Once again I was back on YouTube and fumbling my way through an unfamiliar workflow, though I stuck with Fusion 360 which has its own electronics design suite. For my first attempt I decided that I’d focus on surface-mounting the various components and save integrating the microcontroller into the board for a future project. A large chunk of the time here was spent reading datasheets, sourcing parts and importing their footprints/models into Fusion 360. Once I had learned the basics, I was able to route the circuit on a 2-layer board. One of the nice things about Fusion is that you get a full 3D model of the assembled PCB, which makes designing the enclosure much easier.
When I was finished, I exported the PCB design file and uploaded it to JLCPCB. Five boards (the minimum order) cost £35.41 including shipping, and they arrived five days later. It blows my mind that this is possible.
For my first version I had decided to use 4 AA batteries and use the Arduino’s built-in voltage converter to provide a steady 5 volts. Something I overlooked, however, is that the Arduino’s VIN pin that provides a regulated 5V to the board requires 7-12V input, while my batteries will provide, at best, 6V when new. The board seemed to work OK at this voltage but it would be vulnerable to random resets as the voltage starts to sag and a short battery life.
For the next iteration I decided to get rid of one of the batteries and introduce an Adafruit Miniboost to provide a regulated 5V power supply to the Arduino from the combine 4.5V from the three AA batteries. This allowed me to reduce the weight a little bit and provide the synth with a stable supply of power for a longer duration.
Finally, I updated the enclosure so that I could securely attach the PCB and added a neat little battery compartment. I also added a small bezel to raise the height of OLED display.
It’s been just over a week since my daughter unwrapped her new synth. It now lives on the shelf with her other toys, and so far it gets regular use and is holding up well. One of my goals was to make something fun to fiddle with at a superficial level, but with enough depth to stay interesting as she gets older. The first part seems true, and I’ll see how the second plays out over the coming months. There are still a few kinks to iron out, such as the lag when updating the screen. I’m also planning to upgrade the Elegoo Nano to an ESP32, which should simplify the firmware and open up more options for fun display graphics.
After watching a few children and adults (musical and non-musical) play with it, I think there might be the germ of a real product here. With a better synth engine, audio outputs, and a way to chain multiple units together, it could be a playful introduction to electronic music for older kids - maybe even adults. However, adding features is one thing, but actually bringing a product to market is another. The challenges aren’t just technical: they’re regulatory and financial. Safety certification (UKCA/CE, and FCC in the US) can cost £5-10K or more. Manufacturing is another hurdle. A 3D-printed enclosure is fine for a prototype, but a real product likely needs injection-molded parts, which require expensive tooling. Even a small production run would need more upfront capital than I can sensibly invest right now.
For the moment I’m treating it as a learning project, but the response so far has been encouraging. A more polished open-source version for makers, or possibly a small Kickstarter campaign, might be viable next steps. If anyone reading this has experience bringing small-run hardware to market, I’d love to hear from you.
...
Read the original on bitsnpieces.dev »
Access Policies: Protecting Who Can Access WhatAllow public access to everyone logging into your networkDeploying the Warp client and enrolling into Zero Trust
A while ago, after frustration with Tailscale in environments where it couldn’t properly penetrate NAT/firewall and get a p2p connection, I decided to invest some time into learning something new: Cloudflare Zero Trust + Warp.
There are so many new concepts, but after way too long, I can finally say that I understand Cloudflare Zero Trust Warp now. I am a full-on Cloudflare Zero Trust with Warp convert, and while I still have Tailscale running in parallel, almost everything I do now is going through Zero Trust tunnels.
This post is an explanation of the basic concepts, because I’m sure others will have similar issues wrapping their head around it.
Why would you even sink so much time into learning this? What does it give you?
Argo tunnels through Zero Trust allow you to do a bunch of really cool things:
Connect private networks together - can be home networks, can be kubernetes clusters, you can create tunnels to and from every infraExpose private services to the public, on public hostnames, no matter where they are running. You could even put your router running at 192.168.1.1 on the internet, accessible to everyone, no Warp client requiredCreate fully private networks with private IPs (10.x.x.x) that only resolve when Warp is connected, to services you specifyQuickly expose a public route to any service running locally or on any server, for quick development, testing webhooks or giving coworkers a quick previewCreate a fully private network running at home that’s only available when you’re connected to the Warp VPN client, or only to you, reachable anywhereNo worries about NAT, everything goes through the Cloudflare network, no direct p2p connection requiredAdd very granular access policies on who can access what - what login method does the user need, which email addresses are allowed. Allow bots and server-to-server exceptions with service access tokens. Does the user need to have Warp running? Does he need to be enrolled in Zero Trust? Does he need some special permission flag?Authenticate to SSH servers through Zero Trust access policies without the need of SSH keys. Just connect Warp, type ssh host and you’re logged inClose public SSH ports completely to only allow login through WarpGet the benefits of Cloudflare VPN edge routing on top (similar to 1.1.1.1 Warp+)
To get this out of the way:
Tailscale: peer-to-peer, uses NAT and firewall penetration methods to establish p2p connections. If not possible, it goes through central relay servers. Absolute best speed and latency if a connection is established. Cloudflare: All traffic (with the exception of warp-to-warp routing, which is p2p) goes through Cloudflare’s edge network. So even SSH-ing into your local router will hop through Cloudflare servers. This adds latency, but no issues with NAT at all.
Cloudflare has 2 tools available: Warp Client and Cloudflared. They interact with each other and have similarities in some areas but are not the same.
The tool that connects you to the Cloudflare network. This is the thing that you configure to add clients into your Zero Trust network and enforces policies.
Usually this runs on clients, but can also run on servers.
Warp client also supports warp-to-warp routing which is a true p2p connection similar to Tailscale.
The thing that creates a tunnel and adds it to the Zero Trust network.
Most commonly you run this on servers to expose tunnels into your network, but you can also run it on clients.
On the client side you can use cloudflared access to establish a connection with other things in your Zero Trust network.
Can also create one-time-use tunnels that aren’t connected to the Zero Trust network. Good for testing.
This took me the longest to understand. Zero Trust allows you to configure Tunnels, Routes and Targets; here’s how they interplay.
The most important part of your setup. Tunnels are deployed through cloudflared and are simply an exit for traffic. Think of it as a literal tunnel that has its end somewhere.
Tunnels are deployed to infrastructure in the target network. So if you have a home network with 192.168.1.1/24, you want to deploy cloudflared on any machine that’s always on and within that network. It can be your router, or your Raspi, it doesn’t matter.
For server-hosted services, you can have a tunnel on your main dev server, on a server, or on a pod in your Kubernetes cluster.
Now you have an opening into these networks through Warp/Argo tunnels.
You can either configure tunnels through the Zero Trust UI by “adopting” them, or configure them in the /etc/cloudflared/config.yml config on the machine itself. Personal preference, I usually configure them on the machine itself.
The config specifies where a request should get routed to when it arrives at the tunnel. So the tunnel knows what to do with it.
In this config we tell cloudflared to route traffic arriving at this tunnel for hostname gitlab.widgetcorp.tech to localhost:80, and gitlab-ssh to the local SSH server.
The config alone doesn’t do anything. It just exposes a tunnel, and that’s it. What we need now are routes and targets.
Exposing a private network to the public with tunnels quickly#
Quick addition, as this is a super common use case. If you want to just expose something in your home network to the internet, you can add a config like this:
Then go into Cloudflare DNS settings and map the domain homeassistant.mydomain.com to the tunnel:
Now all traffic going to this domain will go through the cloudflared tunnel, which is configured to route homeassistant.mydomain.com to 192.168.1.3. No Warp client needed, Argo tunnel does everything for us.
Note: If you adopted the tunnels and don’t use config.yaml, you can automatically create matching DNS records in the Cloudflare UI and don’t need to do this manually.
A route defines where to direct traffic to.
Let’s say your homeassistant runs on 192.168.1.3 at home and you want to reach it from outside. Just above we deployed a cloudflared tunnel on our router at 192.168.1.3, and added a config pointing the domain to the Argo tunnel, so homeassistant.mydomain.com is already available to the public. However, 192.168.1.3 isn’t, as it’s a private network IP.
A route like 192.168.1.1/24 pointing at your tunnel, to route ALL traffic to the full IP range through that tunnel (so even 192.168.1.245 will go through your tunnel)Or a more specific route like 192.168.1.3/32 pointing at your tunnel, to ONLY route traffic to 192.168.1.3 through that tunnel.
When configured, once your user connects their Warp client that’s set up with your Zero Trust network, the Warp client will see requests to 192.168.1.3 and route it through the Cloudflare network to reach your specific tunnel. Like a little police helper directing cars where to go.
If the Warp client is not connected, 192.168.1.3 will just resolve in your current local network. If connected, it will resolve to the tunnel.
The routed IP doesn’t need to exist! So you could, for example, route a random IP you like (e.g., 10.128.1.1) to your tunnel, the tunnel then forwards it based on your routes, for example to 192.168.1.1. This is extremely powerful because it allows you to build your own fully virtual network.
That’s all it does, what happens afterwards is up to the tunnel config that we created above. The tunnel decides where to point the incoming request to, whether that’s localhost or somewhere else.
To summarize, the route tells the Warp client where to route traffic to.
Now we have 2 things working:
homeassistant.mydomain.com - goes through a Cloudflare DNS record pointing at an Argo tunnel, which then forwards to 192.168.1.3. This works without Warp connected as it’s on the DNS level, public to everyone.192.168.1.3 - The Warp client sees the request and routes it through the Argo tunnel, which then forwards it to 192.168.1.3 within that network. This needs Warp connected to work, and is only visible to people in your Zero Trust org.
This one took me a while.
Targets are needed to define a piece of infrastructure that you want to protect through Zero Trust. They are like a pointer pointing to something in your network. This goes hand-in-hand with routes, but isn’t always needed.
Let’s say you have 192.168.1.3 (homeassistant) exposed through a Cloudflare tunnel. By default, anyone in your network that is part of your Zero Trust org and has Warp client installed can now access your homeassistant at 192.168.1.3.
We can change that with targets. For example, defining a target with hostname = homeassistant.mydomain.com to the route 192.168.1.3/32 allows us to add access policies to it. We can also put an entire network into the target by specifying 192.168.1.3/24 to control access. This also works with virtual IPs like 10.128.1.1!
Targets alone won’t do anything, they just point to the service or network. “Hey, here is homeassistant”, or “hey, here is my home network”.
Access Policies: Protecting Who Can Access What#
Continuing the example from above:
we have a tunnel running on our home network that routes homeassistant.mydomain.com to 192.168.1.3we set up public DNS records to point homeassistant.mydomain.com to the Argo tunnel in Cloudflarewe created a route 192.168.1.3 to go through the same tunnelwe also created a target pointing to 192.168.1.3
When users access either 192.168.1.3 or homeassistant.mydomain.com, the Warp client will route the request through the tunnel, which then forwards the request to 192.168.1.3. Homeassistant loads and everything is fine.
But do we want that?
With access policies, we can leave things in the public but protect them with Cloudflare Zero Trust access. So while 192.168.1.3 is only available if Warp is connected (so routing to it works), we can add security to our public homeassistant.mydomain.com.
Go to Access -> Applications -> Add an Application -> Self-hosted.
Here we can define what should be protected, and how.
Going with our previous example, we can add a public hostname homeassistant.mydomain.com or an IP like 192.168.1.3 (or both), then attach policies of who should be able to access it.
You can specify Include (“OR”) and Require (“AND”) selectors.
Require rules must always be met, on top of include rules, to grant accessAny of the Include rules must match to grant access
Then there are Actions:
Allow - when the policy matches, allow accessDeny - when the policy matches, deny access. aka blocking something. Bypass - when the policy matches, bypass Zero Trust completely. No more checking.Service Auth - when the policy matches, allow authentication to the service with a service token header (good for server-to-server, or bots). Check Access -> Service Auth to create these tokens.
Allow public access to everyone logging into your network#
The most common use case: homeassistant.mydomain.com is public. We want to keep it public, but add an extra layer of security.
Add an include policy, pick any of the email selectors, add the email of the user you want to allow access to. Now only people authenticated with your Zero Trust org with the specified emails can access your homeassistant, without needing to have Warp running.
We can harden this by adding require rules: Add a Login Method selector rule, pick a specific login method like GitHub. Now only people with specific emails that have authenticated through GitHub can access your homeassistant, without needing to have Warp running.
Another policy I like having is to skip the login screen entirely when connected through Warp. If a user is already enrolled into my Zero Trust org and has the Warp client provisioned, then there’s no need to ask them to authenticate again.
We can add a separate policy (don’t edit the one we just created above), pick the Gateway selector and set it to Allow or Bypass.
Don’t use ‘Warp’ - the Warp selector will match anyone that has Warp running, including the consumer 1.1.1.1 app. Gateway, on the other hand, matches only if someone is connecting through your Gateway, be that DNS or a provisioned Warp client.
Warp through Zero Trust is running on a machine: No login screenNo Warp running (public access): Prompt for login screen, but only allow specific emails that authenticated through GitHub
This setup makes it very convenient to reach homeassistant, no matter if connected through Warp or not.
Deploying the Warp client and enrolling into Zero Trust#
Are you still with me?
Our network is basically done. We have a login-protected homeassistant.mydomain.com that routes through our tunnel into our private network and terminates at 192.168.1.3, and we have a direct route to 192.168.1.3 that only works when connected with Warp.
We also have login policies to make sure only specific users (logged in with GitHub and certain email addresses) can access homeassistant.
So how do we deploy the dang Warp client?
Actually the same: We create some policies.
In Enrollment Permissions, we specify the same policies for who can enroll. For example, “[email protected]” when authenticated through GitHub is allowed to enroll. In the Login Methods we can specify what login methods are available when someone tries to enroll into our Zero Trust org.
Toggle WARP authentication identity settings to make the Gateway selector available in policies, effectively allowing the configured WARP client to be used as a login method.
Careful here, once someone is enrolled, they are basically in your Zero Trust network through Warp. Make sure you harden this.
Then, in Profile settings, we define how the WARP client behaves. These are things like protocol: MASQUE or WireGuard, service mode, what IPs and domains to exclude from WARP routing (e.g., the local network should never go through WARP), setting it to exclude or include mode and so on.
Install CA to system certificate store - installs the Cloudflare CA certificate automatically when enrolled. Override local interface IP - assigns a unique CGNAT private IP to the client. This is needed for warp-to-warp routing.Device Posture - what checks the WARP client should perform for the org. E.g., check the OS version, some OS files on disk, etc. I have this set to WARP and Gateway because I want the client to provide information on whether the user is connected through WARP and Gateway, for skipping certain login pages.
Once done, just open the Warp client (https://developers.cloudflare.com/warp-client/), and log in to your network. This should open the login pages you specified in the Device Enrollment screen, and check all the enrollment policies you specified.
Once passed, congratulations, your WARP client is now connected to your Zero Trust network. The client will then go ahead and start routing 192.168.1.3 through your tunnels, as specified in your tunnel and route settings.
If you followed this guide, here is what we built:
Login methods to connect the Warp client to your Zero Trust org through GitHub and specific email addressesA tunnel within your private network thatForwards any request coming in with host homeassistant.mydomain.com to 192.168.1.3A route that forwards all traffic for 192.168.1.3 to the tunnel in your private network, which will terminate it at 192.168.1.3, which will only work when connected through Warp to route the requestA DNS name homeassistant.mydomain.com that points to the Argo tunnel, and will allow everyone (even if not connected through Warp) to access homeassistant which runs at 192.168.1.3Access policies that willAsk users that are not connected to Zero Trust through Warp to log in with GitHub and specific email, so everyone can access it if they can log inA policy that skips the login screen completely and just shows homeassistant if the user connects through Zero Trust Warp client (enrolled into our org)
You don’t need the public domain and you don’t need the route to 192.168.1.3. These are 2 different options that you can use to expose homeassistant when you’re not at home. One is using a public domain name everyone can see, one is explicitly requiring connecting through enrolled Warp.
What I didn’t cover in this post:
Creating and assigning fully private IPs that only exist within your Zero Trust networkSSH authentication through Zero Trust access policies (that’s what we need Targets for)The other application types besides Self-Hosted
I’m happy to expand on it if there’s interest. Let me know on X or Bluesky.
...
Read the original on david.coffee »
By far the most popular npm package I’ve ever written is blob-util, which is ~10 years old and still gets 5+ million weekly downloads.
It’s a small collection of utilities for working with Blobs in JavaScript. I wrote it because I found that PouchDB users were endlessly confused about how to work with Blobs and how to convert them to strings, ArrayBuffers, etc.
Given that some 80% of developers are now using AI in their regular work, blob-util is almost certainly the kind of thing that most developers would just happily have an LLM generate for them. Sure, you could use blob-util, but then you’d be taking on an extra dependency, with unknown performance, maintenance, and supply-chain risks.
And sure enough, Claude will happily spit out whatever Blob utilities you need when prompted:
> Write me a utility function in TypeScript to convert a Blob to an ArrayBuffer. It should return a Promise.
function blobToArrayBuffer(blob: Blob): Promise
Claude’s version is pretty close to the blob-util version (unsurprising, since it was probably trained on it!). Although it’s much more verbose, unnecessarily checking if readAsArrayBuffer actually gives you an ArrayBuffer (although this does make TypeScript happy). To be fair, it also improves on my implementation by directly rejecting with an error rather than the more awkward onerror event.
Note: for anyone wondering, yes Claude did suggest the new Blob.arrayBuffer() method, but it also generated the above for “older environments.”
I suppose some people would see this as progress: fewer dependencies, more robust code (even if it’s a bit more verbose), quicker turnaround time than the old “search npm, find a package, read the docs, install it” approach.
I don’t have any excessive pride in this library, and I don’t particularly care if the download numbers go up or down. But I do think something is lost with the AI approach. When I wrote blob-util, I took a teacher’s mentality: the README has a cutesy and whimsical tutorial featuring Kirby, in all his blobby glory. (I had a thing for putting Nintendo characters in all my stuff at the time.)
The goal wasn’t just to give you a utility to solve your problem (although it does that) — the goal was also to teach people how to use JavaScript effectively, so that you’d have an understanding of how to solve other problems in the future.
I don’t know which direction we’re going in with AI (well, ~80% of us; to the remaining holdouts, I salute you and wish you godspeed!), but I do think it’s a future where we prize instant answers over teaching and understanding. There’s less reason to use something like blob-util, which means there’s less reason to write it in the first place, and therefore less reason to educate people about the problem space.
Even now there’s a movement toward putting documentation in an llms.txt file, so you can just point an agent at it and save your brain cells the effort of deciphering English prose. (Is this even documentation anymore? What is documentation?)
I still believe in open source, and I’m still doing it (in fits and starts). But one thing has become clear to me: the era of small, low-value libraries like blob-util is over. They were already on their way out thanks to Node.js and the browser taking on more and more of their functionality (see node:glob, structuredClone, etc.), but LLMs are the final nail in the coffin.
This does mean that there’s less opportunity to use these libraries as a springboard for user education (Underscore.js also had this philosophy), but maybe that’s okay. If there’s no need to find a library to, say, group the items in an array, then maybe learning about the mechanics of such libraries is unnecessary. Many software developers will argue that asking a candidate to reverse a binary tree is pointless, since it never comes up in the day-to-day job, so maybe the same can be said for utility libraries.
I’m still trying to figure out what kinds of open source are worth writing in this new era (hint: ones that an LLM can’t just spit out on command), and where education is the most lacking. My current thinking is that the most value is in bigger projects, more inventive projects, or in more niche topics not covered in an LLM’s training data. For example, I look back on my work on fuite and various memory-leak-hunting blog posts, and I’m pretty satisfied that an LLM couldn’t reproduce this, because it requires novel research and creative techniques. (Although who knows: maybe someday an agent will be able to just bang its head against Chrome heap snapshots until it finds the leak. I’ll believe it when I see it.)
There’s been a lot of hand-wringing lately about where open source fits in in a world of LLMs, but I still see people pushing the boundaries. For example, a lot of naysayers think there’s no point in writing a new JavaScript framework, since LLMs are so heavily trained on React, but then there goes the indefatigable Dominic Gannaway writing Ripple.js, yet another JavaScript framework (and with some new ideas, to boot!). This is the kind of thing I like to see: humans laughing in the face of the machine, going on with their human thing.
So if there’s a conclusion to this meandering blog post (excuse my squishy human brain; I didn’t use an LLM to write this), it’s just that: yes, LLMs have made some kinds of open source obsolete, but there’s still plenty of open source left to write. I’m excited to see what kinds of novel and unexpected things you all come up with.
...
Read the original on nolanlawson.com »
Look, I know what you’re thinking. “Why not just use Elasticsearch?” or “What about Algolia?” Those are valid options, but they come with complexity. You need to learn their APIs, manage their infrastructure, and deal with their quirks.
Sometimes you just want something that:
* Is easy to understand and debug
That’s what I built. A search engine that uses your existing database, respects your current architecture, and gives you full control over how it works.
The concept is simple: tokenize everything, store it, then match tokens when searching.
Indexing: When you add or update content, we split it into tokens (words, prefixes, n-grams) and store them with weights
Searching: When someone searches, we tokenize their query the same way, find matching tokens, and score the results
Scoring: We use the stored weights to calculate relevance scores
The magic is in the tokenization and weighting. Let me show you what I mean.
We need two simple tables: index_tokens and index_entries.
This table stores all unique tokens with their tokenizer weights. Each token name can have multiple records with different weights—one per tokenizer.
// index_tokens table structure
id | name | weight
1 | parser | 20 // From WordTokenizer
2 | parser | 5 // From PrefixTokenizer
3 | parser | 1 // From NGramsTokenizer
4 | parser | 10 // From SingularTokenizer
Why store separate tokens per weight? Different tokenizers produce the same token with different weights. For example, “parser” from WordTokenizer has weight 20, but “parser” from PrefixTokenizer has weight 5. We need separate records to properly score matches.
The unique constraint is on (name, weight), so the same token name can exist multiple times with different weights.
This table links tokens to documents with field-specific weights.
// index_entries table structure
id | token_id | document_type | field_id | document_id | weight
1 | 1 | 1 | 1 | 42 | 2000
2 | 2 | 1 | 1 | 42 | 500
The weight here is the final calculated weight: field_weight × tokenizer_weight × ceil(sqrt(token_length)). This encodes everything we need for scoring. We will talk about scoring later in the post.
Why this structure? Simple, efficient, and leverages what databases do best.
What is tokenization? It’s breaking text into searchable pieces. The word “parser” becomes tokens like [“parser”], [“par”, “pars”, “parse”, “parser”], or [“par”, “ars”, “rse”, “ser”] depending on which tokenizer we use.
Why multiple tokenizers? Different strategies for different matching needs. One tokenizer for exact matches, another for partial matches, another for typos.
interface TokenizerInterface
public function tokenize(string $text): array; // Returns array of Token objects
public function getWeight(): int; // Returns tokenizer weight
This one is straightforward—it splits text into individual words. “parser” becomes just [“parser”]. Simple, but powerful for exact matches.
First, we normalize the text. Lowercase everything, remove special characters, normalize whitespace:
class WordTokenizer implements TokenizerInterface
public function tokenize(string $text): array
// Normalize: lowercase, remove special chars
$text = mb_strtolower(trim($text));
$text = preg_replace(‘/[^a-z0-9]/‘, ’ ’, $text);
$text = preg_replace(‘/\s+/‘, ’ ’, $text);
Next, we split into words and filter out short ones:
// Split into words, filter short ones
$words = explode(′ ’, $text);
$words = array_filter($words, fn($w) => mb_strlen($w) >= 2);
Why filter short words? Single-character words are usually too common to be useful. “a”, “I”, “x” don’t help with search.
// Return as Token objects with weight
return array_map(
fn($word) => new Token($word, $this->weight),
array_unique($words)
This generates word prefixes. “parser” becomes [“par”, “pars”, “parse”, “parser”] (with min length 4). This helps with partial matches and autocomplete-like behavior.
First, we extract words (same normalization as WordTokenizer):
class PrefixTokenizer implements TokenizerInterface
public function __construct(
private int $minPrefixLength = 4,
private int $weight = 5
public function tokenize(string $text): array
// Normalize same as WordTokenizer
$words = $this->extractWords($text);
Then, for each word, we generate prefixes from the minimum length to the full word:
$tokens = [];
foreach ($words as $word) {
$wordLength = mb_strlen($word);
// Generate prefixes from min length to full word
for ($i = $this->minPrefixLength; $i
Why use an associative array? It ensures uniqueness. If “parser” appears twice in the text, we only want one “parser” token.
Finally, we convert the keys to Token objects:
return array_map(
fn($prefix) => new Token($prefix, $this->weight),
array_keys($tokens)
Why min length? Avoid too many tiny tokens. Prefixes shorter than 4 characters are usually too common to be useful.
This creates character sequences of a fixed length (I use 3). “parser” becomes [“par”, “ars”, “rse”, “ser”]. This catches typos and partial word matches.
class NGramsTokenizer implements TokenizerInterface
public function __construct(
private int $ngramLength = 3,
private int $weight = 1
public function tokenize(string $text): array
$words = $this->extractWords($text);
Then, for each word, we slide a window of fixed length across it:
$tokens = [];
foreach ($words as $word) {
$wordLength = mb_strlen($word);
// Sliding window of fixed length
for ($i = 0; $i
The sliding window: for “parser” with length 3, we get:
Why this works? Even if someone types “parsr” (typo), we still get “par” and “ars” tokens, which match the correctly spelled “parser”.
return array_map(
fn($ngram) => new Token($ngram, $this->weight),
array_keys($tokens)
Why 3? Balance between coverage and noise. Too short and you get too many matches, too long and you miss typos.
All tokenizers do the same normalization:
This ensures consistent matching regardless of input format.
We have three levels of weights working together:
Tokenizer weights: Word vs prefix vs n-gram (stored in index_tokens)
When indexing, we calculate the final weight like this:
$finalWeight = $fieldWeight * $tokenizerWeight * ceil(sqrt($tokenLength));
Why use ceil(sqrt())? Longer tokens are more specific, but we don’t want weights to blow up with very long tokens. “parser” is more specific than “par”, but a 100-character token shouldn’t have 100x the weight. The square root function gives us diminishing returns—longer tokens still score higher, but not linearly. We use ceil() to round up to the nearest integer, keeping weights as whole numbers.
You can adjust weights for your use case:
* Increase field weights for titles if titles are most important
* Increase tokenizer weights for exact matches if you want to prioritize exact matches
* Adjust the token length function (ceil(sqrt), log, or linear) if you want longer tokens to matter more or less
You can see exactly how weights are calculated and adjust them as needed.
...
Read the original on karboosx.net »
Goldman Sachs analysts attempted to address a touchy subject for biotech companies, especially those involved in the pioneering “gene therapy” treatment: cures could be bad for business in the long run. “Is curing patients a sustainable business model?” analysts ask in an April 10 report entitled “The Genome Revolution.“”The potential to deliver ‘one shot cures’ is one of the most attractive aspects of gene therapy, genetically-engineered cell therapy and gene editing. However, such treatments offer a very different outlook with regard to recurring revenue versus chronic therapies,” analyst Salveen Richter wrote in the note to clients Tuesday. “While this proposition carries tremendous value for patients and society, it could represent a challenge for genome medicine developers looking for sustained cash flow.“Richter cited Gilead Sciences’ treatments for hepatitis C, which achieved cure rates of more than 90 percent. The company’s U. S. sales for these hepatitis C treatments peaked at $12.5 billion in 2015, but have been falling ever since. Goldman estimates the U.S. sales for these treatments will be less than $4 billion this year, according to a table in the report.“GILD is a case in point, where the success of its hepatitis C franchise has gradually exhausted the available pool of treatable patients,” the analyst wrote. “In the case of infectious diseases such as hepatitis C, curing existing patients also decreases the number of carriers able to transmit the virus to new patients, thus the incident pool also declines … Where an incident pool remains stable (eg, in cancer) the potential for a cure poses less risk to the sustainability of a franchise.“The analyst didn’t immediately respond to a request for comment.The report suggested three potential solutions for biotech firms:“Solution 1: Address large markets: Hemophilia is a $9-10bn WW market (hemophilia A, B), growing at ~6-7% annually.”
“Solution 2: Address disorders with high incidence: Spinal muscular atrophy (SMA) affects the cells (neurons) in the spinal cord, impacting the ability to walk, eat, or breathe.”
“Solution 3: Constant innovation and portfolio expansion: There are hundreds of inherited retinal diseases (genetics forms of blindness) … Pace of innovation will also play a role as future programs can offset the declining revenue trajectory of prior assets.”
...
Read the original on www.cnbc.com »
After months of agentic coding frenzy, Twitter is still ablaze with discussions about MCP servers. I previously did some very light benchmarking to see if Bash tools or MCP servers are better suited for a specific task. The TL;DR: both can be efficient if you take care.
Unfortunately, many of the most popular MCP servers are inefficient for a specific task. They need to cover all bases, which means they provide large numbers of tools with lengthy descriptions, consuming significant context.
It’s also hard to extend an existing MCP server. You could check out the source and modify it, but then you’d have to understand the codebase, together with your agent.
MCP servers also aren’t composable. Results returned by an MCP server have to go through the agent’s context to be persisted to disk or combined with other results.
I’m a simple boy, so I like simple things. Agents can run Bash and write code well. Bash and code are composable. So what’s simpler than having your agent just invoke CLI tools and write code? This is nothing new. We’ve all been doing this since the beginning. I’d just like to convince you that in many situations, you don’t need or even want an MCP server.
Let me illustrate this with a common MCP server use case: browser dev tools.
My use cases are working on web frontends together with my agent, or abusing my agent to become a scrapey little hacker boy so I can scrape all the data in the world. For these two use cases, I only need a minimal set of tools:
* Start the browser, optionally with my default profile so I’m logged in
* Navigate to a URL, either in the active tab or a new tab
* Take a screenshot of the viewport
And if my use case requires additional special tooling, I want to quickly have my agent generate that for me and slot it in with the other tools.
People will recommend Playwright MCP or Chrome DevTools MCP for the use cases I illustrated above. Both are fine, but they need to cover all the bases. Playwright MCP has 21 tools using 13.7k tokens (6.8% of Claude’s context). Chrome DevTools MCP has 26 tools using 18.0k tokens (9.0%). That many tools will confuse your agent, especially when combined with other MCP servers and built-in tools.
Using those tools also means you suffer from the composability issue: any output has to go through your agent’s context. You can kind of fix this by using sub-agents, but then you rope in all the issues that sub-agents come with.
Here’s my minimal set of tools, illustrated via the README.md:
# Browser Tools
Minimal CDP tools for collaborative site exploration.
## Start Chrome
\`\`\`bash
./start.js # Fresh profile
./start.js –profile # Copy your profile (cookies, logins)
Start Chrome on `:9222` with remote debugging.
## Navigate
\`\`\`bash
./nav.js https://example.com
./nav.js https://example.com –new
Navigate current tab or open new tab.
## Evaluate JavaScript
\`\`\`bash
./eval.js ‘document.title’
./eval.js ‘document.querySelectorAll(“a”).length’
Execute JavaScript in active tab (async context).
## Screenshot
\`\`\`bash
./screenshot.js
Screenshot current viewport, returns temp file path.
This is all I feed to my agent. It’s a handful of tools that cover all the bases for my use case. Each tool is a simple Node.js script that uses Puppeteer Core. By reading that README, the agent knows the available tools, when to use them, and how to use them via Bash.
When I start a session where the agent needs to interact with a browser, I just tell it to read that file in full and that’s all it needs to be effective. Let’s walk through their implementations to see how little code this actually is.
The agent needs to be able to start a new browser session. For scraping tasks, I often want to use my actual Chrome profile so I’m logged in everywhere. This script either rsyncs my Chrome profile to a temporary folder (Chrome doesn’t allow debugging on the default profile), or starts fresh:
#!/usr/bin/env node
import { spawn, execSync } from “node:child_process”;
import puppeteer from “puppeteer-core”;
const useProfile = process.argv[2] === “–profile”;
if (process.argv[2] && process.argv[2] !== “–profile”) {
console.log(“Usage: start.ts [–profile]“);
console.log(“\nOptions:“);
console.log(” –profile Copy your default Chrome profile (cookies, logins)“);
console.log(“\nExamples:“);
console.log(” start.ts # Start with fresh profile”);
console.log(” start.ts –profile # Start with your Chrome profile”);
process.exit(1);
// Kill existing Chrome
try {
execSync(“killall ‘Google Chrome’”, { stdio: “ignore” });
} catch {}
// Wait a bit for processes to fully die
await new Promise((r) => setTimeout(r, 1000));
// Setup profile directory
execSync(“mkdir -p ~/.cache/scraping”, { stdio: “ignore” });
if (useProfile) {
// Sync profile with rsync (much faster on subsequent runs)
execSync(
‘rsync -a –delete “/Users/badlogic/Library/Application Support/Google/Chrome/” ~/.cache/scraping/’,
{ stdio: “pipe” },
// Start Chrome in background (detached so Node can exit)
spawn(
“/Applications/Google Chrome.app/Contents/MacOS/Google Chrome”,
[“–remote-debugging-port=9222”, `–user-data-dir=${process.env[“HOME”]}/.cache/scraping`],
{ detached: true, stdio: “ignore” },
).unref();
// Wait for Chrome to be ready by attempting to connect
let connected = false;
for (let i = 0; i < 30; i++) {
try {
const browser = await puppeteer.connect({
browserURL: “http://localhost:9222”,
defaultViewport: null,
await browser.disconnect();
connected = true;
break;
} catch {
await new Promise((r) => setTimeout(r, 500));
if (!connected) {
console.error(“✗ Failed to connect to Chrome”);
process.exit(1);
console.log(`✓ Chrome started on :9222${useProfile ? ” with your profile” : “”}`);
All the agent needs to know is to use Bash to run the start.js script, either with –profile or without.
Once the browser is running, the agent needs to navigate to URLs, either in a new tab or the active tab. That’s exactly what the navigate tool provides:
#!/usr/bin/env node
import puppeteer from “puppeteer-core”;
const url = process.argv[2];
const newTab = process.argv[3] === “–new”;
if (!url) {
console.log(“Usage: nav.js
The agent needs to execute JavaScript to read and modify the DOM of the active tab. The JavaScript it writes runs in the page context, so it doesn’t have to fuck around with Puppeteer itself. All it needs to know is how to write code using the DOM API, and it sure knows how to do that:
#!/usr/bin/env node
import puppeteer from “puppeteer-core”;
const code = process.argv.slice(2).join(” “);
if (!code) {
console.log(“Usage: eval.js ‘code’“);
console.log(“\nExamples:“);
...
Read the original on mariozechner.at »
I don’t remember the first time I held a machete, because I’ve never held one. Most members of the BaYaka — a group of nomadic hunter-gatherers found in the Congolese rainforests — probably don’t remember either, but for different reasons. Early memory has its limits. Among the BaYaka, picking up a machete is developmentally akin to language, walking, and chewing solid food.
So goes a BaYaka childhood. Children wander the forests in packs. They climb saplings and bathe in the rivers. They conduct day-long fishing trips: their parents glance toward them as they organize themselves, then let the kids go on their way.
A few months ago, an anthropologist named Gül Deniz Salalı documented these dynamics in a brilliant documentary (linked below), which I cannot recommend highly enough. To our eyes, their lives are utterly strange. We shouldn’t forget how lucky we are to live in a time where we can see such wonders from the comfort of a chair.
But the BaYaka childhood isn’t a novelty. As I’ll discuss shortly, it is probably the norm for our species. And that means something has gone terribly wrong in the West.
Consider some statistics on the American childhood, drawn from children aged 8-12:
* 45% have not walked in a different aisle than their parents at a store;
* 56% have not talked with a neighbor without their parents;
* 61% have not made plans with friends without adults helping them;
* 62% have not walked/biked somewhere (a store, park, school) without an adult;
* 63% have not built a structure outside (for example, a fort or treehouse);
* 71% have not used a sharp knife;
Meanwhile, 31% of 8-12 year olds have spoken with large language models. 23% have talked to strangers online, while only 44% have physically spoken to a neighbor without their parents. 50% have seen pornography by the time they turn 13.
In physical space, Western children are almost comically sheltered. But in digital space, they’re entirely beyond our command; and increasingly, that’s where children spend most of their time. You don’t need me to tell you about the dire consequences of that shift.
Why do our children spend more time in Fortnite than forests? Usually, we blame the change on tech companies. They make their platforms as addicting as possible, and the youth simply can’t resist — once a toddler locks eyes with an iPad, game over.
I want to suggest an alternative: digital space is the only place left where children can grow up without us. For most of our evolutionary history, childhood wasn’t an adult affair. Independent worlds and peer cultures were the crux of development, as they still are among the BaYaka; kids spent their time together, largely beyond the prying eyes of grown-ups.
But in the West, the grown-ups have paved over the forests and creeks where children would have once hidden. They have exposed the secret places. So the children seek out a world of their own, as they have for millennia, if not longer. They find a proverbial forest to wander. They don’t know what we know: this forest has eyes and teeth.
In most human societies, children have spent much of their time exploring and playing within independent peer cultures. This term reflects two important features of human childhood. First, the groups consist entirely of other children. Second, they are functionally and culturally distinct from adult society; they exist alongside but apart from the world of adults.
The evidence for this pattern is rich and widespread. During his research among the Trobriand Islanders, for instance, Bronislaw Malinowski described the “small republic” of children that “acts very much as is its own members determine, standing often in a sort of collective opposition to its elders.”
Margaret Mead reported similar patterns in Samoa. One group of young girls formed a cohort “which played continually together and maintained a fairly coherent hostility towards outsiders.” Of their activities, she writes:
On moonlight nights they scoured the villages alternately attacking or fleeing from the gangs of small boys, peeking through drawn shutters, catching land crabs, ambushing wandering lovers, or sneaking up to watch a birth or a miscarriage in some distant house . . . They were veritable groups of little outlaws escaping from the exactions of routine tasks. (Coming of Age in Samoa, p. 62).
These peer cultures don’t like to be seen — many ethnographers have noticed how playgroups prefer to segregate themselves from adults. Among the Mbuti, another Central African foraging group, Colin Turnbull observed children spending most of their time in a bopi, a playground set away from the main camp:
The water was fairly shallow there, and all day the long the children splashed and wallowed about to their hart’s content . . . Infants watched with envy as the older children swung wildly about, climbing high up on the vine strands and performing all sorts of acrobatics. (The Forest People, p.128).
Even in industrial societies, this love of solitude shows up. In the 1950s, Iona and Peter Opie documented the behaviors of post-war British children. They noticed that the children liked to roam through bomb sites, where they would build fires and play hide-and-seek.
It is possible to see these interests playing out in ancient societies, too. A paper by Ella Assaf and colleagues documented handprints, footprints and paintings left in Paleolithic caves, all probably produced by children. Through involvement in ritual activities, they propose, “Upper Paleolithic children were able to actively shape their own reality as individuals, as well as the reality of their community and its well-being.”
Why do independent peer cultures emerge so reliably in our species? Anthropologists tend to view childhood as a period during which we over imitate and copy our way to cultural competency. But if that’s true, it seems hard to account for the fact that kids really, really want to get away from adults. They would rather spend time with each other. If childhood is all about efficient cultural transmission, children should be hanging on our every word.
As of yet, I don’t think there’s a good explanation. Maybe independent peer cultures expose children to a wider variety of information. Maybe they provide safe spaces for the mimicry of adult activities. Maybe adults just aren’t very much fun. As Antoine de Saint-Exupéry writes in The Little Prince: “All grown-ups were children once, but only a few of them remember it.”
All of these accounts are probably a little bit true. The important point is that kids want to spend time together, in their own space, away from the tiresome grown-ups.
Which is relatively easy, if you’re living in a giant forest with parents who don’t pay much attention to your activities. But our children now find themselves in a strange situation — they have nowhere to hide, and even if they did, we might not let them go there in the first place.
Over the past few decades, childhood mobility in the West has dropped precipitously. You might think that the change has something to do with the emergence of the Internet. But longitudinal data suggests otherwise. Check out these statistics from a decades-long survey on the mobility of English children:
At least in England, independence isn’t an Internet problem. Drop-offs in English childhood mobility have been ongoing since the 1970s. In 1971, 80% of seven and eight-year-old children went to school unaccompanied by an adult, and 55% of children under ten-years-old were allowed to travel alone to places other than school within walking distance. By 1990, those numbers had dropped to 9% and 30%, respectively.
We see similar patterns in other places. Here’s data from Sweden, for instance.
In the United States, similarly, there was a drop-off from 42% (1969) to 16% (2001) in the number of children who walked or biked to school alone.
So it doesn’t seem like the collapse of independent mobility is a phone issue. The truth is much more complicated.
One element is parental attitudes: according to responses from a survey by Play England, many parents fear “stranger danger” or judgement from neighbors if they let their kids play unsupervised outdoors.
Adult employment patterns and lifestyle changes have also been slowly trending toward car-dependency, which means that kids often end up living far away from their friends. If children want people to play with, the most efficient solution is for their parents to drive them to an organized sport or other structured activity.
In the Play England survey, though, parents were most afraid that their kids would get hit by a car. Sadly, this isn’t an unreasonable fear. All the forests are covered in concrete. What would we make of a city-bound parent who let their toddler roam the streets without an adult nearby?
Unsurprisingly, these changes have been very bad for children. In 2013, UNICEF tracked childhood well-being against independent mobility, and their results reveal a stark correlation.
A mix of shifting parental attitudes, car-dependency, and urbanization have led to an unprecedented situation in the history of human childhood. Children don’t have peer cultures, because they have nowhere to play and no one to play with. Even if those factors were ameliorated, we might not let them out anyway. Too bad for them — they can’t hide from us anymore.
Or can they?
The kids won’t get off their damn phones. Children aged 6-14 average nearly three hours per day of screen time, not counting schoolwork. 50% of teenagers average over four hours of screen time daily. And so on — you know the statistics.
Amidst all the horror stories, though, people often ignore the fact that the kids don’t like it either. For example: most teens, especially girls, say they spend too much time on their smartphones. 72% of 8 to 12-year-olds say they would rather “spend most of there time together doing things in-person, without screens.” The kids are not alright, but they aren’t dumb — they understand that something is wrong with the technological world we’ve handed them.
So why don’t they just stop?
Usually, we blame the corporations. And there are lots of fingers to point. It is now well-known, I think, that many platforms design their applications like slot machines to maximize our attention, and that they purposefully aim for the more malleable minds of children.
But data on the varieties of screen usage suggests another explanation. Social media and gaming are now arguably the two most common digital activities for children: notably, and unlike television, both allow children to engage with distinct virtual communities that adults don’t notice or understand.
Tellingly, kids really want those communities to exist. 45% of American 8 to 12-year-olds say they would prefer to “participate in an activity with their friends in person that’s not organized by adults.” 61% wish they had more time to “play with friends in person without adults.” And again, most of them wish they could spend less time on screens. It seems like what they want is to wander together in a forest.
But they can’t. So they boot up Fortnite or TikTok instead.
By retreating to digital space, children have found an open frontier that lies beyond the interest or comprehension of their parents. We don’t know how to play Roblox, or what “6-7” means. These worlds are so impenetrable that The Guardian writes explanatory articles to keep us at least slightly up-to-date.
But the kids know, and they know that the other kids know, and they know that we don’t know shit. Children have always looked for a world without us. With the advent of the Internet, they have found one lying in the rubble.
Of course, the problem is that social media platforms and video games are not Congolese rainforests — though at least there are no leopards. These digital spaces make our children depressed and anxious and insecure. They expose them to pornography. They drive them into frightening political rabbit holes. The children need somewhere to play, but this can’t possibly be the right place for it.
And yet, it’s hard to picture an alternative. Parents should worry less about stranger danger: fine. But everything is still covered in concrete, and everyone is still moving to cities. Do we want to tear down the skyscrapers and unpave the roads? Should we demand a forest for our children?
The truth is that we’re not going back. My children will not grow up like the BaYaka, and there is nothing I can do about it.
But that doesn’t change the fact that kids need their independent peer cultures. If we can’t provide physical spaces for them to form, then we must accept that they will often form in digital spaces instead. So if we’re unhappy with the digital spaces on offer — if we think there are too many figurative leopards in those forests — then we should make something better.
What does ‘something better’ look like? Well, probably a platform that preserves the aspects of digital space that kids find freeing, but without the aspects that make those spaces dangerous and addictive.
Games like Roblox present some interesting ideas. Children absolutely love Roblox; according to the game’s parent corporation, their monthly player base includes half of all American children under the age of 16. They love it because it’s multiplayer, exploratory, and extremely open-ended. Independent cultures and systems of governance emerge in the course of gameplay. They’re sufficiently complex that some of my colleagues are now using Roblox as a playground for studying collective behavior in humans.
But there are adults in Roblox, hidden behind avatars. Their presence ensures that kids will end up seeing disturbing content that they don’t have the tools to understand. Roblox is also highly Vegasified, featuring all the usual exploitative tactics of loot boxes, season upgrades and cosmetic passes. So it is nowhere near the ideal; all things considered, children would probably be better off if we banned them from playing.
Still, I can picture something like Roblox that might also sustain peer cultures. When I remember the happiest parts of my childhood, everything centers on secret places: pillow forts, hidden corners in parks, soccer fields after dark. But I also remember Minecraft. My friends and I would set up a Skype call, start a world and spend hours exploring and building.
When we spent time together in physical space, we were nearly always being supervised. Not so in Minecraft. I spent thousands of hours mastering the rules of that procedurally generated world; my parents didn’t even know what ‘procedurally generated’ meant. So, dystopian as it may sound, Minecraft servers were the closest thing I had to sprawling Congolese rainforests.
Perhaps that isn’t so bad. I wish the children of today had a forest. But they don’t. They’re making do with what history has handed them.
We can complain about their screen time, lament the anxious generation, scoff at how ‘unnatural’ this brave new world has become. Simultaneously, though, we should do our best to understand why kids are behaving this way. There’s no point in whining about the impulses endowed to them by several hundred thousand years of evolution. Don’t hate the player; hate the game. And if you really hate the game, make a better one.
...
Read the original on unpublishablepapers.substack.com »
The Pragmatic Programmer: From Journeyman to Master by Dave Thomas and Andrew Hunt was given to me as a gift after an internship. The book gave me invaluable advice as I started out in my career as a professional software engineer. Re-reading it a decade later, I thought the general advice still held up well, but it made references to technologies such as CORBA that are no longer used and felt dated as a result. The authors agreed and wrote a 20th anniversary edition that was updated for modern developers. A third of the book is brand-new material, covering subjects such as security and concurrency. The rest of the book has been extensively rewritten based on the authors’ experience putting these principles into practice. We discussed the 20th anniversary edition in my book club at work.
The book is meant for those just starting out in the world of professional software engineering. Many of the tips, such as Tip 28: Always Use Version Control will seem obvious to experienced hands. However, it can also be a guide for senior developers mentoring junior developers, putting actionable advice into words. The book is also valuable to those who lack a formal CS education; it explains things like big-O notation and where to learn more about these subjects. I think that any software engineer will get one or two things out of this book, though it’s most valuable for beginners.
One of the things I appreciate about the book is that they talk about applying the principles not only to software engineering but to writing the book as well. The book was originally written in troff and later converted to LaTeX. For example, to illustrate Tip 29: Write Code That Writes Code they wrote a program to convert troff markup to LaTeX. In the 20th anniversary edition, they talk about their efforts to use parallelism to speed up the book build process and how it led to surprising bugs.
Perhaps the best thing about the book is that the authors summarize their points into short tips highlighted throughout the book. The authors helpfully attach these tips to a card attached to the physical book. This makes it easy to remember the principles espoused in the book and to refer to them later. I think this is a feature that more books should include, especially managerial or technical books.
The first chapter is less about coding and more about the general principles a pragmatic programmer follows. Most of all, it’s about taking responsibility for your work. The first tip of the chapter is Tip 3: You Have Agency: if you don’t like something, you can be a catalyst for change. Or you can change organizations if change isn’t happening. The most important tip of the chapter to me is Tip 4: Provide Options, Don’t Make Lame Excuses. In this section, they discuss taking responsibility for the commitments you make and having a contingency plan for things outside your control. If you don’t meet the commitment, provide solutions to fix the problems. Don’t tell your boss, “The cat ate my source code.”
Software rots over time without efforts to fix it. The authors talk about broken windows policing, the theory that minor problems such as a single broken window give people the psychological safety to commit larger crimes. Regardless of whether broken windows policing is actually true, the metaphor applies to software. This leads to Tip 5: Don’t Live with Broken Windows: If you see a broken window in your software, make an effort to fix it, even if it’s only a minor effort to board it up. This may seem impractical if your project already has a lot of broken windows, but this tip helps you avoid creating such an environment in the first place. In my experience, it works: when we set up a new project at work, we made a commitment to use git commit hooks to enforce coding standards. This made each of us more reluctant to compromise on software to begin with, and all of the code was a good example to copy from.
A pragmatic programmer is always learning, and learns things outside their specialty; they are a jack of all trades. Even if they are a specialist in their current role, they invest regularly in a broad knowledge portfolio. In addition to software skills, people skills are important as well. The section “Communicate!” shows how to effectively communicate your ideas, such as how to present, what to say, and how pick the right time. In the words of Tip 11: English is Just Another Programming Language. If you don’t have an answer to an email immediately, respond with an acknowledgment and that you’ll get back to them later - nobody wants to be talking to a void. Don’t be afraid to reach out for help if you need it; that’s what your colleagues are there for, after all. And don’t neglect documentation! Make it an integral part of the development process, not an afterthought.
Finally, the principles in this book are not iron-clad: you must consider the tradeoffs between different values and make the right decision for your project. Your software does not need to be perfect. When working on software, involve your users in deciding what quality issues are acceptable in return for getting it out faster. After all, if you wait a year to ship the perfect version, their requirements will change anyways. As Tip 8 says: Make Quality a Requirements Issue.
Why is decoupling good? Because by isolating concerns we make each easier to change. ETC.
Why is the single responsibility principle useful? Because a change in requirements is mirrored by a change in just one module. ETC.
Why is naming important? Because good names make code easier to read, and you have to read it to change it. ETC!
However, the authors also stress that ETC is a value, not a rule. For example, ETC may not be appropriate for writing code that has high performance requirements; making the code complex to achieve the performance requirements is an acceptable tradeoff.
They then turn to another important acronym for implementing ETC in Tip 15: DRY—Don’t Repeat Yourself. DRY makes things easier to change by having one place to change anything. Worse, if you forget to make a change, you’ll have contradictory information in your program that could crash it or silently corrupt data.
What kind of duplication is there?
Code Duplication: For example, having a case statement duplicated across several different places rather than in a single function.
Documentation Duplication: Some people believe that every function needs a comment. If you do this, you will also have to update the comments each time the function changes. Ask what your comment adds to the code before writing it!
Data Duplication: Caching an expensive result and forgetting to update the cache when the source data changes.
Representational Duplication: When you work with external API, the client and server must adhere to the same format in order to work. If one changes, the other side will break Having a common specification, such as openAPI allows you to integrate more reliably with the service.
Interdeveloper duplication: When two developers do the same work. This can be mitigated by Tip 16: Make It Easy to Reuse. If it’s hard to use your code, other developers will be tempted to duplicate it.
A closely related principle to DRY is Orthogonality. Two components of a software system are orthogonal if changes in one do not effect the other. Systems should be designed as a set of cooperating independent modules, each of which has a single, well-defined purpose. Modules communicate between themselves using well defined interfaces and don’t rely on shared global data or the implementation details of another module. Unless you change a component’s external interfaces, it should not cause changes in the rest of the system. Orthogonal systems are easier to test, because more testing can be done at the module level in unit tests rather than end-to-end integration tests that test the whole system.
Often, when starting a software project, there are a lot of unknowns. The user has an idea of what they want, but there’s some ambiguity in the requirements. You don’t know if the library and frameworks you pick will work nicely together. The solution here is Tip 20: Use Tracer Bullets to Find the Target. In a machine gun, tracer bullets are bullets that glow in the air, enabling the user to see if they’re hitting the target at night. Tracer Bullet Development provides that kind of immediate feedback. Look for a single feature that can be built quickly using the architectural approach you’ve chosen, and put that in front of the users. You may miss; users may say that’s not quite what they wanted. But that’s the point of tracer code: it allows you to adjust your aim with a skeleton project that’s easier to change than a final application. Users will be delighted to see something working early, and you’ll have an integration platform to build the rest of the application on.
Tracer code is different from prototypes. To the authors, prototypes are disposable code used to learn about a problem domain, never meant to be used in production. Prototypes don’t even have to be code. A UI can be mocked up in an interface builder, or an architecture mapped out with post-it notes. In terms of Tip 21: Prototype to Learn. In contrast, tracer bullet code is meant to be part of the final application.
The final tip of this chapter I bring up is Tip 18: There Are No Final Decisions. Decisions should be reversible; if you rely on MySQL today, you may find yourself needing to switch to Postgres six months from now. If you’ve properly abstracted the database logic, making this change should be easy. Marketing may decide that your web app should be a mobile app in the future; if your architecture is built well, this extra demand should not be a burden. This is one tip I disagree with: I think it can easily be taken too far. If you provide too much reversibility, you’ll end up with over-abstracted code with configuration options that are never used. I think it’s more reasonable to think about what decisions can reasonably change and make them flexible; if you spend all your time trying to cover for every possibility, you’ll never get around to actually coding the required functionality.
This chapter focuses on how to make the most out of your tools, what tools to invest in, and how to approach debugging. The first bit of advice: Tip 25: Keep Knowledge in Plain Text. By plain text, they mean keep knowledge such as configuration or data in a simultaneously human-readable and computer readable format. Plain text insures you against obsolesce; you can always write something to parse it later, while reverse-engineering a binary format is significantly harder. In addition, almost any other tool in existence can process plain text in some way, so you’ll have an extensive suite of other tools to use. As an extension of the power of plain text, they also suggest you master a command shell such as bash. Shells provide a family of tools that are composable with each other, and can be combined as much as your imagination allows. A GUI in contrast, limits you to the actions the programmers of the GUI thought of in advance. Finally, you should learn a text processing language such as awk or perl to get the most out of text - the authors used perl (first edition) and ruby (20th anniversary edition) to automatically highlight the source code in the book, for example.
The next topic the authors turn to is debugging. Debugging is the main task a software engineer does throughout their day, so it’s essential you get good at it. Defects show up in a variety of ways, from misunderstood requirements to coding errors. Some cultures try to find someone to blame for a defect; the authors think you should avoid that with Tip 29: Fix the Problem, Not the Blame.
They give the following tips on debugging your code:
Tip 30: Don’t Panic: It’s easy to panic when you’re on a tight deadline or a client is angry at you. However, take a deep breath and think about the problem at hand. The cause of the bug may be several layers removed from what you’re seeing, so try to focus on root causes rather than fixing the symptoms.
The Impossible has Happened: If you think to yourself “that’s not possible” - you’re wrong. It’s clearly possible, and it’s staring you in the face.
Reproduce It!: Find a minimal case that triggers the bug, whether that be a certain input data set, or pattern of actions. Once you can reliably cause the bug, you can trace it through your code.
Tip 32: Read the Damn Error Message: Enough said.
The Operating System is Fine: It’s possible that you found a bug in the Linux kernel or postgres, but these are extensively battle-tested applications. It’s much more likely that the problem is in your code.
The Binary Chop: Cut things in half until you find the problem. This massively decreases the search space you have to work in. If you have a long stack trace and are trying to find which function mangled the value, log the value halfway through. If the value is fine, log the value halfway through the next half, or if it’s mangled, halfway through the previous half, and so on. If a release introduces a regression, find a version that’s fine, and binary chop through the commits to find the commit that introduced the bug.
Use a Debugger and/or Logging Statements: Debuggers allow you to step through the code and inspect the values of variables, finding the exact point where things go wrong. In environments where a debugger is not available, logging statements can show you how a variable changes in time, or just how far the program got before crashing.
Rubber Ducking: Explain the bug to a colleague, or talk out loud to a rubber duck. You don’t have to get a response, by verbalizing your assumptions you may gain sudden insight into the problem.
Once you’ve solved the bug, however there’s still one more step: you should write a test to catch that bug in the future.
Tip 36: You Can’t Write Perfect Software starts off the chapter. While we’d like to write perfect software, there will always be bugs, poor design decisions, and missing documentation. The theme of this chapter is how to design this fact in mind.
The first idea they propose is Design By Contract. Similar to legal contracts, it explains a function or module’s rights and responsibilities. A contract has three parts: It has Preconditions: things that must be true when it is called, such as what qualifies as valid inputs. Postconditions are what will be true when it is done, such as a sort routine returning a sorted array. Finally, Invariants are things that are always true from the caller’s perspective - they may change while the routine is running, but will hold at the beginning and the end of the call. For example, in a sort routine, the invariant is that the list to be sorted will contain the same number of items when it started as when it finished. If the contract is violated, the contract will specify what to do, such as crash or throw an exception.
Some languages, such as Clojure have built-in semantics for design by contract, with explicit pre- and post- conditions. However, if your language doesn’t support contracts, you can implement them with Tip 39: Use Assertions to Prevent the Impossible. You can assert that the conditions of your contract are true, and handle the cases where the contract is violated. If you don’t know what to do when a contract is violated, the authors recommend Tip 38: Crash Early. It’s better that you crash rather than write incorrect data to the database. After all, dead programs tell no lies. Of course, crashing immediately may not be appropriate - if you have resources open make sure to close them before exiting.
The final paranoid tip is Tip 43: Avoid Fortune-Telling. Pragmatic programmers only make decisions that they can get immediate feedback on. The more predictions you make about the future, the more likely you’ll get some of the predictions wrong and make the wrong decision based on them.
You might find yourself slipping into fortune telling when you have to:
In a previous chapter, the authors wrote about making decisions reversible and easier to change. This chapter tells you how to implement it in your code. The key here is to make your code flexible rather than rigid - good code bends to circumstances rather than breaks. Part of this is decoupling code. Code is consider coupled when they share something in common. This may be something as simple as a shared global variable, or something more complex like an inheritance chain.
The authors argue against what they term Train Wrecks - long chains of method calls, such as this example they give:
This code is traversing many different levels of abstraction - you have to know that a customer object exposes orders, that orders have a find method, and that the order find returns has a getTotal method. If any of these levels of abstraction are changed, your code might break. And requirements may change; What if the business decides to implement a maximum discount amount of 40%? Certainly, this could be applied in the applyDiscount routine, but anything could modify the grandTotal and discount fields - this rule could be violated if other modules modifying the totals object don’t get the memo.
The authors suggest refactoring the code so that there is no orders object, just a find method and an applyDiscount method for the order object that implements the 40% rule:
The authors suggest having only one . when you access something if that something is likely to change, such as anything in your application, or a fast moving external API. This includes using intermediate variables between accesses, such as this code:
However, the rule does not apply to things that are unlikely to change, such as core language APIs. So this code is ok:
Another source of coupling is globally accessible data. Global data makes it hard to reason about the state of a program, since any other module might be able to change it. Global data includes design patterns such as singletons, and external resources such as databases. Given how extensive global resources are, how can one avoid them? If global data is unavoidable, the key is to manage them through a well-defined API that you control, rather than allowing anything to read and write global data. In the words of Tip 48: If It’s Important Enough to Be Global, Wrap It in an API.
Poor use of inheritance is a third source of coupling. Inheritance is used for two reasons: code reuse and type modeling. Inheritance doesn’t work for code reuse; Not only is the code of a child class coupled to any ancestor of the class, so is any code that uses the class. Things may unexpectedly break when an ancestor changes an API, even if you are using a subclass.
Nor does inheritance work for modeling types. Class hierarchies quickly become tangled, wall covering monstrosities. Another problem is multiple inheritance. A Car may be a type of Vehicle, but it may be an Asset or InsuredItem. Multiple inheritance is required to model this, and many OO languages don’t support multiple inheritance. Instead of paying the inheritance tax, the authors suggest using:
Interfaces or Protocols are classes that contain no code but instead contains behaviors. A class that implements an interface promises to define the behaviors. For example, a Car might implement Drivable which has methods such as accelerate and brake. Interfaces can be used as types, and any class that implements the interface will be compatible with that type. This is a much easier way to provide polymorphism than inheritance.
Another alternative to inheritance is delegation. If you want to include behavior from class Foo add a member of type Foo to your class rather than inherit from Foo. You can then use Foo’s API wrapped in code you control. Delegation is a has-a relationship rather than a is-a relationship.
The problem with interfaces and delegation is that they require writing lots of boilerplate code. For example, it’s likely that most of your classes that implement Drivable will have the same logic for brake, but each class will have to write it’s own implementation of brake. This leads to repeated code across your codebase, violating the DRY principle. To resolve this, the authors turn to Mixins - sets of functions that can be “mixed into” a class. This allows you to add common functionality without using inheritance. I wonder how mixins are implemented in a language like Java, which doesn’t have an obvious version of that feature. It’s also not clear to me how mixins are different from inheritance; aren’t they just a form of multiple inheritance?
Tip 55: Parameterize Your App Using External Configuration: Code may have values that change while the application is running, such as credentials for for third-party services. Rather than directly including the values in your code, you should externalize them and put them in a configuration bucket. Keeping credentials in source code is a security risk - hackers scan public git repositories for common security credentials, such as AWS keys. It’s common to store them in a flat file or database tables, and read them when the application initializes. However, in our world of highly-available applications that’s not as appropriate. Instead the authors propose configuration-as-a-service, where configuration is stored behind a service API. This allows multiple applications to share configuration information, use access control to control who can see and edit configuration, and provide a UI to easily edit config information. Using the configuration service, applications can subscribe to a configuration item and get notifications when they change. This allows applications to update config data on their side without restarting.
This chapter deals with Parallelism, where two pieces of code run at the same time, and Concurrency, where things act as if they run at the same time. In the real world, things are asynchronous - the user is supplying input, network resources are called, and the screen is being redrawn all at the same time. Applications that run everything serially feel sluggish.
In Tip 56: Analyze Workflow to Improve Concurrency the authors advocate that you break temporal coupling where possible. Temporal Coupling is when your code depends on event A happening before event B. You should look at your workflow to see what can be executed concurrently. Look for activities that take a lot of time that would allow for something else to be done in the meantime. If your application makes multiple independent API calls to a remote service, execute them on separate threads rather than serially, then gather up the results of each call. If your workflow allows a way to split the work into multiple independent units, take advantage of those multiple cores and execute them in parallel.
Of course, parallelism has its pitfalls as well. For example, imagine reading an integer, incrementing it, and writing it back. If two processes read that integer at the same time, they will each increment the value to n+1, when you want it to be n+2. The update needs to be atomic; each process needs to do this sequentially without the other process interfering. This can be done through synchronized methods, semaphores, or other forms of resource locking. However, they have their own dangers as well, such as deadlocking, where two processes each get a lock on one of two needed resources, but not the other. Each waits forever for the other to release its lock. The authors think you should avoid shared state rather than try to handle yourself wherever possible; Tip 57: Shared State Is Incorrect State.
The authors ran into this issue when writing the 20th anniversary edition: they updated the build process for the book to utilize parallelism. However, the build would randomly fail. The authors tracked this down to changing the directory temporarily. In the original, a subtask would change directory, then go back to the original directory. However, this no longer worked when new threads started, expecting to be in the root directory. Depending on the timing, this could break the build. This prompted them to write Tip 58: Random Failures Are Often Concurrency Issues.
Chapter 7: While You Are Coding
This chapter is more of a grab-bag. It covers subjects such as psychology, big-O notation, refactoring, security, and testing.
In Tip 61: Listen to Your Inner Lizard the authors talk about listening to your instincts (your lizard-brain). If you find yourself having a hard time writing code, your brain is trying to tell you something. Perhaps the structure or design is wrong, or you don’t fully understand the requirements. If you find yourself in this situation, take a step back and think about what you are doing. Maybe go for a walk, or sleep on it. You might find that the solution is staring you in the face when you come back.
Perhaps you need to refactor the code instead of writing more. Refactoring is a continuous process, espoused in Tip 65: Refactor Early, Refactor Often. If anything strikes you as wrong in your code, such as DRY violations, outdated knowledge or non-orthogonal design, don’t hesitate to fix it. When you are refactoring, make sure you have a good suite of unit tests beforehand to test if your changes break anything. Run the tests frequently to check if you’ve broken anything.
Speaking of tests, the authors start with a bold assertion: Tip 67: Testing Is Not About Finding Bugs. Instead, tests function as the First User of Your Code - a source of immediate feedback, and immediately forces you to think about what counts as a correct solution. In addition, tightly coupled code tends to be hard to test, so it helps you make good design decisions. The authors emphatically do not think you should adopt full-on Test Driven Development - it’s too easy to become a slave to writing tests. They note an example of a TDD advocate starting a sudoku solver using TDD and spent so much time writing the tests they failed to write the solver itself!
In a sidebar, Dave Thomas explains that he stopped writing tests for a few months, and said “not a lot” happened. The quality didn’t drop, nor did he introduce bugs into the code. His code was still testable, it just wasn’t tested.
Andy says I shouldn’t include this sidebar. He worries it will tempt inexperienced developers not to test. Here’s my compromise: Should you write tests? Yes. But after you’ve been doing it for 30 years, feel free to experiment a little to see where the benefit lies for you.
This chapter focuses on how to start your project on the right foot. The first subject the authors tackle is requirements gathering: The Requirements Pit. While we talk about gathering requirements as if they are on the ground, waiting to be picked up, requirements are non-obvious because of Tip 75: No One Knows Exactly What They Want. They think of requirements gathering as a kind of therapy, where you take an initial requirement and ask questions about the details to nail down exactly what they need. The authors show an example of a simple requirement: “Shipping should be free on all orders costing $50 or more”. Does that include the shipping cost itself? Tax? If you’re selling ebooks as well, should they be included? The job of the programmer is Tip 76: Programmers Help People Understand What They Want. You should find any edge cases the client may not have considered and make sure they’re documented. This doesn’t mean creating long specifications the client won’t read. Instead, the authors think requirements should be able to fit on an index card. This helps prevent feature creep; if the client understands how adding one more index card will impact the schedule, they’ll consider the tradeoffs and prioritize the requirements they need the most.
You are given constraints in your requirements as well. Your job as a software engineer is to evaluate if those constraints are things you actually have to live with or if you can relax them. In the words of Tip 81: Don’t Think Outside the Box—Find the Box, the constraints are the edges of the box. What you initially thought of as a constraint may actually be an assumption you held.
Another tip the authors advocate for is Tip 78: Work with a User to Think Like a User. If you’re building an inventory system, work in the warehouse for a few days to get an idea of their processes and how your system will be used. If you don’t understand how it will be used, you could create something that meets all of the requirements but is totally useless. They cite an example of a digital sound mixing board that could do anything to sound that was possible, yet nobody wanted to use it. Rather than take advantage of recording engineers’ experience with tactile sliders and knobs, they built an interface that was unfamiliar to them. Each feature was buried behind menus and given unintuitive names. It did what was required, but didn’t do it how it was required.
The authors also consider in this chapter what it means to be Agile. Many teams and companies are eager for an off-the-shelf solution: call it Agile-in-a-Box. But no process can make you Agile; “Use this process and you’ll be agile” ignores a key part of the Agile manifesto: Individuals and interactions over processes and tools. To the authors Agile can be boiled down to the following:
Work out where you are.
Make the smallest meaningful step towards where you want to be.
Evaluate where you end up, and fix anything you broke.
Do this for every level of what you do, from process to code, and you’ll have adopted the Agile spirit.
Can the lessons of The Pragmatic Programmer be applied to teams too? The authors say yes. This chapter focuses on how to apply the lessons of the previous chapters to the team level. Many of the lessons are the same as those mentioned previously, so I won’t go into them again.
The authors advise Tip 87: Do What Works, Not What’s Fashionable. Just because Google or Facebook adopts process $ x $ doesn’t mean it’s right for your team. How do you know if something works? Try it. Pilot an idea with a small team, and see what works about it and what doesn’t. The goal isn’t to “do Scrum” or “be Agile”, but to deliver working software continuously. When you adopt a new idea, you should do it with improving continuous deployment of software in mind. If you’re measuring your deployments in months, try to get it down to weeks instead. Once you get it down to weeks, try to deliver in one-week iterations.
Related to continuously delivering software is Tip 96: Delight Users, Don’t Just Deliver Code. Delivering working software in a timely matter is not enough to delight your users; that is merely meeting expectations. The authors suggest you ask your users a question:
How will you know that we’ve all been successful a month (or a year, or whatever) after this project is done?
The answer may not be related to the requirements, and may surprise you. For example, a recommendations engine might be valued on driving customer retention. But once you know what the secret to success is, you should aim not just to hit the goal but to exceed it.
Finally, take pride in your work. The final tip of the book is Tip 97: Sign Your Work.
I was only able to cover a portion of this remarkable book in this review. I highly recommend this book to any software engineer, especially to those just starting out in the field. It makes a great graduation gift to someone just finishing their CS degree.
<< Previous
...
Read the original on www.ahalbert.com »
To add this web app to your iOS home screen tap the share button and select "Add to the Home Screen".
10HN is also available as an iOS App
If you visit 10HN only rarely, check out the the best articles from the past week.
If you like 10HN please leave feedback and share
Visit pancik.com for more.