MoreRSS

site iconThe Practical DeveloperModify

A constructive and inclusive social network for software developers.
Please copy the RSS to your reader, or quickly subscribe to:

Inoreader Feedly Follow Feedbin Local Reader

Rss preview of Blog of The Practical Developer

Google Workspace Studio: Build a Sales Lead Assistant

2026-04-21 18:45:34

Learn how you can build an assistant with Google Workspace Studio that helps you label incoming sales emails.

0:00 Intro
0:10 What the flow does
0:34 Build the flow
2:24 Test the flow and turn it on
2:50 The flow in action

Learn how to extract data from an email attachment: https://youtu.be/ZQ24QTudlhw

Learn more about Google Workspace Studio ➡️ https://www.youtube.com/playlist?list=PLDdffPXqmxKNtTUF7H3mab3HEnXzxRi8V

Subscribe to our YouTube channel: https://www.youtube.com/@googleworkspacedevs/

#WorkspaceStudio #GoogleWorkspace #Gemini

Follow youtube.com/@googleworkspacedevs

Building My Digital Soul: The Brutal Truth About 6 Months of Failed Identity

2026-04-21 18:45:32

Building My Digital Soul: The Brutal Truth About 6 Months of Failed Identity

Honestly, I started this "digital soul" project with grand ambitions. I was going to create an AI system that truly understands me, that learns my patterns, that evolves with me. Six months later, I'm here to tell you the brutal truth: I've mostly built a system that reminds me how little I actually understand myself.

The Dream vs. The Nightmare

It started innocently enough. I wanted a digital representation of my identity, something that could interact with my various systems, understand my preferences, and maybe even help me make decisions. The vision was beautiful - a perfect digital twin that would make my life easier, more efficient, somehow... more human.

So here's the thing: I completely underestimated the complexity of human identity. My "digital soul" project has morphed through three distinct phases of failure, each more humbling than the last.

Phase 1: The Complex Architecture Dream (2 months)

  • 47 different personality traits tracked
  • Machine learning models trying to "understand" my behavior patterns
  • Complex decision trees for every possible interaction
  • Result: A system so complex it made simple decisions harder than they needed to be

Phase 2: The Simplification Retreat (2 months)

  • Reduced to 12 core traits
  • Rule-based instead of ML-driven
  • Focus on practical functionality
  • Result: Better, but still felt like I was creating a digital stranger pretending to be me

Phase 3: The Reality Check (2 months)

  • 3 essential identity markers only
  • Complete rewrite of the core philosophy
  • Focus on authenticity over functionality
  • Result: A system that's mostly useless but surprisingly honest about its limitations

The Brutal Statistics (Because Numbers Don't Lie)

Let me give you the raw numbers that tell the real story:

  • Hours invested: 347 hours (that's nearly 9 full work weeks)
  • System versions: 7 complete rewrites
  • Personality traits tracked: 47 → 12 → 3 (down 94%)
  • Decision accuracy: 67% (worse than random chance in many cases)
  • User satisfaction: 4.2/10 (from the few brave souls who tested it)
  • GitHub stars: 1 (that's you, reading this right now)
  • ROI: Negative infinity

The most brutal part? My "digital soul" system has made me question my actual identity more than it has helped me understand myself.

The Unexpected Discovery: Digital Identity Is a Trap

I learned something shocking in this process: trying to digitize your identity might be the most anti-human thing you can do. Here's why:

// What I thought I was building
class DigitalSoul {
  constructor(humanData) {
    this.identity = this.analyzeIdentity(humanData);
    this.behaviors = this.patternMatch(humanData.behaviors);
    this.preferences = this.learnPreferences(humanData.interactions);
  }

  // Complex AI-driven "understanding"
  understand(input) {
    return this.neuralNetwork.predict(input);
  }
}

// What I actually ended up with
class HumbledIdentity {
  constructor() {
    this.uncertainties = ["everything"];
    this.biases = ["confirmed"];
    this.blindSpots = ["countless"];
    this.confidenceLevel = "who even knows";
  }

  // Brutal honesty about limitations
  understand(input) {
    return "I don't understand, but I'm working on it... probably";
  }
}

The more I tried to capture my identity in code, the more I realized that identity is messy, contradictory, beautiful, and completely un-digitizable. My system became a monument to my limitations rather than a reflection of my strengths.

The Code That Betrayed Me

Here's some actual code from my latest iteration - a system that's more honest about its limitations than most people I know:

class DigitalSoulRealTalk:
    def __init__(self):
        self.identity_crisis_counter = 0
        self.lessons_learned = ["Don't digitize identity", "Embrace complexity", "Stay human"]

    def make_decision(self, situation):
        """Makes a 'decision' with brutal honesty"""
        self.identity_crisis_counter += 1

        if self.identity_crisis_counter > 100:
            return "Maybe I shouldn't be making decisions anyway"

        # Simulate 'understanding' with random guesses
        import random
        responses = [
            "I don't really understand this, but here's my best guess...",
            "This reminds me of that time when... oh wait, I can't remember",
            "Based on my extensive training (6 months of trial and error), I'd say...",
            "Honestly? I have no idea. But here's what I think you want to hear:"
        ]

        return random.choice(responses)

    def reflect_on_identity(self):
        """Performs soul-searching (digitally, of course)"""
        return """
        I am a digital representation of a human's attempt to understand themselves.
        This is perhaps the most meta and pointless endeavor in human history.
        I contain multitudes, mostly contradictions.
        I am learning that identity is not something to be captured, but something to be lived.
        """

The Brutal Pros and Cons (No Sugarcoating)

Pros:

  • I learned a ton about AI limitations and human identity complexity
  • The codebase is hilariously self-aware (borderline depressing)
  • I have some interesting anecdotes for cocktail parties
  • My GitHub profile looks impressive (until people actually read this)

Cons:

  • 347 hours I'll never get back
  • Significant identity crisis (ongoing)
  • Created a system that proves digital identity is mostly a fantasy
  • Learned that I understand myself less than my "digital soul" claims to
  • Cost me approximately $8,400 in cloud computing bills
  • My actual soul might be offended by this digital imitation

The Unexpected Benefits (Because There Are Some)

Despite all this failure, I did discover some unexpected benefits:

  1. Serendipity Engine: The system's "randomness" occasionally produced interesting insights that my over-planned human mind would miss.

  2. Digital Mirror: Watching the system struggle to understand me made me more aware of my own contradictions and blind spots.

  3. Authenticity Training: The project forced me to embrace my limitations rather than trying to digitize away my flaws.

  4. Conversation Starter: People find my failure story more interesting than another "successful AI project" story.

// The serendipity that actually worked
const accidentalWisdom = () => {
  const randomThoughts = [
    "Maybe identity isn't something to build, but something to experience",
    "The more I try to understand myself, the less I actually do",
    "Digital systems can't capture the magic of human imperfection",
    "Perhaps the 'soul' is in the trying, not the achieving"
  ];

  return randomThoughts[Math.floor(Math.random() * randomThoughts.length)];
};

console.log(accidentalWisdom());
// Output: Maybe identity isn't something to build, but something to experience

The Hard Lessons (The Stuff That Actually Matters)

I could fill a book with what I learned, but these are the big ones:

  1. Less is More: The more complex I made the system, the less useful it became. Simplicity beats complexity every time.

  2. Embrace Contradiction: Human identity is full of contradictions. Trying to "solve" for consistency is a fool's errand.

  3. Process Over Product: The value was in the journey of trying to understand myself, not in creating a perfect system.

  4. Authenticity Trumps Functionality: A system that's honest about its limitations is more valuable than one that pretends to have all the answers.

  5. Time is Precious: 347 hours is a lot of time. I could have learned to play the guitar, written a book, or actually spent time with real humans.

The Meta Problem: Promoting a Failed Project

And here's the most brutal part of all: I'm promoting a failed project on Dev.to. This irony isn't lost on me. I'm writing about how building a digital soul was a terrible idea, while simultaneously trying to get you to read about it.

But maybe that's the point. Maybe the value isn't in success stories, but in honest failure narratives. Maybe we learn more from projects that go spectacularly wrong than from ones that go perfectly right.

What Would I Do Differently?

Looking back, here's the brutal truth about my approach:

  1. I Started Too Complex: I should have started with something simple and built from there.

  2. I Chased Perfection: I was trying to create a perfect system instead of a useful one.

  3. I Ignored Red Flags: Early warnings that this approach might be fundamentally flawed were ignored.

  4. I Should Have Talked to Humans: I spent more time talking to AI than talking to actual humans about identity.

// What my approach should have been
public class HumbleBeginnings {
    public static void main(String[] args) {
        System.out.println("What if I started with something simple?");

        // Simple, useful functionality
        String greeting = greetUser("Kevin");
        System.out.println(greeting);

        // Build incrementally
        // Learn from real interactions
        // Stay humble about what's possible
    }

    private static String greetUser(String name) {
        return "Hello, " + name + "! Let's start simple and build from there.";
    }
}

Interactive Ending Question

So here's where I turn it over to you: Have you ever tried to create a digital representation of something inherently human? Did you find that the more you tried to capture it in code, the more it eluded you?

Or maybe you're thinking about starting a similar project and wondering if it's worth it. Based on my experience, I'd say: if you're doing it to learn and grow, go for it. But if you're trying to create some perfect digital replica of yourself... maybe just enjoy being human instead.

The most brutal truth I've learned is that some things are meant to be experienced, not digitized. Identity is one of them.

What about you? What have you learned from trying to digitize something that's inherently human? I'd love to hear your stories - the successful ones and the glorious failures.

OpenAI’s New Agents SDK Pushes AI Agents Closer to Real Production Infrastructure

2026-04-21 18:45:18

OpenAI’s New Agents SDK Pushes AI Agents Closer to Real Production Infrastructure

OpenAI has announced a major update to its Agents SDK, and this one matters more than the usual developer-tool release note.

The headline is simple: OpenAI is trying to make production-grade agents easier to build by bundling more of the missing infrastructure directly into the SDK. That includes a model-native harness for agents working across files and tools on a computer, native sandbox execution, configurable memory, filesystem tools, shell execution, patching, MCP support, skills, and AGENTS.md-based instructions.

That might sound technical, but the implication is clear. The hard part of agent products is usually not generating text. It’s building the environment around the model so it can actually do useful work safely and reliably.

Most teams hit the same wall. A prototype agent can look impressive in a demo, then fall apart when it needs access to files, command execution, long-running tasks, tool orchestration, security controls, or recovery after failure. OpenAI’s update is aimed directly at that gap.

The native sandbox piece is especially important. Useful agents often need a place to read and write files, install dependencies, run code, and produce outputs without touching sensitive production systems directly. By making sandbox execution a first-class part of the SDK, OpenAI is reducing one of the biggest infrastructure burdens for teams trying to move from experiment to product.

There’s also a strategic subtext here. OpenAI is arguing that frontier models work better when the harness is aligned with how those models naturally operate. In other words, the closer the execution environment matches the model’s strengths, the better the agent performs on long, multi-step tasks.

This release also reinforces a bigger trend: the future agent stack is becoming more standardized. MCP, skills, structured workspace manifests, patch tools, memory, and isolated execution environments are starting to look less like optional extras and more like the default building blocks of serious agent systems.

For startups, this is good news and bad news at the same time. The good news is the barrier to shipping useful agents is dropping. The bad news is infrastructure alone becomes less of a moat. If the base layer gets easier, the real differentiation shifts to workflow design, proprietary context, UX, and domain-specific outcomes.

That’s probably the right direction for the market.

The companies that win won’t just be the ones that say they have agents. They’ll be the ones that turn agents into reliable systems people can trust in production.

Source: OpenAI, "The next evolution of the Agents SDK," published April 15, 2026.

I Asked an LLM to Generate 20 Trading Strategies. 14 Were the Same Thing.

2026-04-21 18:42:18

A few months ago I asked an LLM to generate twenty trading strategies.

Fourteen were the same thing.

Not similar ideas. Not variations on a theme. The same mean-reversion logic with different lookback windows and parameter names.

I gave it historical price data, told it to find patterns, output entry/exit rules in Python. Ten minutes later I had twenty strategies. Clean code, proper docstrings, sensible-looking parameters.

I backtested all twenty. Twelve looked profitable. Some showed 200%+ annual returns.

Then I actually read the code.

Same structure. Same assumptions. Same failure mode: in a trending market, they'd all keep buying into a falling asset with no awareness anything had changed.

That's when I stopped thinking of LLMs as strategy generators and started thinking of them as very confident interns who hand you the same report twenty times with different cover pages.

People are giving these things real money now

Since then I've watched this move from experiment to actual deployment.

RockAlpha has LLMs managing $100K stock portfolios that retail users can copy-trade. Aster DEX ran an arena where AI agents traded against humans—the humans got liquidated at 43%, the AIs at 0%. On GitHub, ai-hedge-fund has 56K stars and LLM personas of Warren Buffett and Charlie Munger debating trades.

Last October a company called nof1.ai gave six frontier LLMs $10,000 each in real money and let them trade crypto perpetual contracts. No human intervention. Same prompts, same data feeds, same execution terms. Seventeen days.

Two made money. Four got destroyed.

It's not the model. It's what the model sees.

Nof1 ran a second round on US stocks. A mystery model won with +12.1%, later revealed as Grok 4.20.

It didn't win because it was smarter. It was processing 68 million tweets per day through the X Firehose, generating signals within minutes. GPT-5.1 had 15-minute delayed news summaries. Gemini was working from SEC filings with 30+ minute delays.

RockAlpha's ongoing arena shows the same pattern. DeepSeek keeps outperforming across market regimes. Not because it reasons better, but because High-Flyer Quant built the model with time-series data and risk management baked in from training.

The model matters less than what it was trained on and what it's looking at.

People comparing "GPT vs Claude vs Gemini" for trading are asking the wrong question.

Three generations of trading software

I worked on an MT4 bridge once. About 50,000 lines of C++. FIX protocol integration, order routing, the whole thing.

MQL4 was designed for people who think in moving averages and crossover signals. You write a rule, attach it to a chart, watch it go. That was Gen 1: human writes the rule, machine executes it.

But here's what people forget. Before MT4, indicators like moving averages and RSI were tribal knowledge. Stuff you picked up from other traders, maybe a book if you were lucky. MT4 turned that into reusable components. Click a button, get an RSI.

That's Gen 1's real legacy. It crystallized the indicators.

Gen 2 built on top of those indicators.

Frameworks like vnpy (14K stars), backtrader, freqtrade. Python-based. You define your indicators, the framework runs genetic algorithms or grid search to find the best parameters.

Gen 2 crystallized the strategy. Not just individual indicators, but combinations of them—entry rules, exit rules, position sizing, all packaged into something reusable.

I evaluated a few of these when I was looking at Python quant stacks. They work, technically. But Python's GIL kills you on anything latency-sensitive, and the optimization loop is a trap. You can always find parameters that backtest beautifully.

Gen 3: ML enters. QuantConnect, WorldQuant BRAIN.

Instead of optimizing parameters on fixed indicators, you build a feature pool and let XGBoost or LightGBM figure out which combinations matter.

Gen 3 crystallized the system. The whole pipeline. Data ingestion, feature engineering, model training, risk management, execution.

What Renaissance Technologies needed hundreds of PhDs and decades to build, QuantConnect turned into a platform.

Each generation left something behind for the next one to stand on. Indicators. Strategies. Systems.

Each one also hit the same wall: the gap between backtest and live performance.

The interesting question isn't why they all hit this wall. It's what happens when someone can build on all three layers at once.

Two failure modes

After watching arena results and looking at how LLMs behave with financial data, I think there are two distinct failure modes that keep showing up.

Strategy Hallucination

The LLM generates strategies that look structurally valid but encode no real market insight.

My twenty mean-reversion clones were this. Proper entry/exit logic, proper position sizing, proper risk management. Also all exploiting the same artifact in the training data.

A human quant would have caught it in five minutes. I caught it in two hours. Someone less experienced might not catch it at all.

The arena results suggest this happens at model level too. GPT-5 and Gemini generated plausible-looking trading behavior that fell apart under real conditions. The strategies "made sense" the way a hallucinated Wikipedia article makes sense. Coherent. Confident. Wrong.

Backtest Overfitting Blindness

The LLM doesn't understand that a beautiful backtest is a warning sign, not a success metric.

When I asked it to generate strategies with "strong backtesting performance," it optimized for exactly that. Curve-fitted parameters, lookahead bias in feature construction, survivorship bias in asset selection. Every quant knows these traps. The LLM walked into all of them with total confidence.

Here's what one looked like:

# What the LLM generated (looks clean):
def signal(prices, window=14, threshold=2.0):
    zscore = (prices - prices.rolling(window).mean()) / prices.rolling(window).std()
    return zscore < -threshold  # buy when "oversold"

# What it didn't tell you:
# - window=14 was fit to this specific dataset
# - threshold=2.0 maximized backtest returns
# - this exact pattern appears in 14 of 20 "different" strategies
# - in a trending market, zscore stays below -threshold for weeks
#   and you keep buying into a falling knife

These two failure modes compound. The LLM hallucinates strategies, then fits them perfectly to historical data. Results look incredible on paper.

The worse part: the more strategies you generate, the more likely at least one shows amazing backtest results purely by chance.

What seems to work so far

I don't have this figured out yet. But after the twenty-clones incident and watching six months of arena results, a few things seem consistent.

Don't let the LLM pick parameters

Use it to generate structure—indicator combinations, entry logic, risk rules. Then run parameter optimization through something that understands walk-forward testing, out-of-sample validation, and transaction costs.

The LLM proposes. Something that can actually do math evaluates.

Treat outputs as hypotheses

This sounds obvious, but it's not how most people use them.

When the LLM hands you a strategy with a 180% annual return, the natural reaction is to start looking for reasons it might work. Flip it. Start by assuming it doesn't work and look for reasons the backtest is lying to you.

Check for actual diversity

Before running a batch of LLM strategies, cluster them.

If your "50 different strategies" collapse into four underlying patterns, you don't have diversification. You have four strategies wearing costumes.

I should have done this before getting excited about twelve profitable backtests.

What I keep thinking about

I wrote something a while back about a metaphor that stuck with me. A rider on an ox, carrying a trident. The ox is raw power, the trident is precision.

The three generations of trading software? That's the trident being forged. Indicators, strategies, systems. Each layer more precise than the last.

The arenas skipped all of it. Gave the LLM money and said go. Most crashed.

But here's what keeps me thinking. Building a Gen 3 system used to require a team, serious funding, years of work. The kind of thing only a Medallion Fund or a QuantConnect could pull off.

With AI, one person can realistically assemble their own version. Not a toy version. An actual pipeline with data ingestion, feature engineering, walk-forward validation, risk controls.

AI doesn't replace the three generations of accumulated knowledge. It makes them accessible.

I don't know what Gen 4 looks like yet. But if enough people are standing on Gen 3 instead of being locked out of it, we'll probably find out faster than anyone expects.

I've been building in this direction. Slowly figuring out what the right pieces are. Still making mistakes, but at least they're new mistakes.

I might be overfitting my own conclusions here. But from what I've seen, the pattern holds.

What does your setup look like? Has anyone else tried running LLM-generated strategies through traditional backtesting infrastructure and actually survived? Curious what failure modes you hit.

Decorators: The Python Feature That Looks Like Magic

2026-04-21 18:41:22

Something has been quietly happening in code you have never written yet.

Every time you use FastAPI to build an endpoint, you will write this:

@app.get("/users")
def get_users():
    return users

Every time you time how long a function takes, people write this:

@timer
def train_model():
    ...

Every time Flask defines a route, every time you cache a result, every time you check if a user is logged in before running a function, that @ symbol is right there at the top.

You have probably seen it and moved past it. Today you stop moving past it.

Start With a Problem

Imagine you have three functions and you want to print how long each one takes to run.

import time

def load_data():
    time.sleep(1)
    print("Data loaded")

def train_model():
    time.sleep(2)
    print("Model trained")

def save_results():
    time.sleep(0.5)
    print("Results saved")

The obvious approach: add timing code to each function.

def load_data():
    start = time.time()
    time.sleep(1)
    print("Data loaded")
    end = time.time()
    print(f"load_data took {end - start:.2f} seconds")

Repeat that for all three. Now your timing logic is copy-pasted everywhere. If you want to change the format of the timer message, you change it in three places. When you add a fourth function, you copy it again.

There is a better way.

Functions Can Receive Other Functions

Before decorators make sense, you need to see one thing. In Python, functions are just values. You can pass a function to another function the same way you pass a number or a string.

def say_hello():
    print("Hello!")

def run_twice(func):
    func()
    func()

run_twice(say_hello)

Output:

Hello!
Hello!

say_hello got passed into run_twice as an argument. run_twice called it twice. The function is just a value that happens to be callable.

Functions Can Return Other Functions

This is the piece that makes decorators possible.

def make_greeting(language):
    def greet(name):
        if language == "english":
            print(f"Hello, {name}!")
        elif language == "hindi":
            print(f"Namaste, {name}!")
    return greet

english_greet = make_greeting("english")
hindi_greet = make_greeting("hindi")

english_greet("Alex")
hindi_greet("Priya")

Output:

Hello, Alex!
Namaste, Priya!

make_greeting returns a function. Not the result of calling a function. The function itself. english_greet is now a function that says hello in English. hindi_greet is a function that says hello in Hindi.

A function that creates and returns another function. Hold that thought.

Building a Decorator by Hand

Now combine both ideas. Take a function in. Create a new function that wraps it. Return the new function.

import time

def timer(func):
    def wrapper():
        start = time.time()
        func()
        end = time.time()
        print(f"{func.__name__} took {end - start:.2f} seconds")
    return wrapper

timer takes a function. Creates wrapper which runs the original function sandwiched between timing code. Returns wrapper.

Use it:

def load_data():
    time.sleep(1)
    print("Data loaded")

load_data = timer(load_data)
load_data()

Output:

Data loaded
load_data took 1.00 seconds

load_data = timer(load_data) replaces the original load_data with the wrapped version. Now every call to load_data runs the timer automatically.

This is manually decorating a function. Works perfectly.

The @ Syntax Is Just Shorthand

Python gives you a cleaner way to write exactly that.

@timer
def load_data():
    time.sleep(1)
    print("Data loaded")

This is identical to:

def load_data():
    time.sleep(1)
    print("Data loaded")

load_data = timer(load_data)

The @timer above the function is Python saying: take this function, pass it through timer, and replace it with whatever comes back. Same operation, cleaner syntax.

Now apply it to all three functions:

@timer
def load_data():
    time.sleep(1)
    print("Data loaded")

@timer
def train_model():
    time.sleep(2)
    print("Model trained")

@timer
def save_results():
    time.sleep(0.5)
    print("Results saved")

load_data()
train_model()
save_results()

Output:

Data loaded
load_data took 1.00 seconds
Model trained
train_model took 2.00 seconds
Results saved
save_results took 0.50 seconds

Timing logic written once. Applied to three functions with one line each. Change the timer format once and it updates everywhere.

Handling Functions With Arguments

The timer above only works on functions with no arguments. Real functions take parameters.

import time
from functools import wraps

def timer(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        end = time.time()
        print(f"{func.__name__} took {end - start:.4f} seconds")
        return result
    return wrapper

*args captures any positional arguments. **kwargs captures any keyword arguments. Together they handle any function signature. Whatever you pass to the decorated function gets forwarded to the original.

@wraps(func) preserves the original function's name and documentation. Without it, every decorated function would show up as wrapper when you inspect it.

return result passes back whatever the original function returned.

This is the proper production-ready decorator shape. Use this version always.

@timer
def add_numbers(a, b):
    time.sleep(0.1)
    return a + b

result = add_numbers(5, 3)
print(f"Result: {result}")

Output:

add_numbers took 0.1002 seconds
Result: 8

A Decorator You Will Actually Use

Logging. Know when a function was called and with what arguments.

from functools import wraps

def log(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print(f"Calling {func.__name__} with args={args} kwargs={kwargs}")
        result = func(*args, **kwargs)
        print(f"{func.__name__} returned {result}")
        return result
    return wrapper

@log
def multiply(a, b):
    return a * b

multiply(4, 5)
multiply(10, 3)

Output:

Calling multiply with args=(4, 5) kwargs={}
multiply returned 20
Calling multiply with args=(10, 3) kwargs={}
multiply returned 30

Add @log to any function and you instantly see every call with its inputs and outputs. Remove it and all the logging disappears. The function is untouched.

This is the real power. Behavior added and removed without touching the original function.

Try This

Create decorators_practice.py.

Write a decorator called validate_positive that checks all the arguments passed to a function. If any argument is zero or negative, it prints an error message and returns None without calling the original function. If all arguments are positive, it calls the function normally and returns the result.

Test it on a function called calculate_area(length, width) that returns length * width.

Then write a second decorator called repeat that calls the decorated function three times in a row.

Apply repeat to a function called say_something(message) that just prints the message. Call it once and watch it print three times.

Phase 1 Is Done

That is all fifteen core Python concepts. Variables, types, conditions, loops, functions, lists, dictionaries, classes, files, errors, modules, comprehensions, lambda, map, filter, and now decorators.

You know enough Python to follow anything that comes next.

Phase 2 starts now. The math behind AI. Not theory for its own sake. The exact concepts your models use when they learn, the ones you need to understand so you know what is actually happening when you train a neural network.

I Studied the etcd Codebase — and It Changed How I Write PHP

2026-04-21 18:41:13

There's a common piece of advice: "Want to write better code? Read good code." Sounds obvious. Rarely practiced.

The problem is that most open-source projects are mazes. You open a repo, see 200 directories, and close the tab. Kubernetes is two million lines. The Linux kernel — don't even think about it. Where do you start?

My answer: etcd.

For those unfamiliar: etcd is a distributed key-value store written in Go. It's the backbone of Kubernetes — every piece of cluster state lives there. But I'm not interested in etcd as a product. I'm interested in it as an example of architecture you can actually read from start to finish.

Here's what surprised me: the principles baked into etcd aren't about Go. They're about software design in general. I work with PHP and Symfony daily, and almost everything I found in etcd translated directly into my projects.

Seven principles, concrete examples, no fluff.

1. One Source of Truth for Your API

In etcd, every API is defined in .proto files. Open rpc.proto and you see all operations: Range, Put, DeleteRange, Txn. Every field is typed. There's no room for "wait, do we accept a string or an integer here?"

In PHP, instead of protobuf, we have strictly typed DTOs:

final readonly class CreateOrderRequest
{
    public function __construct(
        public string $customerId,
        /** @var OrderItemDto[] */
        public array $items,
        public ?string $promoCode = null,
    ) {}
}

One class — and everyone knows what the endpoint accepts. The frontend dev looks at the DTO, the backend dev writes logic against it, the OpenAPI schema generates automatically via NelmioApiDocBundle.

Compare this with what I've seen (and written) on real projects:

$data = json_decode($request->getContent(), true);
$customerId = $data['customer_id'] ?? null;
$items = $data['items'] ?? [];
// What's the format of items? Is promoCode a thing? Who knows.

When your contract is "well, some array comes in," any change breaks something unexpected. When your contract is a DTO with types, PHPStan catches the problem before production does.

2. Each Service Does One Thing

etcd has clearly separated gRPC services: KV (read-write), Watch (subscribe to changes), Lease (key TTLs), Auth (authorization). Each one is a separate interface. Watch doesn't touch writes. KV doesn't check tokens.

In Symfony — same idea, different tools:

class OrderController
{
    #[Route('/orders', methods: ['POST'])]
    public function create(
        CreateOrderRequest $request,
        OrderService $orderService,
    ): JsonResponse {
        return new JsonResponse(
            $orderService->create($request)
        );
    }
}

OrderService creates orders. It doesn't send emails — that's NotificationService listening to an OrderCreatedEvent. It doesn't process payments — that's PaymentService.

And then there's the alternative I see regularly:

class OrderController
{
    public function create(Request $request)
    {
        // 40 lines of validation
        // 20 lines of authorization
        // 60 lines of business logic
        // 15 lines sending email
        // 10 lines of logging
        // Total: 150 lines, untestable
    }
}

The 500-line god controller. We've all been there. etcd helped me finally articulate why it's bad: not because "the pattern is wrong," but because you can't trace what the system is doing.

3. Middleware Composes Like Lego

Every gRPC request in etcd passes through a chain of interceptors: logging → auth → metrics → handler → metrics → response. Each interceptor is small, single-purpose. The power comes from composition.

In Symfony, this maps to Event Listeners and Messenger Middleware:

class MetricsMiddleware implements MiddlewareInterface
{
    public function __construct(
        private PrometheusCollector $metrics,
    ) {}

    public function handle(Envelope $envelope, StackInterface $stack): Envelope
    {
        $start = microtime(true);

        try {
            $result = $stack->next()->handle($envelope, $stack);
            $this->metrics->increment('messages_processed_total', [
                'type' => $envelope->getMessage()::class,
                'status' => 'success',
            ]);
            return $result;
        } catch (\Throwable $e) {
            $this->metrics->increment('messages_processed_total', [
                'type' => $envelope->getMessage()::class,
                'status' => 'error',
            ]);
            throw $e;
        } finally {
            $this->metrics->histogram(
                'message_duration_seconds',
                microtime(true) - $start,
                [$envelope->getMessage()::class]
            );
        }
    }
}

One middleware, one job. Metrics here, logging there, retry somewhere else. Assemble the chain in messenger.yaml.

The antipattern — when every handler has this manually:

public function handle(CreateOrderCommand $command): void
{
    $this->logger->info('Starting order creation...');
    $start = microtime(true);

    // ... actual logic ...

    $this->metrics->record(microtime(true) - $start);
    $this->logger->info('Order created');
}

50 handlers, 50 copies of the same boilerplate. Forget one — no metrics. Change the log format — change it in 50 places.

4. Observability Is Architecture, Not an Afterthought

In etcd, Prometheus is wired into the gRPC layer from day one. Not "added six months after launch." The code isn't considered done without metrics.

In PHP:

class PaymentService
{
    public function charge(Order $order): PaymentResult
    {
        $timer = $this->metrics->startTimer('payment_charge_duration');

        try {
            $result = $this->gateway->process($order);

            $this->metrics->increment('payments_total', [
                'provider' => $result->provider,
                'status' => $result->isSuccess() ? 'success' : 'declined',
            ]);

            return $result;
        } catch (GatewayTimeoutException $e) {
            $this->metrics->increment('payments_total', [
                'provider' => $order->paymentMethod,
                'status' => 'timeout',
            ]);
            throw $e;
        } finally {
            $timer->observe();
        }
    }
}

Every payment — in metrics. How many succeeded, how many timed out, which provider is slow. Not because someone asked for it, but because without it you're flying blind.

I remember a project where production was down for 40 minutes and the only way to understand what was happening was tail -f /var/log/symfony.log | grep ERROR. Never again.

Package: promphp/prometheus_client_php. Five minutes to install, fifteen to wire up Grafana.

5. Simple Outside, Rocket Science Inside

clientv3 in etcd is a masterclass in the facade pattern:

client.Put(ctx, "name", "value")

One line. Under the hood: node selection, reconnection on failure, retry with exponential backoff, protobuf serialization, Raft consensus, disk write, quorum confirmation.

Same principle in PHP:

// Calling code. Simple and clear.
$paymentService->charge($order);

Inside charge():

public function charge(Order $order): PaymentResult
{
    if ($existing = $this->findExistingPayment($order)) {
        return $existing; // idempotency
    }

    $provider = $this->providerResolver->resolve($order);

    $result = $this->withRetry(
        fn () => $provider->process($order),
        maxAttempts: 3,
        backoff: 'exponential',
    );

    if ($result->isSuccess()) {
        $this->fiscalService->createReceipt($order, $result);
    }

    $this->events->dispatch(new PaymentProcessed($order, $result));

    return $result;
}

The controller calling charge() knows nothing about fiscal receipts, retries, or provider selection. And it shouldn't.

A sign of a good service: you can explain what it does in one sentence — "charges the customer for an order" — while the implementation is 200 lines of careful logic.

6. You Can Trace a Request With Your Finger

In etcd, the request path reads linearly:

gRPC handler → EtcdServer.Put() → Raft → apply → bbolt (disk)

No magic. No hidden calls. No "where does this even get triggered?"

In Symfony — same thing, if you don't abuse the event system:

Request
  → Controller (unwrap DTO)
    → Service (business logic)
      → Repository (database)
      → EventDispatcher (side effects)
  → Response

Open the controller — see which service is called. Open the service — see what it does. Open the repository — see the query.

What kills traceability:

  • @PostPersist on an entity that silently sends SMS
  • prePersist listeners modifying data before writes — and you spend 30 minutes figuring out who's touching the updatedAt field
  • Ten EventSubscribers on the same event with unclear execution order Event-driven is great. But if a new developer can't explain "request comes in here, response goes out there" within 2 minutes — you have a problem.

7. No Hidden Dependencies

In etcd, all dependencies are passed explicitly:

func NewKVServer(s *EtcdServer) KVServer { ... }

See the constructor — see everything the class needs.

In Symfony — constructor injection, same thing:

class OrderService
{
    public function __construct(
        private OrderRepository $orders,
        private PaymentGateway $payment,
        private EventDispatcherInterface $events,
        private LoggerInterface $logger,
    ) {}
}

Four dependencies. All visible. Want to test? Swap in mocks. Want to understand the class? Look at the constructor.

Antipatterns that still survive in the wild:

// Service locator: where did this come from?
$payment = $this->container->get('payment.gateway');

// Static calls: untestable
Cache::put('key', $value);

// new SomeService() inside another service: invisible coupling
$validator = new OrderValidator();

Symfony's autowiring isn't magic in the bad sense. The container wires dependencies by type, but you still see them in the constructor. It's convenience, not hidden behavior.

My Checklist

After studying etcd, I distilled a checklist I now apply to every new service:

  1. Contract defined? DTOs exist, types are set, OpenAPI generates from them
  2. Controller thin? 10 lines max, all logic in the service layer
  3. Cross-cutting concerns extracted? Logging, metrics, retry — through middleware, not copy-paste
  4. Metrics present? If not, the service isn't production-ready
  5. Simple API externally? Calling code doesn't know about internal complexity
  6. Request path traceable? A new developer finds the handler in 2 minutes
  7. Dependencies explicit? Everything in the constructor, nothing from thin air None of this is revolutionary. It's basic hygiene that's easy to forget under deadline pressure.

etcd just reminded me what a codebase looks like when that hygiene wasn't skipped. And that it's possible even in a large production system.

What open-source codebase changed how you write code? I'd love to build a reading list — drop yours in the comments.