<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Anthony Terrell — Blog</title>
    <link>https://anthonyterrell.com/blog</link>
    <description>Chicago-based lead software engineer. PHP, Laravel, and AI-augmented development — writing about building software that doesn&#039;t fall over.</description>
    <language>en-us</language>
    <atom:link href="https://anthonyterrell.com/feed.xml" rel="self" type="application/rss+xml" />
        <lastBuildDate>Wed, 01 Apr 2026 00:00:00 +0000</lastBuildDate>
            <item>
      <title>Building AI guardrails for your dev teams</title>
      <link>https://anthonyterrell.com/blog/ai-guardrails-dev-teams</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/ai-guardrails-dev-teams</guid>
      <pubDate>Wed, 01 Apr 2026 00:00:00 +0000</pubDate>
      <description>Most teams adopted AI tools bottom-up with zero guardrails. Here&#039;s how to add governance that protects the team without killing the momentum.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>AI tools are everywhere in engineering now. Copilot in the IDE, ChatGPT in the browser, AI-powered code review bots in the PR pipeline. Most teams adopted these tools bottom-up — individual developers started using them, and leadership caught up later. The result is a lot of teams running AI-assisted development with <strong>zero guardrails</strong>.</p>

<p>That's not sustainable. Not because AI is dangerous, but because ungoverned AI usage creates inconsistency, security risks, and a false sense of productivity. Here's how I think about building guardrails that protect the team without killing the momentum.</p>

<h2>Start with what leaves the building</h2>

<p>The first guardrail isn't about code quality — it's about <strong>data exposure</strong>. Every time a developer pastes code into an AI tool, they're potentially sending proprietary logic, API keys, customer data, or internal architecture details to a third party.</p>

<p>Before anything else, establish clear rules:</p>

<ul>
<li><strong>Which tools are approved?</strong> There's a difference between Copilot (running in your IDE with enterprise data agreements) and pasting code into a random chatbot. Make the approved list explicit.</li>
<li><strong>What can't be shared?</strong> Environment variables, customer data, internal API schemas, security configurations. Developers need a clear line, not a vague "use your judgment."</li>
<li><strong>Where do credentials live?</strong> If AI tools are autocompleting <code>.env</code> files or suggesting hardcoded secrets, your secret management practices need to be airtight before AI amplifies the problem.</li>
</ul>

<blockquote>
  <p>The biggest risk isn't that AI writes bad code. It's that a developer pastes sensitive context into a tool with no data retention guarantees.</p>
</blockquote>

<h2>Code review doesn't change — it gets more important</h2>

<p>AI-generated code needs <strong>the same review rigor</strong> as human-written code. I'd argue it needs more, because AI-generated code has a specific failure mode: it looks plausible. It compiles, it passes a surface-level read, and it might even work for the happy path. But it can carry subtle bugs, security vulnerabilities, or patterns that don't match your codebase conventions.</p>

<p>Your code review process should account for this:</p>

<ul>
<li><strong>Reviewers should assume AI involvement.</strong> Not to be suspicious, but to be thorough. Read the logic, don't just skim the structure.</li>
<li><strong>Check for AI-typical mistakes.</strong> Overly verbose code, unnecessary abstractions, deprecated API usage, security patterns that are "textbook correct" but wrong for your specific context.</li>
<li><strong>Assertions and tests matter more.</strong> If a PR includes AI-generated logic, the tests need to be especially strong. Weak tests on AI code are how bugs ship.</li>
</ul>

<h2>Standardize the prompts, not just the output</h2>

<p>One thing I've found effective is creating <strong>shared prompt templates</strong> for common tasks. Instead of every developer writing their own prompt for "generate a migration" or "write tests for this service," the team maintains a set of vetted prompts that encode your conventions.</p>

<p>This isn't about controlling how people use AI. It's about <strong>encoding institutional knowledge</strong> into the prompts so the output is consistent. When everyone's prompt includes "follow our existing error handling pattern in <code>src/lib/errors</code>" or "use our test factory pattern from <code>tests/factories</code>," the generated code fits the codebase instead of fighting it.</p>

<p>Here's a sample template a team might standardize for test generation:</p>

<pre><code class="language-text">Write Pest tests for the following service method.

Conventions:
- Build models via our factories in `tests/Factories` — never call Model::create() directly.
- Use `expect()` assertions. No `$this-&gt;assert*`.
- Mock outbound HTTP with `FakeHttp` in `tests/Support/FakeHttp.php` — not `Http::fake()`.
- Group related cases with `describe()`.
- Required coverage: happy path, each validation branch, and one failure mode per external dependency.
- Do not test framework behavior (Eloquent relations, built-in validation rules).

Method:
&lt;paste method here&gt;
</code></pre>

<p>It's not fancy — but it replaces four developers writing four different prompts and getting four different test styles back. Some teams go further and build these into CLI tools or IDE snippets. The overhead is minimal, and the consistency gain is real.</p>

<h2>Set boundaries on where AI can operate</h2>

<p>Not every part of your codebase should be fair game for AI-assisted development. I draw lines based on risk:</p>

<ul>
<li><strong>Low risk:</strong> UI components, utility functions, CRUD endpoints, test scaffolding. AI is great here. Let it rip.</li>
<li><strong>Medium risk:</strong> Business logic, data transformations, API integrations. AI can draft, but a human needs to understand every line before it merges.</li>
<li><strong>High risk:</strong> Authentication, authorization, payment processing, cryptography, data migrations. AI should assist with research and boilerplate at most. The core logic should be human-written and human-reviewed by someone who understands the domain.</li>
</ul>

<p>This isn't about distrusting AI. It's about matching the level of scrutiny to the blast radius of a mistake.</p>

<h2>Monitor and measure</h2>

<p>You can't manage what you don't measure. Track what AI adoption actually looks like on your team:</p>

<ul>
<li><strong>What percentage of PRs include AI-generated code?</strong> You don't need exact numbers — even self-reported data helps.</li>
<li><strong>Are bug rates changing?</strong> Not just total bugs, but the character of bugs. AI-typical bugs (plausible but wrong logic, missed edge cases) are worth tracking separately.</li>
<li><strong>Is velocity actually improving?</strong> More code isn't more productivity. If AI is generating code that takes longer to review and debug, the net effect might be negative.</li>
</ul>

<h2>The human stays accountable</h2>

<p>The most important guardrail is cultural: <strong>the developer who submits the code owns the code</strong>, regardless of how it was generated. AI is a tool, like a linter or a Stack Overflow answer. It doesn't own the commit, you do.</p>

<p>If your team internalizes one thing, make it this. AI doesn't reduce the need for understanding — it increases it. The developers who thrive with AI tools are the ones who use them to move faster through the parts they already understand, not to skip the parts they don't.</p>

<p>Build guardrails that reinforce that mindset, and AI becomes a genuine multiplier. Skip the guardrails, and you're just shipping code faster with less understanding. That's a debt that compounds.</p>
]]></content:encoded>
    </item>
        <item>
      <title>Using AI to generate test suites — what works, what doesn&#039;t</title>
      <link>https://anthonyterrell.com/blog/ai-generated-test-suites</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/ai-generated-test-suites</guid>
      <pubDate>Sat, 28 Mar 2026 00:00:00 +0000</pubDate>
      <description>AI-assisted test generation isn&#039;t the hype or the hate — it&#039;s messier and more useful than either. Here&#039;s what actually works, what doesn&#039;t, and how to use it well.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>I've been using AI-assisted coding tools for test generation for a while now, and my take is probably not what you'd expect. It's not "AI will replace test writing" and it's not "AI-generated tests are useless." The truth is messier and more interesting than either extreme.</p>

<h2>What actually works well</h2>

<p><strong>Boilerplate and scaffolding.</strong> Setting up test files, configuring mocks, writing the structural bits that every test needs — this is where AI shines. I can describe a class and get a test file with proper imports, setup/teardown methods, and a reasonable structure in seconds. That used to take me 5-10 minutes of tedious typing.</p>

<p><strong>Happy path tests.</strong> Give Copilot or an LLM a function signature and a brief description, and it'll generate solid happy path coverage. CRUD operations, straightforward transformations, simple validation — these come out surprisingly good. The tests are readable, they follow conventions, and they actually catch regressions.</p>

<p><strong>Repetitive patterns.</strong> If you're testing 15 API endpoints that all follow the same pattern — validate input, call service, return response — AI can crank those out faster than any human. You write the first one, and it extrapolates the rest.</p>

<blockquote>
  <p>AI-generated tests are excellent first drafts. The mistake is treating them as final drafts.</p>
</blockquote>

<h2>What doesn't work</h2>

<p><strong>Edge cases.</strong> This is the big one. AI-generated tests almost never cover the weird stuff — null values in nested objects, concurrent access patterns, timezone-related bugs, off-by-one errors in pagination. These are the tests that actually save you from production incidents, and they require understanding the <strong>domain</strong>, not just the code.</p>

<p><strong>Integration tests.</strong> Getting an LLM to write a meaningful integration test that sets up realistic state, exercises a real workflow, and asserts on the right outcomes? I haven't seen it done well yet. The generated tests either mock too much (defeating the purpose) or assume an environment that doesn't exist.</p>

<p><strong>Nuanced business logic.</strong> If your settlement calculation has special handling for leap years, partial refunds, and multi-currency rounding rules, no AI tool is going to infer those test cases from the code alone. It needs context that lives in product specs, Slack conversations, and the developer's head.</p>

<h2>The false confidence problem</h2>

<p>This is what worries me most. I've reviewed PRs where a developer pointed to 90% test coverage as proof their code was solid. When I looked closer, the AI-generated tests were asserting on... basically nothing. Tests that call a function and check that it doesn't throw. Tests that verify a response is "not null" without checking the actual values. Tests that mock every dependency so thoroughly that the test is just exercising the mocking framework.</p>

<p><strong>Coverage numbers lie</strong> when the assertions are weak. A test that passes regardless of the implementation is worse than no test at all, because it gives you confidence you haven't earned.</p>

<h2>How I actually use AI for tests</h2>

<p>Here's my workflow:</p>

<ol>
<li><strong>Generate the scaffolding.</strong> Let AI set up the test file, imports, and basic structure.</li>
<li><strong>Generate happy path tests.</strong> Accept these with minor tweaks — they're usually 80% right.</li>
<li><strong>Write edge case tests myself.</strong> This is where the real thinking happens. I look at the code and ask: what breaks? What are the boundary conditions? What assumptions am I making?</li>
<li><strong>Use AI to generate variations.</strong> Once I have one good edge case test, I'll ask AI to generate similar variations. "Now test this with negative amounts. Now test with amounts exceeding the maximum. Now test with zero."</li>
<li><strong>Review every assertion.</strong> I read every <code>assert</code> or <code>expect</code> statement the AI wrote. If it's asserting something trivial, I strengthen it or delete the test.</li>
</ol>

<h2>The bottom line</h2>

<p>AI test generation is a <strong>productivity multiplier</strong>, not a quality multiplier. It makes you faster at producing tests, but it doesn't make the tests better. The thinking — what to test, why it matters, what could go wrong — still has to come from you.</p>

<p>I'd estimate AI saves me about 30-40% of the time I used to spend writing tests. That's significant. But the time I save on typing, I reinvest in <strong>thinking about what to test</strong>. That's the part that actually prevents bugs, and it's the part no tool can do for you yet.</p>

<p>Use AI-generated tests as a starting point. A good starting point. Just never mistake the starting point for the finish line.</p>
]]></content:encoded>
    </item>
        <item>
      <title>AI organization and setup within your code repositories</title>
      <link>https://anthonyterrell.com/blog/ai-organization-code-repositories</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/ai-organization-code-repositories</guid>
      <pubDate>Sun, 01 Mar 2026 00:00:00 +0000</pubDate>
      <description>Your repo structure is the context window for AI coding tools. Here&#039;s how to organize instruction files, conventions, and configuration so the output actually matches your codebase.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>Every team I talk to is using AI coding tools. Almost none of them have thought about how those tools interact with their repository structure. They install Copilot, maybe drop a <code>.cursorrules</code> file in the root, and call it done. Then they wonder why the AI keeps suggesting patterns that don't match their codebase.</p>

<p>Your repo structure, configuration files, and conventions are the <strong>context window</strong> for AI tools. If that context is messy, the output will be too. Here's how I organize repositories to get the most out of AI-assisted development.</p>

<h2>Instruction files: teach the AI your codebase</h2>

<p>Most AI coding tools support some form of project-level instructions. Cursor has <code>.cursorrules</code>, GitHub Copilot has <code>.github/copilot-instructions.md</code>, Claude Code has <code>CLAUDE.md</code>. These files are your chance to front-load context that the AI would otherwise have to infer (badly).</p>

<p>What goes in these files:</p>

<ul>
<li><strong>Tech stack and versions.</strong> "This project uses Node 20, TypeScript 5.4, Express, and Prisma with PostgreSQL." Simple, but it prevents the AI from suggesting Python patterns or outdated Node APIs.</li>
<li><strong>Coding conventions.</strong> "We use functional components only. Error handling follows the pattern in <code>src/lib/errors.ts</code>. All database queries go through the repository layer." The more specific, the better.</li>
<li><strong>File organization rules.</strong> "Tests live next to source files as <code>*.test.ts</code>. Migrations are in <code>db/migrations/</code> with sequential numbering. API routes follow the pattern <code>src/routes/{resource}/{action}.ts</code>."</li>
<li><strong>What to avoid.</strong> "Do not use <code>any</code> type. Do not add <code>console.log</code> statements. Do not import from <code>src/legacy/</code> — those modules are deprecated."</li>
</ul>

<blockquote>
  <p>An AI instruction file isn't documentation for humans. It's a contract with the tool. Write it like you're onboarding a fast but context-free junior developer.</p>
</blockquote>

<h2>Directory structure matters more than you think</h2>

<p>AI tools use file paths and directory names as signals. A well-organized repo gives the AI <strong>structural context</strong> that improves suggestions significantly.</p>

<p>Patterns that help:</p>

<ul>
<li><strong>Consistent naming.</strong> If your services are in <code>src/services/</code> and they all follow <code>{entity}.service.ts</code>, the AI will pick up that pattern and apply it to new files. If half are in <code>src/services/</code> and half are in <code>src/lib/</code> with inconsistent naming, the AI guesses — and guesses wrong.</li>
<li><strong>Colocated tests.</strong> When tests live next to the code they test, the AI can reference the implementation while generating tests. When tests are in a separate <code>tests/</code> tree with a different directory structure, the AI has to work harder to find the connection.</li>
<li><strong>Clear boundaries.</strong> Separate directories for API routes, business logic, data access, and shared utilities tell the AI where new code should go. A flat <code>src/</code> directory with 50 files gives no structural hints.</li>
</ul>

<h2>Context files for complex domains</h2>

<p>For codebases with complex business logic, I create <strong>context files</strong> that live in the repository and explain domain concepts. These aren't documentation in the traditional sense — they're reference material that AI tools can pull from.</p>

<p>I keep these in a <code>docs/context/</code> or <code>.ai/</code> directory:</p>

<ul>
<li><code>domain-glossary.md</code> — Defines business terms. What's a "settlement"? What's the difference between "authorization" and "capture"? AI tools reference this when generating code that uses domain language.</li>
<li><code>architecture-decisions.md</code> — Key decisions and why they were made. "We use event sourcing for the ledger because..." This prevents the AI from suggesting approaches you've already considered and rejected.</li>
<li><code>api-conventions.md</code> — How your APIs are structured, error formats, pagination patterns, auth schemes. Gives the AI a template for generating new endpoints.</li>
</ul>

<p>The key is keeping these <strong>concise and current</strong>. A 50-page architecture document that hasn't been updated in a year is worse than no document. Short, accurate files that evolve with the codebase are what AI tools can actually use.</p>

<h2>Keep your tooling config in version control</h2>

<p>This should be obvious, but I still see teams where AI tool configurations live on individual developer machines. Your <code>.cursorrules</code>, <code>CLAUDE.md</code>, <code>.github/copilot-instructions.md</code>, and any context files should be <strong>committed to the repo</strong>.</p>

<p>This means:</p>

<ul>
<li>Every developer gets the same AI behavior out of the box.</li>
<li>Changes to AI configuration go through code review.</li>
<li>The instructions evolve alongside the code they describe.</li>
<li>New team members (and new AI tools) inherit the context immediately.</li>
</ul>

<h2>Iterate on the setup</h2>

<p>Your AI configuration isn't a set-it-and-forget-it thing. As your codebase evolves, the instructions should too. I treat AI instruction files like any other code — when I refactor a major pattern, I update the instruction file in the same PR.</p>

<p>Some teams add a recurring reminder to review their AI config files quarterly. That works, but I prefer the organic approach: if the AI keeps making the same mistake, update the instruction file to address it. The config becomes a living record of "things the AI gets wrong about our codebase," and it gets better over time.</p>

<p>The teams getting the most value from AI tools aren't the ones with the best prompts. They're the ones with the <strong>best-organized repositories</strong>. Clean structure, clear conventions, and explicit instructions turn an AI tool from a generic code generator into something that actually understands your project. The setup cost is a few hours. The compounding return is every AI interaction after that being slightly more useful.</p>
]]></content:encoded>
    </item>
        <item>
      <title>HMAC Webhook Verification: What Most Tutorials Get Wrong</title>
      <link>https://anthonyterrell.com/blog/hmac-webhook-verification</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/hmac-webhook-verification</guid>
      <pubDate>Sat, 15 Nov 2025 00:00:00 +0000</pubDate>
      <description>You copied the webhook verification snippet from the docs. The tests pass. But four common mistakes can still leave your endpoint wide open.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>You copy the webhook verification snippet from the docs, the tests pass, you ship it. Six months later someone replays an old event and something bad happens. Here's why.</p>

<p>This isn't a "here's what HMAC is" post. There are a thousand of those. This is about the specific, real mistakes that appear in production codebases written by developers who thought they'd implemented webhook verification correctly.</p>

<h2>What HMAC Actually Does (Quick Version)</h2>

<p>HMAC is a keyed hash. The provider signs the payload with a shared secret. You verify the signature matches before trusting the payload. Without it, anyone can POST to your webhook endpoint and trigger payment logic -- create fake orders, mark transactions as paid, trigger refunds.</p>

<p>Stripe, Adyen, GitHub, Twilio -- they all use variants of this pattern. The concept is simple. The implementation is where things quietly go wrong.</p>

<h2>Mistake #1: Parsing the Body Before Verifying It</h2>

<p>This is the big one. I've seen it in production codebases at multiple companies.</p>

<p>Here's the problem: JSON parsers normalize whitespace, can reorder keys (in some implementations), and change encoding. HMAC is computed over the exact byte sequence the provider sent. If you parse the payload to an object first, then re-serialize to verify, you may be hashing a different byte sequence than what was signed.</p>

<p>The wrong way:</p>

<pre><code class="language-python"># WRONG -- body is already parsed by the framework
@app.post("/webhook")
async def webhook(payload: PaymentEvent):  # FastAPI auto-parsed this
    verify_signature(payload.json(), request.headers["X-Signature"])
</code></pre>

<p>The right way:</p>

<pre><code class="language-python"># RIGHT -- read raw bytes first, parse after
@app.post("/webhook")
async def webhook(request: Request):
    raw_body = await request.body()   # bytes, untouched
    verify_signature(raw_body, request.headers["X-Signature"])
    payload = json.loads(raw_body)    # parse AFTER verification
</code></pre>

<p>In Laravel:</p>

<pre><code class="language-php">// Use $request-&gt;getContent(), not $request-&gt;all()
$rawBody = $request-&gt;getContent();
</code></pre>

<p>The result of getting this wrong: verification fails silently, and somewhere in the debugging process, a developer disables signature checking "temporarily" because it "never works." I've seen that comment in code. <code>// TODO: re-enable signature verification</code>. The TODO never happens.</p>

<h2>Mistake #2: No Replay Attack Protection</h2>

<p>An attacker captures a valid signed webhook -- say, "payment succeeded." They replay it hours or days later to trigger the same logic again. The signature is valid. The payload hasn't been tampered with. Naive verification passes.</p>

<p>The fix is timestamp validation. Stripe includes a <code>t=&lt;unix_timestamp&gt;</code> in the signature header for exactly this reason. Reject any webhook where the timestamp is older than your tolerance window -- typically five minutes.</p>

<pre><code class="language-python">import time

def verify_timestamp(signature_header: str, tolerance: int = 300):
    # Stripe format: t=1614556828,v1=abc123...
    elements = dict(
        pair.split("=", 1)
        for pair in signature_header.split(",")
    )
    timestamp = int(elements["t"])
    if abs(time.time() - timestamp) &gt; tolerance:
        raise ValueError("Webhook timestamp outside tolerance window")
</code></pre>

<pre><code class="language-php">$timestamp = /* extract from signature header */;
if (abs(time() - $timestamp) &gt; 300) {
    abort(403, 'Webhook timestamp outside tolerance window');
}
</code></pre>

<p>One gotcha: this requires your server clock to be reasonably in sync. If you're running containers, make sure NTP is configured. A clock that's drifted six minutes will reject every valid webhook.</p>

<p>This is a one-liner addition, but almost nobody includes it because the tutorials skip it.</p>

<h2>Mistake #3: Using String Comparison Instead of Constant-Time Comparison</h2>

<p>Regular string comparison (<code>==</code>) short-circuits on the first mismatched character. That means a comparison against the correct signature returns faster when the first characters match than when they don't. An attacker can statistically infer the correct signature by measuring response times.</p>

<p>Sounds theoretical. It's a known attack vector. And it's trivial to fix.</p>

<pre><code class="language-python"># Wrong
if computed_signature == provided_signature:
    pass

# Right
import hmac
if hmac.compare_digest(computed_signature, provided_signature):
    pass
</code></pre>

<p>In PHP:</p>

<pre><code class="language-php">// Wrong
if ($computed === $provided) { }

// Right
if (hash_equals($computed, $provided)) { }
</code></pre>

<p>Most Laravel developers already know <code>hash_equals()</code> from password comparison. They just forget it applies here too.</p>

<h2>Mistake #4: Logging the Raw Payload</h2>

<p>This one's less about security verification and more about what happens around it. Raw webhook payloads from payment providers often contain: card last four digits, billing addresses, customer emails, transaction amounts.</p>

<p>Logging these violates PCI DSS guidance and potentially GDPR. I've seen full webhook payloads dumped to application logs during development, left there through deployment, and sitting in a log aggregator with broad team access.</p>

<p>The fix: log the event type and event ID. That's it. Never the full payload.</p>

<pre><code class="language-python">import logging

logger = logging.getLogger(__name__)

# Right -- enough to debug, nothing sensitive
logger.info("Webhook received", extra={
    "event_type": payload.get("type"),
    "event_id": payload.get("id"),
})

# Wrong -- PII and financial data in your logs
logger.info(f"Webhook payload: {payload}")
</code></pre>

<h2>Putting It All Together</h2>

<p>Here's a complete, correct implementation. This is the "copy this" section.</p>

<p><strong>Python (FastAPI):</strong></p>

<pre><code class="language-python">import hashlib
import hmac
import json
import logging
import time

from fastapi import FastAPI, Request, HTTPException

app = FastAPI()
logger = logging.getLogger(__name__)
WEBHOOK_SECRET = "whsec_..."  # from environment, not hardcoded

@app.post("/webhook")
async def handle_webhook(request: Request):
    # 1. Read raw bytes
    raw_body = await request.body()
    signature_header = request.headers.get("Stripe-Signature", "")

    # 2. Parse signature header
    elements = dict(
        pair.split("=", 1)
        for pair in signature_header.split(",")
    )
    timestamp = int(elements.get("t", 0))
    provided_signature = elements.get("v1", "")

    # 3. Check replay window
    if abs(time.time() - timestamp) &gt; 300:
        raise HTTPException(status_code=403, detail="Timestamp expired")

    # 4. Compute expected signature
    signed_payload = f"{timestamp}.{raw_body.decode()}"
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        signed_payload.encode(),
        hashlib.sha256,
    ).hexdigest()

    # 5. Constant-time comparison
    if not hmac.compare_digest(expected, provided_signature):
        raise HTTPException(status_code=403, detail="Invalid signature")

    # 6. Parse after verification
    payload = json.loads(raw_body)

    # 7. Log safely
    logger.info("Webhook verified", extra={
        "event_type": payload.get("type"),
        "event_id": payload.get("id"),
    })

    return {"status": "ok"}
</code></pre>

<p><strong>PHP (Laravel):</strong></p>

<pre><code class="language-php">// app/Http/Middleware/VerifyWebhookSignature.php
public function handle(Request $request, Closure $next)
{
    $rawBody = $request-&gt;getContent();
    $signatureHeader = $request-&gt;header('Stripe-Signature', '');

    // Parse signature header
    $elements = collect(explode(',', $signatureHeader))
        -&gt;mapWithKeys(fn ($pair) =&gt; [
            Str::before($pair, '=') =&gt; Str::after($pair, '='),
        ]);

    $timestamp = (int) $elements-&gt;get('t', 0);
    $providedSignature = $elements-&gt;get('v1', '');

    // Check replay window
    if (abs(time() - $timestamp) &gt; 300) {
        abort(403, 'Webhook timestamp expired');
    }

    // Compute expected signature
    $signedPayload = "{$timestamp}.{$rawBody}";
    $expected = hash_hmac('sha256', $signedPayload, config('services.stripe.webhook_secret'));

    // Constant-time comparison
    if (! hash_equals($expected, $providedSignature)) {
        abort(403, 'Invalid webhook signature');
    }

    // Log safely
    $payload = json_decode($rawBody, true);
    Log::info('Webhook verified', [
        'event_type' =&gt; $payload['type'] ?? 'unknown',
        'event_id' =&gt; $payload['id'] ?? 'unknown',
    ]);

    return $next($request);
}
</code></pre>

<h2>The Checklist</h2>

<ol>
<li><strong>Read the raw body</strong> before any parsing or framework deserialization</li>
<li><strong>Validate the timestamp</strong> -- reject anything older than five minutes</li>
<li><strong>Use constant-time comparison</strong> -- <code>hmac.compare_digest()</code> in Python, <code>hash_equals()</code> in PHP</li>
<li><strong>Log event type and ID only</strong> -- never the full payload</li>
</ol>

<p>These four things take maybe 30 lines of code. They're the difference between a webhook endpoint that looks secure and one that actually is. The concepts apply across providers -- Stripe, GitHub, Twilio, Adyen all use variants of the same pattern. Get it right once, and you've got a template for every integration that follows.</p>
]]></content:encoded>
    </item>
        <item>
      <title>PCI compliance considerations for API integrations</title>
      <link>https://anthonyterrell.com/blog/pci-compliance-api-integrations</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/pci-compliance-api-integrations</guid>
      <pubDate>Tue, 15 Jul 2025 00:00:00 +0000</pubDate>
      <description>The goal of PCI DSS isn&#039;t to be compliant — it&#039;s to stay out of scope entirely. Opinions from enough audits to have them.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>PCI DSS compliance is one of those topics that makes developers' eyes glaze over — until they realize their API is accidentally logging credit card numbers to CloudWatch. I've been through enough PCI audits to have opinions, and the biggest one is this: <strong>the goal isn't to be compliant, it's to stay out of scope entirely.</strong></p>

<h2>What PCI DSS means for APIs</h2>

<p>PCI DSS (Payment Card Industry Data Security Standard) applies to any system that stores, processes, or transmits cardholder data. If your API touches a card number, expiration date, or CVV at any point — even transiently — you're in scope.</p>

<p>Being "in scope" means your infrastructure, code, deployment processes, access controls, and logging all need to meet PCI requirements. That's a lot of surface area to secure, audit, and maintain. The smart move is to <strong>minimize your PCI footprint</strong> so as little of your system as possible falls under those requirements.</p>

<h2>Tokenization is your best friend</h2>

<p>The single most effective thing you can do is <strong>never handle raw card data</strong>. Use a payment processor's hosted fields or tokenization SDK so that card numbers go directly from the user's browser to the processor. Your server only ever sees a token — an opaque reference that's useless to an attacker.</p>

<p>Stripe, Braintree, Adyen — they all offer this pattern. The card data flows from the client to the processor, you get back a token, and you use that token for charges, refunds, and lookups. Your API never touches the PAN (Primary Account Number), which keeps you at <strong>SAQ A</strong> or <strong>SAQ A-EP</strong> instead of the full <strong>SAQ D</strong> assessment.</p>

<blockquote>
  <p>If your backend API is receiving raw card numbers from your frontend, stop and rearchitect. The risk reduction from tokenization is worth any refactoring cost.</p>
</blockquote>

<h2>Encryption: at rest and in transit</h2>

<p><strong>In transit:</strong> TLS everywhere. Not just on your public-facing endpoints — between internal services too. Mutual TLS (mTLS) between services that handle payment data adds another layer. I've seen teams that encrypt external traffic but send payment tokens over plain HTTP between internal microservices. Don't be that team.</p>

<p><strong>At rest:</strong> If you must store any payment-related data (even tokens or last-four digits), encrypt it. Use your cloud provider's KMS for key management rather than rolling your own. Rotate keys on a schedule. And make sure your database backups are encrypted too — that's a common audit finding.</p>

<h2>Common mistakes I've seen</h2>

<p><strong>Logging card data.</strong> This is the most frequent violation. A developer adds request logging middleware, and suddenly full card numbers are sitting in your log aggregator. Always sanitize payment-related fields before logging. Better yet, use an allowlist approach — only log fields you've explicitly marked as safe.</p>

<p><strong>Storing CVVs.</strong> PCI DSS is crystal clear: you cannot store CVV/CVC values after authorization. Ever. Not encrypted, not hashed, not "temporarily." If your database has a CVV column, you have a problem.</p>

<p><strong>Passing PAN in query strings.</strong> Query parameters end up in access logs, browser history, referrer headers, and CDN logs. Card numbers in URLs are a disaster. Always use POST bodies for sensitive data, and make sure your API design enforces this.</p>

<p><strong>Overly broad access.</strong> When every developer on the team has access to the production payment database, your PCI scope includes every one of their workstations. Limit access to the minimum necessary, and audit it regularly.</p>

<h2>Scoping: draw the boundary tight</h2>

<p>PCI scope isn't all-or-nothing. You can segment your network so that only a small, well-defined set of systems are in scope. The key techniques:</p>

<ul>
<li><strong>Network segmentation</strong> — Put payment-handling services in their own VPC or subnet with strict firewall rules.</li>
<li><strong>Service isolation</strong> — Your payment service should be a separate, small service with a minimal API surface. Don't bolt payment processing onto your monolith.</li>
<li><strong>Data flow mapping</strong> — Document exactly where cardholder data enters, moves through, and exits your system. Auditors love this, and building it forces you to find scope creep.</li>
</ul>

<h2>Working with payment processors</h2>

<p>The best payment processors are partners in keeping you out of scope. When evaluating processors, ask:</p>

<ul>
<li>Do they offer client-side tokenization?</li>
<li>Can they handle 3D Secure / SCA flows without card data touching your servers?</li>
<li>Do they provide PCI attestation documentation you can reference in your own assessment?</li>
<li>What does their webhook payload contain? (Some processors include masked card data in webhooks — make sure you're not inadvertently storing it.)</li>
</ul>

<p>My approach is to treat the payment processor as the <strong>system of record</strong> for all card-related data. My systems store tokens, transaction IDs, and references. If I need card details for display, I fetch masked data from the processor on demand rather than caching it locally.</p>

<p>PCI compliance doesn't have to be painful if you design for it from the start. Keep card data off your servers, encrypt everything, log carefully, and draw your scope boundary as tight as possible. Your future self — and your auditor — will thank you.</p>
]]></content:encoded>
    </item>
        <item>
      <title>Retry mechanisms for transaction failures</title>
      <link>https://anthonyterrell.com/blog/retry-mechanisms-transaction-failures</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/retry-mechanisms-transaction-failures</guid>
      <pubDate>Sun, 06 Apr 2025 00:00:00 +0000</pubDate>
      <description>Retry logic sounds simple, until a naive retry charges someone twice or orphans a transaction for days. In payment systems, the edge cases are the whole story.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>Retry logic sounds simple. Request fails, try again. But in payment systems, a naive retry can charge someone twice, trigger fraud alerts, or create orphaned transactions that take days to reconcile. I've debugged enough of these to know that <strong>the edge cases are the whole story</strong>.</p>

<h2>The basics: backoff, jitter, and circuit breakers</h2>

<p>If you're not already using <strong>exponential backoff with jitter</strong>, start there. A fixed retry interval means all your failed requests retry simultaneously, creating a thundering herd that makes the problem worse. Exponential backoff spreads the retries over time, and jitter randomizes them so they don't all land at the same moment.</p>

<p>A simple formula: <code>delay = min(base * 2^attempt + random(0, jitter), max_delay)</code></p>

<p><strong>Circuit breakers</strong> sit on top of retry logic. If a downstream service fails repeatedly, the circuit breaker "opens" and short-circuits requests for a cooldown period instead of hammering a service that's already down. This protects both you and the downstream system.</p>

<blockquote>
  <p>The goal of retry logic isn't to make every request succeed. It's to recover from transient failures without making permanent failures worse.</p>
</blockquote>

<h2>Idempotency keys: the non-negotiable</h2>

<p>Every payment request your system sends should include an <strong>idempotency key</strong> — a unique identifier that tells the downstream system "if you've already processed this request, return the original result instead of processing it again."</p>

<p>Without idempotency keys, a timeout becomes a nightmare. Did the charge go through? You don't know. If you retry, you might double-charge. If you don't retry, you might have a failed payment that's actually successful.</p>

<p>Implementation isn't complicated, but the details matter:</p>

<ul>
<li><strong>Generate the key client-side</strong>, tied to the intent (e.g., hash of order ID + amount + timestamp). Don't use random UUIDs — if the client crashes and retries, it needs to produce the same key.</li>
<li><strong>Store the key and result server-side</strong> for a reasonable TTL (24-72 hours for payment operations).</li>
<li><strong>Return the cached result</strong> for duplicate keys, including the original status code. A retry should be indistinguishable from the original request.</li>
</ul>

<h2>The edge cases that break everything</h2>

<p><strong>Timeout ambiguity.</strong> Your request to the payment processor times out after 30 seconds. Did it succeed? The processor might have received and processed it but the response was lost. This is the hardest problem in retry logic. My approach: record the attempt with a "pending" status, query the processor for the transaction status before retrying, and only retry if you can confirm the original didn't succeed.</p>

<p><strong>Partial failures.</strong> A multi-step transaction — authorize, capture, settle — can fail partway through. You authorized the card, but the capture timed out. Now you have a hold on the customer's card with no corresponding charge. You need compensating actions: release the authorization if capture fails, and track the state of each step independently.</p>

<p><strong>Duplicate charges.</strong> Even with idempotency keys, duplicates happen. Maybe the key TTL expired. Maybe you're integrating with a processor that doesn't support idempotency natively. Build reconciliation into your system from day one. A nightly job that compares your records against the processor's records catches duplicates before they become customer complaints.</p>

<p><strong>Race conditions in concurrent retries.</strong> If your retry logic runs on multiple instances (which it does in any distributed system), two instances might retry the same failed transaction simultaneously. Both get through before the idempotency check kicks in. Use distributed locks or optimistic concurrency on the transaction record to prevent this.</p>

<h2>When NOT to retry</h2>

<p>Not every failure is retryable. Knowing when to stop is as important as knowing when to try again.</p>

<ul>
<li><strong>Hard declines.</strong> Expired card, stolen card, or insufficient funds. Retrying won't help and will annoy the issuer.</li>
<li><strong>Fraud declines.</strong> The processor's fraud system flagged the transaction. Retrying is actively harmful — it looks like a fraud pattern.</li>
<li><strong>Validation errors.</strong> Invalid card number, bad CVV, malformed request. Fix the input, don't retry the same bad data.</li>
<li><strong>4xx errors in general.</strong> These are client errors. Retrying the same request will produce the same result.</li>
</ul>

<p>Only retry on <strong>transient failures</strong>: network timeouts, 502/503/504 responses, connection resets, rate limits (with appropriate backoff). Everything else should fail fast and surface the error to the caller.</p>

<h2>What I've learned the hard way</h2>

<p>The retry mechanism itself needs monitoring. Track retry rates by endpoint, by error type, by time of day. A spike in retries is often the first signal of a downstream degradation — before the alerts fire, before the dashboards turn red.</p>

<p>Log every retry attempt with the original request ID, the retry count, the error that triggered it, and the delay before the next attempt. When something goes wrong (and it will), this log is the difference between a 10-minute investigation and a 4-hour one.</p>

<p>And test your retry logic under failure conditions, not just in happy-path integration tests. Kill the downstream service mid-request. Inject random timeouts. Simulate a processor that accepts the charge but drops the response. These scenarios aren't hypothetical — they're Tuesday.</p>
]]></content:encoded>
    </item>
        <item>
      <title>API design for sensitive and settlement data</title>
      <link>https://anthonyterrell.com/blog/api-design-sensitive-data</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/api-design-sensitive-data</guid>
      <pubDate>Tue, 06 Aug 2024 00:00:00 +0000</pubDate>
      <description>Designing APIs for PII, financial records, and settlement data isn&#039;t the same as a standard CRUD service. When the data is sensitive, the design principles shift.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>Designing an API that handles PII, financial records, or settlement data isn't the same as building a standard CRUD service. The stakes are different. A bug in your blog API shows the wrong post. A bug in your settlement API sends the wrong amount to the wrong account. The design principles shift when the data is sensitive.</p>

<h2>Field-level encryption and data masking</h2>

<p>Most teams encrypt data at rest and in transit, and call it done. But when you're dealing with sensitive payloads, <strong>field-level encryption</strong> is worth the complexity.</p>

<p>The idea is simple: instead of encrypting the entire row in your database, you encrypt individual fields — SSN, account number, routing number — with their own keys. This means a developer who has access to query the database still can't read the encrypted fields without the decryption key. It's defense in depth.</p>

<p>For API responses, <strong>data masking</strong> is essential. Your internal service might store a full account number, but your API should return <code>****4521</code>. The masking should happen at the serialization layer, not as an afterthought. I build it into the response DTOs so it's impossible to accidentally leak a raw value.</p>

<blockquote>
  <p>If your API can return unmasked sensitive data without an explicit <code>include_sensitive=true</code> flag and elevated permissions, your default is wrong.</p>
</blockquote>

<h2>Audit trails and immutability</h2>

<p>Every read and write of sensitive data needs an audit trail. Not just "who changed what" but "who viewed what." In financial systems, being able to prove that only authorized users accessed settlement records is a regulatory requirement, not a nice-to-have.</p>

<p>I design audit logging as a <strong>first-class concern</strong>, not middleware bolted on later:</p>

<ul>
<li>Every API endpoint that touches sensitive data emits an audit event.</li>
<li>Events include the actor, timestamp, action, resource ID, and which fields were accessed or modified.</li>
<li>Audit records are <strong>append-only</strong>. Nobody can delete or modify them. I typically write these to a separate data store with restricted access — sometimes an immutable ledger service.</li>
</ul>

<p>For settlement data specifically, <strong>immutability</strong> is critical. Once a settlement record is finalized, it should never be mutated. If a correction is needed, you create a new adjustment record that references the original. This gives you a complete history and makes reconciliation possible.</p>

<h2>Access control patterns</h2>

<p>Role-based access control (RBAC) is the starting point, but it's often not granular enough. A "settlements admin" role might need to view settlement summaries but shouldn't see individual transaction details.</p>

<p><strong>Attribute-based access control (ABAC)</strong> gives you more flexibility. Instead of just checking the user's role, you evaluate attributes of the request: the user's department, the sensitivity classification of the data, the time of day, whether the request is coming from a trusted network.</p>

<p>In practice, I combine both:</p>

<ul>
<li><strong>RBAC</strong> for coarse-grained access — can this user access the settlements API at all?</li>
<li><strong>ABAC</strong> for fine-grained control — can this user see unmasked account numbers for settlements over $10,000?</li>
</ul>

<p>The key is making the access control <strong>declarative and centralized</strong>. If access rules are scattered across controller methods, someone will forget to add a check. I use policy objects or a dedicated authorization service that every endpoint calls before returning data.</p>

<h2>Versioning sensitive APIs</h2>

<p>API versioning is annoying in general. Versioning APIs that carry sensitive payloads is worse.</p>

<p>When you introduce a new version that changes the shape of a sensitive field — say, moving from a flat <code>account_number</code> to a nested <code>account.number</code> — you need to support both versions simultaneously during migration. That means your encryption, masking, and audit logic all need to handle both shapes. It compounds fast.</p>

<p>My approach:</p>

<ul>
<li><strong>Version at the resource level</strong>, not the entire API. <code>/v2/settlements</code> can coexist with <code>/v1/transactions</code> without forcing a full migration.</li>
<li><strong>Deprecate aggressively but migrate carefully.</strong> Give consumers a clear timeline, but don't rush them. A consumer sending sensitive data to a deprecated endpoint is still your problem.</li>
<li><strong>Never change encryption schemes between versions without a migration plan.</strong> If v1 used AES-256-CBC and v2 uses AES-256-GCM, you need a path to re-encrypt existing data or support dual decryption.</li>
<li><strong>Keep the audit trail continuous across versions.</strong> A version change shouldn't create a gap in your audit history.</li>
</ul>

<h2>Design for distrust</h2>

<p>The underlying philosophy for sensitive data APIs is <strong>design for distrust</strong>. Assume every consumer might mishandle the data. Assume every internal service might log the response. Assume someone will try to access data they shouldn't.</p>

<p>That means: mask by default, encrypt at every layer, log every access, version carefully, and make the secure path the easiest path. If doing the right thing requires extra effort from your consumers, they'll eventually cut corners. Make security the default, not the opt-in.</p>
]]></content:encoded>
    </item>
        <item>
      <title>What Years of PHP Taught Me About Python</title>
      <link>https://anthonyterrell.com/blog/php-to-python-transition</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/php-to-python-transition</guid>
      <pubDate>Sun, 03 Mar 2024 00:00:00 +0000</pubDate>
      <description>A senior PHP developer&#039;s honest take on picking up Python -- what transferred immediately, what surprised me, and what I had to actively unlearn.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>I spent the first week wondering why my Python functions kept returning <code>None</code>. I'd been writing PHP for nine years.</p>

<p>This isn't a "PHP vs Python" post. I still respect PHP. Laravel is still excellent. This is about what the transition taught me -- things I couldn't have seen from inside one ecosystem.</p>

<h2>What Made Me Look at Python</h2>

<p>I'll keep this brief.</p>

<p>The PHP/Laravel job market is competitive and narrowing. That's not a knock on the language -- it's a market reality that any honest senior PHP developer will recognize. AI and ML tooling is Python-native. If you want to work in that space, Python is the path.</p>

<p>I built a Python automation tool and realized I actually enjoyed it. The goal wasn't to abandon PHP -- it was to add range.</p>

<h2>What Transferred Immediately</h2>

<p>These are the things PHP prepared me for better than I expected.</p>

<p><strong>Mental models for web backends.</strong> Request/response lifecycle, middleware, dependency injection -- the concepts are identical. Laravel's service container maps almost directly to FastAPI's <code>Depends()</code>. Writing a route handler in FastAPI felt immediately familiar.</p>

<p><strong>Database thinking.</strong> Years of Eloquent ORM made SQLAlchemy feel readable almost immediately. Migration mindset -- never touch the DB manually, always migrate -- transferred perfectly. Query optimization instincts translate directly. N+1 problems look the same in any ORM.</p>

<p><strong>Queue and worker patterns.</strong> Laravel Horizon to ARQ is a conceptual one-to-one. Job idempotency, retry logic, dead letter queues -- same problems, different syntax. Redis as a queue backend: identical.</p>

<p><strong>Testing discipline.</strong> PHPUnit to Pytest is a smaller jump than expected. The habit of writing tests at all is the hard part, and that habit was already built. Fixtures in Pytest are more powerful than Laravel factories in interesting ways -- I found myself wishing I'd had them in PHP.</p>

<p><strong>API design instincts.</strong> REST conventions, status codes, error envelopes -- all language-agnostic. Years of reading Stripe's API design made FastAPI's decorator style feel natural.</p>

<h2>What Genuinely Surprised Me</h2>

<p><strong>Whitespace as syntax takes longer to internalize than you think.</strong> You know indentation matters in Python. You'll still spend 20 minutes debugging an indentation error in week two. PHP's curly braces felt like training wheels I didn't know I was wearing. The surprising upside: whitespace enforcement forces a consistency that PHP codebases rarely have organically.</p>

<p><strong>Python's import system is weird coming from Composer.</strong> <code>from app.core.config import settings</code> versus Laravel's autowiring felt verbose at first. Virtual environments (<code>venv</code>) are a context switch from Composer's project-local approach. Once it clicks, it's fine -- but it takes a week to click.</p>

<p><strong>Type hints are optional, which is a trap.</strong> PHP 7+ pushed me toward type declarations. Python lets you skip them entirely. The temptation to skip types "just for now" is real, and it leads to unmaintainable code fast. My advice: treat type hints as mandatory from day one, even though Python won't force you to.</p>

<p><strong><code>None</code> is not <code>null</code> in exactly the way you think it is.</strong> My first real Python bug came from assuming a function would return something implicitly -- in PHP, a function without an explicit return gives you <code>null</code>, and you learn to check for it. In Python, every function without a return statement gives you <code>None</code>, but the patterns around checking for it are subtly different. PHP's loose comparison (<code>==</code> vs <code>===</code>) trains habits that backfire in Python, where <code>is None</code> is the convention and <code>== None</code> is a code smell.</p>

<p><strong>Async is a first-class concern, not an afterthought.</strong> PHP is synchronous by default. Async is bolted on -- Swoole, ReactPHP. Python's <code>async</code>/<code>await</code> is woven into the standard library and ecosystem. FastAPI is async-native. You can't half-commit to it the way you might with PHP async. This required a genuine mental model shift, not just syntax learning.</p>

<h2>What I Had to Actively Unlearn</h2>

<p>These are the PHP habits that caused real problems in Python.</p>

<h3>Mutable default arguments</h3>

<p>The classic Python gotcha that every PHP developer hits.</p>

<pre><code class="language-python"># This is a bug -- the list is shared across all calls
def add_item(item, items=[]):
    items.append(item)
    return items
</code></pre>

<p>In PHP, default parameter values are re-evaluated each call. In Python, they're evaluated once at function definition time. The list is created once and mutated forever. The fix:</p>

<pre><code class="language-python">def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items
</code></pre>

<p>This one will get you. It got me.</p>

<h3>String interpolation habits</h3>

<p>PHP's <code>"Hello $name"</code> embeds variables naturally. Python f-strings (<code>f"Hello {name}"</code>) are actually better -- more expressive, more capable. But the muscle memory takes time. The subtle trap: forgetting the <code>f</code> prefix and getting a literal string with curly braces. You'll stare at <code>"Hello {name}"</code> in your output for longer than you'd like to admit before you spot it.</p>

<h3>Array thinking vs list/dict thinking</h3>

<p>PHP arrays are everything -- ordered maps, lists, stacks, queues. Python separates these concerns: <code>list</code>, <code>dict</code>, <code>tuple</code>, <code>set</code>, each with distinct semantics. Learning to reach for the right data structure instead of defaulting to "array" made my code meaningfully cleaner. But the first week, everything was a <code>list</code> because that's the closest thing to what I knew.</p>

<h3>Magic methods and facades</h3>

<p>Laravel facades train you to reach for static-feeling APIs everywhere. Python doesn't have this pattern -- explicit dependency passing is the norm. This is actually better architecture, but it felt verbose at first. The transition from <code>Cache::get('key')</code> to passing a cache dependency explicitly made me realize how much magic I'd been relying on without thinking about it.</p>

<h2>What Python Does Genuinely Better</h2>

<p>I'm being fair and specific here. Not generic "Python is great" talking points.</p>

<p><strong>The REPL and iterative exploration.</strong> <code>python -m asyncio</code> for async exploration beats <code>php artisan tinker</code> in several ways. The ability to just drop into a Python shell and test an idea in seconds -- without booting a framework -- changed how I prototype.</p>

<p><strong>The data science and AI ecosystem.</strong> There's no PHP equivalent. Period. NumPy, pandas, scikit-learn, PyTorch -- if this is the direction your career is heading, Python is the only serious option.</p>

<p><strong>Concurrency model.</strong> <code>async</code>/<code>await</code> in Python feels more coherent than anything in PHP's ecosystem. It's built in, well-documented, and the community has converged on patterns that work.</p>

<p><strong>Readability enforcement.</strong> The whitespace thing I resisted in week one genuinely produces more consistent codebases in teams. When you can't argue about brace placement, you argue about things that matter instead.</p>

<h2>What PHP/Laravel Still Does Better</h2>

<p>This section is what makes this post honest. I'm not going to skip it.</p>

<p><strong>Laravel is still the best full-stack web framework in any language for certain use cases.</strong> That's a defensible position, and I'll stand by it. Eloquent's expressiveness for rapid CRUD development has no Python equivalent that's as polished. Laravel Nova, Livewire, the first-party ecosystem -- Python has nothing as cohesive.</p>

<p>The PHP community's investment in developer experience is genuinely impressive. Tinker, Telescope, Horizon UI -- these tools reflect a community that cares about the day-to-day experience of building software, not just the software itself.</p>

<p>For agencies building client sites and SaaS products rapidly, Laravel is still often the right tool. I wouldn't tell a Laravel shop to rewrite in Python. That would be bad advice.</p>

<h2>Honest Advice for PHP Developers Considering Python</h2>

<p>Don't try to learn Python in the abstract. Build something specific. I built an automation tool, and having a real problem to solve made the learning stick in ways that tutorials never could.</p>

<p>Map concepts to Laravel equivalents as you go. It accelerates learning dramatically. When I thought of FastAPI's <code>Depends()</code> as "service container injection," it clicked immediately.</p>

<p>Commit to type hints from day one. Optional typing is a false economy. You'll thank yourself in month two when you're reading code you wrote in week one.</p>

<p>The transition is easier than you think and harder than you think -- in different places. The web concepts transfer cleanly. The ecosystem differences take real adjustment. The syntax is a weekend. The mental models take a month.</p>

<p>You don't have to abandon PHP. Bilingual engineers are more valuable, not less. I'm still writing Laravel. I'm also writing Python. The two make me better at each.</p>
]]></content:encoded>
    </item>
        <item>
      <title>Never Store Money as a Float</title>
      <link>https://anthonyterrell.com/blog/storing-currency-integers</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/storing-currency-integers</guid>
      <pubDate>Mon, 10 Apr 2023 00:00:00 +0000</pubDate>
      <description>Floating point math is a silent source of bugs in payment systems. Here&#039;s why storing currency as integers eliminates an entire class of problems you didn&#039;t know you had.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<pre><code>0.1 + 0.2 = 0.30000000000000004
</code></pre>

<p>If your payment system stores amounts as floats or doubles, this is your codebase.</p>

<p>This isn't academic. I've worked on payment infrastructure where small rounding discrepancies accumulated into real reconciliation headaches. Refunds off by a cent. Totals that don't add up. Accounting teams asking questions you don't have good answers for. All of it traceable back to one decision made early in the data model.</p>

<h2>Why Floats Are Wrong for Money</h2>

<p>I'm not going to spend paragraphs on IEEE 754. Here's the short version.</p>

<p>Floating point numbers can't represent most decimal fractions exactly in binary. <code>0.1</code> in binary is a repeating fraction -- like <code>1/3</code> in decimal. The computer stores the closest approximation it can. For most math, that approximation is fine. For money, it compounds.</p>

<p>The key insight: money is not continuous math. $12.50 is not a measurement that needs floating point precision. It's a discrete count of cents -- 1250. Treating it as anything else introduces errors that don't exist in the real world.</p>

<h2>What Goes Wrong in Practice</h2>

<h3>The accumulation problem</h3>

<p>Process 10,000 transactions at $0.10 each. Float math may give you $999.9999999999998 instead of $1,000.00. Your ledger doesn't reconcile. Your accounting team is unhappy. You're debugging arithmetic.</p>

<h3>The display problem</h3>

<p><code>$12.999999999999998</code> formatted as a price. Or worse -- rounded to <code>$13.00</code> when the actual charge was <code>$12.99</code>. A one-cent error on a single transaction is annoying. Across a million transactions, it's a liability.</p>

<h3>The comparison problem</h3>

<pre><code class="language-python"># This can fail even when amounts "should" be equal
if transaction.amount == expected_amount:
    mark_as_reconciled()
</code></pre>

<p>Two floats that represent the same dollar amount may not be equal due to different calculation paths. I've seen this break reconciliation logic in production -- transactions stuck in an unreconciled state because <code>49.99</code> computed two different ways produced two different float representations.</p>

<h3>The refund problem</h3>

<p>Partial refund logic that uses float arithmetic can produce a refund amount that's off by a fraction of a cent. Most payment processors reject amounts that aren't whole integers. Your refund silently fails, and now you've got a customer support ticket and a confused customer.</p>

<h2>The Fix: Store Cents as Integers</h2>

<p>Simple rule, no caveats needed.</p>

<blockquote>
  <p>Store all monetary amounts as integers representing the smallest currency unit. For USD: cents. For GBP: pence. For JPY: yen (already a whole unit).</p>
</blockquote>

<ul>
<li><code>$12.50</code> becomes <code>1250</code></li>
<li><code>$0.99</code> becomes <code>99</code></li>
<li><code>$1,000.00</code> becomes <code>100000</code></li>
</ul>

<p>In your database:</p>

<pre><code class="language-sql">-- Right
amount INTEGER NOT NULL  -- stored in cents

-- Wrong
amount DECIMAL(10,2)
amount FLOAT
</code></pre>

<p><strong>Why not DECIMAL?</strong> Many developers reach for <code>DECIMAL</code> thinking it solves the float problem. It does solve the binary representation issue, but it still requires your application to handle decimal arithmetic correctly. It introduces the question of "how many decimal places?" which varies by currency. Integer cents is simpler, faster, and unambiguous.</p>

<h2>Working With Integer Amounts in Code</h2>

<p>Conversion only happens at the edges -- input and display.</p>

<pre><code class="language-python"># Input: convert from user-facing decimal to integer cents
def dollars_to_cents(amount: str | float) -&gt; int:
    from decimal import Decimal
    return int(Decimal(str(amount)) * 100)

# Output: convert from integer cents to display string
def cents_to_dollars(amount_cents: int) -&gt; str:
    return f"${amount_cents / 100:.2f}"
</code></pre>

<pre><code class="language-php">// PHP equivalents
function dollarsToCents(string $amount): int {
    return (int) bcmul($amount, '100', 0);
}

function centsToDollars(int $cents): string {
    return '$' . number_format($cents / 100, 2);
}
</code></pre>

<p>When you need to do arithmetic on monetary amounts -- splitting bills, calculating percentages, applying discounts -- use <code>bcmath</code> in PHP and Python's <code>decimal.Decimal</code> module. Not native floats. This is the one place where you work in decimal space before converting back to integers.</p>

<h2>Multi-Currency Gotcha</h2>

<p>Different currencies have different subunits. USD, EUR, and GBP use cents (2 decimal places). JPY uses whole yen (0 decimal places). KWD uses fils (3 decimal places).</p>

<p>A robust system needs to know the currency's exponent to convert correctly. ISO 4217 defines this. Practical advice: store the currency code alongside every amount. Never store an amount without knowing what currency it's in.</p>

<pre><code class="language-python">class Money:
    amount: int       # in smallest unit (cents, pence, etc.)
    currency: str     # ISO 4217 code: "USD", "GBP", "JPY"
</code></pre>

<h2>What Stripe Does (And Why It Matters)</h2>

<p>Stripe's API accepts and returns all amounts as integers in the smallest currency unit. This isn't accidental -- it's a deliberate API design decision made by people who process billions of transactions.</p>

<p>When the payment processor you're integrating with uses integers, and your internal storage uses floats, you're introducing a conversion step that has no reason to exist. Match the representation of the external system you're integrating with.</p>

<h2>The Bottom Line</h2>

<p>The float mistake is common, easy to make, and surprisingly hard to detect in testing -- because the errors are small and intermittent. Fixing it at the data model level, before you write any business logic, costs nothing and eliminates an entire class of bugs.</p>

<p>Store cents. Use integers. Move on to the hard problems.</p>
]]></content:encoded>
    </item>
        <item>
      <title>A/B testing frameworks for backend features</title>
      <link>https://anthonyterrell.com/blog/ab-testing-backend-features</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/ab-testing-backend-features</guid>
      <pubDate>Thu, 16 Feb 2023 00:00:00 +0000</pubDate>
      <description>Not every experiment is a button color. Here&#039;s how to A/B test backend algorithms, queries, and services without a single UI change.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>When most people hear "A/B testing," they picture two different button colors or a rearranged landing page. But some of the most impactful experiments I've run had <strong>zero UI changes</strong>. They were entirely on the backend — different algorithms, new database queries, refactored service calls. And the tooling for that kind of testing is a different beast.</p>

<h2>It's not just feature flags (but it starts there)</h2>

<p>Feature flags are the foundation. At the simplest level, you're toggling a code path on or off for a subset of users. But backend A/B testing goes further — you need <strong>percentage rollouts</strong>, <strong>user segmentation</strong>, and <strong>metric collection</strong> baked into the same system.</p>

<p>I've used a few approaches over the years:</p>

<ul>
<li><strong>LaunchDarkly</strong> — The most polished option. Great SDK support, targeting rules, and built-in analytics hooks. The downside is cost. Once you're past the free tier, it adds up fast for a growing team.</li>
<li><strong>Unleash</strong> — Open-source alternative that covers most of what you need. Self-hosted, so you own your data. The trade-off is operational overhead — you're running another service.</li>
<li><strong>Homegrown solutions</strong> — I've built these too. A database table of experiments, a simple evaluation engine, and some middleware. It works for small teams, but it gets messy fast. You end up reinventing targeting logic, audit trails, and rollback mechanisms that the dedicated tools already handle.</li>
</ul>

<blockquote>
  <p>The best framework is the one your team will actually use consistently. A fancy setup that only one person understands is worse than a simple config file everyone can read.</p>
</blockquote>

<h2>Measuring success on the backend</h2>

<p>Frontend A/B tests measure clicks and conversions. Backend tests measure different things entirely:</p>

<ul>
<li><strong>Latency</strong> — Did the new query plan make things faster or slower under load?</li>
<li><strong>Error rates</strong> — Are we seeing more 5xx responses in the experiment group?</li>
<li><strong>Business metrics</strong> — Settlement times, transaction throughput, processing costs. These are the numbers that actually matter.</li>
</ul>

<p>The tricky part is attribution. When a request flows through multiple services, you need to propagate the experiment context. I typically pass experiment assignments as headers or metadata through the call chain so downstream services know which variant a request belongs to.</p>

<h2>Canary deployments vs. feature flags</h2>

<p>These are related but different. A <strong>canary deployment</strong> routes a percentage of traffic to a new version of an entire service. A <strong>feature flag</strong> toggles a specific code path within the same deployment.</p>

<p>I use canaries for infrastructure-level changes — new runtime versions, dependency upgrades, major refactors. Feature flags are for logic changes — a new pricing algorithm, a different fraud scoring model, an alternative retry strategy.</p>

<p>Combining both gives you the most control. Deploy the new code behind a flag, canary the deployment to 5% of traffic, then gradually enable the flag for more users within that canary group.</p>

<h2>The gotchas nobody warns you about</h2>

<p><strong>Stateful services</strong> are the biggest headache. If your service maintains in-memory state or sticky sessions, you can't just flip a flag mid-request. You need to evaluate the experiment assignment once and carry it through the entire session lifecycle.</p>

<p><strong>Database migrations</strong> during experiments are painful. If variant B requires a new column or table, you need that schema in place for everyone, even though only a fraction of traffic uses it. I've learned to keep schema changes decoupled from experiment logic — migrate first, flag second.</p>

<p><strong>Cache invalidation</strong> will bite you. If variant A and variant B produce different responses for the same cache key, you'll serve stale or incorrect data. The fix is to include the variant in the cache key, but that effectively splits your cache and reduces hit rates. Plan for the capacity impact.</p>

<p><strong>Interaction effects</strong> between experiments are real. Running two backend experiments simultaneously on overlapping user segments can produce confounding results. Most mature frameworks support mutual exclusion groups — use them.</p>

<h2>When to roll your own</h2>

<p>Honestly? Almost never. But if your constraints are unusual — air-gapped environments, extreme latency requirements, regulatory restrictions on third-party services — a lightweight homegrown solution might be justified. Just keep it simple. A config file, a hash-based assignment function, and structured logging will get you surprisingly far.</p>

<p>The important thing is to <strong>start experimenting on the backend at all</strong>. Too many teams treat their server-side code as a monolith that changes only through big-bang releases. Small, measured, reversible changes are just as valuable behind the API as they are in front of it.</p>
]]></content:encoded>
    </item>
        <item>
      <title>Work life balance</title>
      <link>https://anthonyterrell.com/blog/work-life-balance</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/work-life-balance</guid>
      <pubDate>Mon, 11 Apr 2022 00:00:00 +0000</pubDate>
      <description>After working remotely since 2011, I don&#039;t love the term &#039;work life balance&#039; anymore. But here&#039;s what&#039;s actually helped.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>I've worked remotely since 2011, and have spoken / advocated for "work life balance" ever since. Remote was a big mindset shift for me in the beginning. At this point I don't think I love the actual term.</p>

<p>As I was drafting this, Tighten released an episode of Twenty Percent Time and I <strong>highly suggest</strong> giving it a listen.</p>

<p>A couple of things that have also helped me the last 2 years.</p>

<p>1) Ask yourself what you want to change now, and what your ideal balance is.</p>

<p>This inherently gives you a goal roadmap. Also making it easier to visualize your next steps in additions to communicating your needs.</p>

<p>2) Find someone who truly models the work-life balance you're working towards.</p>

<p>Making notes of things such as:</p>

<ul>
<li>Boundaries set, professionally and personally</li>
<li>Routines kept to maintain said balance</li>
<li>How the balance is maintained in actuality; life happens.</li>
</ul>

<p>Keep them close and ask questions, as they now part of your support in your efforts.</p>
]]></content:encoded>
    </item>
        <item>
      <title>Noisy notifications and building for users</title>
      <link>https://anthonyterrell.com/blog/notifications-are-noise</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/notifications-are-noise</guid>
      <pubDate>Mon, 12 Mar 2018 00:00:00 +0000</pubDate>
      <description>Every app is battling for your attention. A short take on notification fatigue and building with your users&#039; sanity in mind.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<blockquote>
  <p>"What is the best daily alert a person could get -- SMS sized, in your opinion?"</p>
</blockquote>

<p>I was building a little app for myself when another dev friend asked me a question regarding notifications. While I was coming up with a funny come back I found myself having more to say than I thought; which brings me here.</p>

<p>Growing up before cell phones, there was no way to contact someone in an instant, other than calling their house phone. The idea of a notification didn't exist. If it was important you either got a phone call or a piece of mail, which expected you to make another phone call. Let's just pretend faxing never existed, m'kay?</p>

<p>Lately everything is a notification. Every app, website and service is battling for your attention. Your bank account notifications are lumped in with your Twitter notifications. <strong>In my opinion only one of those are important.</strong></p>

<p>You as a consumer are now tasked with the responsibility of sifting through that noise to find what's actually important. That means changing preferences and configuring devices and emails, the list goes on.</p>

<p>To answer the original question, there is no best answer. If you notify your users, you are creating additional noise. The best you can do for a user is present them with options for how they want to stay informed.</p>

<p>Personally I disable 90% of my notifications. I usually keep work related email off my phone and also enable <a href="https://support.apple.com/en-us/HT204321">do not disturb mode</a> from 7pm to 7am. That's personal time, even phone calls don't alert me.</p>

<p>If it is truly important I will receive a phone call or a text, during business hours. Everything else I can check-in on individually.</p>
]]></content:encoded>
    </item>
        <item>
      <title>Improving client communication during deployments</title>
      <link>https://anthonyterrell.com/blog/deployments-and-clients</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/deployments-and-clients</guid>
      <pubDate>Wed, 21 Feb 2018 00:00:00 +0000</pubDate>
      <description>How I automated changelogs and deployment notifications to keep clients informed without extra manual work.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>As my deployment processes evolve, I like to find new ways I can communicate <strong>valuable</strong> information to my clients. Of course, I like to automate this process as much as possible so I can focus on what I do best, writing code.</p>

<p>Using Laravel Forge easily allows one way of sharing statuses with its <a href="https://forge.laravel.com/features">deployment notifications</a>. With some clients, I have shared a <a href="https://slack.com/">Slack</a> channel with them so they can see deployment statuses in real time.</p>

<p><em>A downside to this is when a deployment fails, the client will also be notified.</em></p>

<p>Since most of my projects are applications, I use semantic versioning (<a href="https://semver.org/">Semver</a>). When generating changelogs, I compile a list of commits per release. Recently, I have automated and integrated this into my deployment process.</p>

<h3>Generating Changelogs</h3>

<pre><code class="language-bash">#!/usr/bin/env bash
previous_tag=0
for current_tag in $(git tag --sort=-creatordate)
do

if [ "$previous_tag" != 0 ];then
    tag_date=$(git log -1 --pretty=format:'%ad' --date=short ${previous_tag})
    printf "## ${previous_tag} (${tag_date})\n\n"
    git log ${current_tag}...${previous_tag} --pretty=format:'*  %s [View](https://bitbucket.org/projects/test/repos/my-project/commits/%H)' --reverse | grep -v Merge
    printf "\n\n"
fi
previous_tag=${current_tag}
done
</code></pre>

<p>After a successful deployment, I run this bash script which generates the current changelog. Once the changelog.md file is updated, I trigger an <a href="https://laravel.com/docs/master/artisan">Artisan</a> command that sends an email to myself and the client with the changelog as an attachment.</p>

<p>Now whenever I trigger a deployment, my client receives a detailed changelog of what is in the current release. Along with the changelog the email itself is indication that the deployment has been finished.</p>
]]></content:encoded>
    </item>
        <item>
      <title>Cleaning up controllers with model observers</title>
      <link>https://anthonyterrell.com/blog/model-observers</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/model-observers</guid>
      <pubDate>Thu, 15 Feb 2018 00:00:00 +0000</pubDate>
      <description>How to use Laravel model observers to abstract critical logic out of controllers and into maintainable, testable classes.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>Lately I've been doing a lot of refactor work and wanted to share a great way to clean up some of those "god objects" and growing controllers. Often times we place a lot of responsibility on a single area of the application and it can get a bit hard to maintain.</p>

<p>For this piece, we will use a Blog post as an example. Lets pretend for a second the <a href="https://spatie.be/en/opensource">Spatie</a> <a href="https://github.com/spatie/laravel-sluggable">Sluggable</a> package doesn't exist, and that they're not awesome at everything they do. I know, a hard feat!</p>

<h3>Example</h3>

<pre><code class="language-php">/**
 * Store a newly created resource in storage.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return \Illuminate\Http\Response
 */
public function store(Request $request)
{
    $post = new BlogPost($request-&gt;all());
    $post-&gt;generateSlug(); // generate a url friendly slug of the article
    $post-&gt;save();
    $post-&gt;publish(); // some logic to officially publish the article

    // return response
}
</code></pre>

<p>This is fairly straight forward. We are creating a new BlogPost, filling it with the form input and then performing a couple actions afterwards. There's really nothing wrong with this workflow. Though, like anything else this can be improved.</p>

<p>The issue I find with code like this is, there's critical functionality being called in a potentially hard to discover fashion. ~~If~~ When there are any updates, you'll need to track down where you've manually called these functions.</p>

<h3>Model Observers</h3>

<p><a href="https://laravel.com/docs/5.6/eloquent#observers">Model observers</a> are very similar to <a href="https://laravel.com/docs/master/eloquent#events">Events &amp; Listeners</a>. You can trigger logic on events such as: <code>creating</code>, <code>created</code>, <code>updating</code>, <code>deleting</code>, etc. Observers, being their own classes, are abstracted away from the model.</p>

<p>Lets see what this could look like using a model observer.</p>

<pre><code class="language-php">/**
 * BlogPost creating event
 *
 * @param \App\BlogPost $blogPost
 */
public function creating(BlogPost $blogPost)
{
    $blogPost-&gt;generateSlug();
}

/**
 * BlogPost created event
 *
 * @param \App\BlogPost $blogPost
 */
public function created(BlogPost $blogPost)
{
    $blogPost-&gt;publish();
}
</code></pre>

<p>Now whenever the app is <strong>creating</strong> a new BlogPost, it will generate a slug <em>before it finishes persisting to the datastore.</em> Abstracting this logic here, removes the need of manually triggering critical logic.</p>

<p>Back to our BlogPostController.</p>

<pre><code class="language-php">/**
 * Store a newly created resource in storage.
 *
 * @param  \App\Http\Requests\BlogPostRequest  $request
 * @return \Illuminate\Http\Response
 */
public function store(BlogPostRequest $request)
{
    try {
        BlogPost::create($request-&gt;all());
    } catch (Exception $exception) {
        // error handling
    }

    // return response
}
</code></pre>

<p>We've reduced the logic to store a new instance to one line. That one line being a Laravel standard <code>create()</code> call. All critical logic is in <em>one</em> place behind the scenes.</p>

<p>We've also ditched the standard Illuminate request for a custom form request object <code>BlogPostRequest</code>. <em>This way we know the data coming in has already passed a level of validation.</em></p>

<p>Now our responsibilities are contained. Our controllers handle requests and validation, models handle their data objects and our observers listen in between to ensure the consistency of our app.</p>

<h3>When should I use Model Observers?</h3>

<p>The best use for Observers are critical logic that needs to be performed during model <a href="https://en.wikipedia.org/wiki/Create,_read,_update_and_delete">CRUD</a> events.</p>

<p>Take entering in a hotel booking for example. The observer can be the last opportunity to throw an exception -- maybe the room was reserved during the time it took to submit the request.</p>

<h3>When not to use Model Observers?</h3>

<p>Observers can really improve the maintainability of your projects, though like anything else, should be used with caution. Creating Observers that are hard to maintain and test defeats their purpose.</p>

<p><strong>What about triggered third party services or API calls?</strong></p>

<p>While this <em>can</em> be done with observers, in my opinion it should be kept to a minimum. When deciding where to put event driven functionality I ask myself a question.</p>

<p><em>"How many other events are being triggered by this particular event?"</em></p>

<p><strong>If the answer is two or more, I'm likely to reach for a full featured Event and a Listener.</strong></p>

<p>A use case for an event listener can be user registration. During this event a few things may need to happen:</p>

<ol>
<li>Send a welcome or confirmation email to the user.</li>
<li>Process some data against the user profile, or build a report.</li>
<li>Schedule a job to synchronize data to a marketing service.</li>
</ol>

<p>Since a lot is happening there, it'd be better maintained in a full listener for that particular event.</p>

<h3>Summary</h3>

<ol>
<li>If there's logic that needs to be triggered on model events, an observer can help maintainability.</li>
<li>Don't overcomplicate your model observers, the goal is maintainability and stability.</li>
<li>If two or more events need to be triggered, reach for an event listener.</li>
<li>Refrain from triggering data cascading logic. Keep this responsibility on the datastore itself when possible.</li>
<li>Refrain from calling too many APIs or sending emails, those should be handled elsewhere.</li>
<li>Like anything else, Observers should be tested, keep this in mind.</li>
</ol>
]]></content:encoded>
    </item>
        <item>
      <title>Why you should learn PHP</title>
      <link>https://anthonyterrell.com/blog/why-you-should-learn-php</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/why-you-should-learn-php</guid>
      <pubDate>Sat, 30 Dec 2017 00:00:00 +0000</pubDate>
      <description>A friend asked for three reasons new developers should learn PHP. Here&#039;s what came to mind -- plus a bonus.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>The other night another dev friend of mine texted me, asking for <strong>3 reasons why I think new developers should learn PHP</strong>. I thought this was a cool moment to look back on my experience with programming and PHP. I'm a bit biased so the answers came rather quickly.</p>

<h2>1. Stability</h2>

<p>The language has been around for quite some time. There are a lot of examples and frameworks to learn from. All at this point are tried and true since there's been years of contribution and LTS (long term support).</p>

<h2>2. Versatility</h2>

<p>PHP can be used to write a quick and dirty script to accomplish one task or fine tuned to scale large platforms. What I like is how close it can work at lower levels of the server. Once you have access to a <a href="https://en.wikipedia.org/wiki/LAMP_(software_bundle)">LAMP stack</a> your possibilities are almost endless due to argument 1.</p>

<h2>3. Community</h2>

<p>While I came on when things were already turning over in the PHP community, I can say that in the past 10 years the language itself has made some really great steps forward. Adapting new concepts to stay current but true to its nature not stepping into the fad spotlight.</p>

<p>A great example of this is looking at the Laravel framework, what it's done for the community and the dozens of advocates of the platform surrounding it. Very forward thinking and shaping a new community.</p>

<h2>Bonus. Jobs</h2>

<p>Due to a lot of that legacy LTS code, there are plenty of PHP positions. They range from the super lean hip startups to the corporate world. The salaries for mid-senior level positions are well worth the investment to learn the language.</p>

<hr />

<p><strong>Footnotes</strong></p>

<p>PHP actually gets a lot of hilarious hate in the programming community. Really developers are extremely anal and opinionated people, so basically we hate <em>everything</em>. A good source for dev laughs, PHP included, is <a href="https://www.reddit.com/r/ProgrammerHumor">/r/programmerhumor</a> on Reddit.</p>
]]></content:encoded>
    </item>
        <item>
      <title>When not to use a framework</title>
      <link>https://anthonyterrell.com/blog/when-not-to-use-a-framework</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/when-not-to-use-a-framework</guid>
      <pubDate>Tue, 12 Dec 2017 00:00:00 +0000</pubDate>
      <description>A contractor built a custom framework inside Laravel. Here&#039;s why that&#039;s a problem, and when rolling your own actually makes more sense.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>I'm currently working on a project that involves rebuilding a product that was once built by a contractor. I've been looking through this previous codebase and I noticed a few things that got me scratching my head. I couldn't quite squeeze my thoughts into ~~140~~ 240 characters, so this was a better space.</p>

<p><em>There are some obvious issues with this scenario. The first being, how the contractor built this product without my client being fully aware of code quality.</em></p>

<p>The codebase I was looking through was using a framework, Laravel. What I noticed though is that the developers barely used <em>any</em> of Laravel's out-of-box tools and helpers. The developers went out of their way to engineer their own framework within a framework. Framework inception!</p>

<blockquote>
  <p>Don't do this! Framework inception is bad, m'kay?</p>
</blockquote>

<p>I've also worked on projects that had their own custom framework. It was more of a bunch of vanilla code that loosely matched the companies code style. Things like data models, logging and API methods.</p>

<p>I can only imagine the headaches these developers had. Every step of the way they were up against how Laravel was suppose to work. No Eloquent models were used correctly, no database migrations ran, nothing. It was vanilla PHP fighting against the request stack, middleware, and the built in ORM.</p>

<p>This is a perfect scenario of when <strong>not</strong> to use a framework. This company should have built their own custom framework. In this case, they really already did. For them, working with a framework was quite simply swimming upstream in a swimming race.</p>

<p><strong>Know your tools and more importantly, <em>know when to use them</em>.</strong></p>

<hr />

<h4>A layman's explanation of a development framework</h4>

<p>Building a product with a framework is like building a Lego car with a box set vs building with spare Lego you have laying around the house. Everything you need to build a car comes in that set, almost. If you want fancy wheels, you can get a separate Lego set with fancy wheels and add those on.</p>
]]></content:encoded>
    </item>
        <item>
      <title>What happens during a meeting in my home</title>
      <link>https://anthonyterrell.com/blog/what-happens-during-a-meeting-in-my-home</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/what-happens-during-a-meeting-in-my-home</guid>
      <pubDate>Wed, 22 Nov 2017 00:00:00 +0000</pubDate>
      <description>An accurate, GIF-based depiction of what my wife and kids do during a lengthy conference call. No explanation needed.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>It's no secret that I work from home, and that I'm an advocate for more positions being remote. My personal distractions and home life has evolved as I've became a husband and most definitely since I've been a father.</p>

<p>Below is an accurate description of the progression of my wife &amp; children during a lengthy conference call. To leave room for interpretation, I will leave no description or explanation.</p>

<p><img src="https://media.giphy.com/media/4W486btP1Q14Q/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/v5V3XxJCV0pFu/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/XGuJ1Z5WrrIsM/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/ClbZrxHClgrdu/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/5xyw3mqekqOVW/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/3o6Ztpe2hEveO3PPZm/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/VH8QBcXLrfz1u/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/10MrzQU61tJlVm/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/NWg7M1VlT101W/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/xT8qBvH1pAhtfSx52U/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/26xBHgdvD0dN1ZXby/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/xUA7b5D7GhLs1ZXjKU/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/OwhrqQmSu36GQ/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/Wp4HCRN8MCiSk/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/MhHcCN6PoTdL2/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/t6f2bNAjx7Bio/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/13xb3GPki9Kqdi/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/rKHFZzVwZ1uJq/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/c4t11obaChpu0/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/Z3cS2TfVAwNfW/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/13NGnjve8XVTC8/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/kh8ysL8e41xiE/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/R0pOiDuDY45Og/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/GiV7AxwduuKac/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/IABBXfhPp8iRO/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/tFpFEmppuiAFi/giphy.gif" alt="" /></p>

<p><img src="https://media.giphy.com/media/l0Hee6sB3fXbAdhRK/giphy.gif" alt="" /></p>
]]></content:encoded>
    </item>
        <item>
      <title>Northshore Code and Coffee</title>
      <link>https://anthonyterrell.com/blog/northshore-code-and-coffee</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/northshore-code-and-coffee</guid>
      <pubDate>Wed, 16 Aug 2017 00:00:00 +0000</pubDate>
      <description>A casual weekly meetup for developers on Chicago&#039;s Northshore -- coffee in the morning, code all day, beer in the evening.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<h3>Where, when and how?</h3>

<p><strong>Location:</strong> <a href="http://www.hoosiermamapie.com/evanston.html">Hoosier Mama Pie Company &amp; Dollop</a></p>

<p><strong>Time:</strong> Every Friday, between 8:30am-4PM</p>

<hr />

<p>I've been working remotely for roughly 7 years now as of this post. In that time visits to an office became less frequent. So the normal watercooler talk and office life became another part of my day to maintain.</p>

<p>Living in Evanston, we have lots of talent. Being on the Northshore with Northwestern University and Loyola University in the neighborhood south there's a lot of students and residents. I wanted to create a space where anyone can meetup at a local coffeeshop and work together.</p>

<p>For the time being, this is not structured in anyway. Some days I am there all day, some days just the afternoons. I originally chose Friday as for the most part it's a slower day for everyone. This makes it easier to get some work done while still being able to engage with one another.</p>

<p>As the day closes, some of us head a block north to <a href="http://www.sketchbookbrewing.com/">Sketchbook Brewery</a> for a pint and to finish the day. This is a great family friendly micro-brewery and there's great craft sodas for anyone who isn't a fan of beer.</p>

<p><em>If this continues to grow, more arrangements may be made in regards to location and timing.</em></p>
]]></content:encoded>
    </item>
        <item>
      <title>30</title>
      <link>https://anthonyterrell.com/blog/thirty</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/thirty</guid>
      <pubDate>Tue, 08 Aug 2017 00:00:00 +0000</pubDate>
      <description>A short reflection on turning thirty, looking back at the last decade, and setting the next round of goals.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<h2>Today I turned thirty.</h2>

<p>I honestly haven't thought about that number until a few days ago. I don't see turning thirty as such a big ordeal. Admittedly it is more uncomfortable than I originally had thought it would be. I let it sink in and started replaying the last decade or so.</p>

<p>One thing however is a big deal to me, <em>looking at where I am today</em>. I have achieved and surpassed every dream I had as a young kid. I've been grinding every day and night. There were many difficult decisions, tasks and feats to overcome and I curb stomped them; walking by without looking back.</p>

<blockquote>
  <p>I set ridiculously specific outlandish goals, and I crushed the shit out of every single one of them.</p>
</blockquote>

<p>As I near the end of my first day of being thirty, I've already come up with my next set of goals. To the next chapter!</p>
]]></content:encoded>
    </item>
        <item>
      <title>I have a lot of work to do</title>
      <link>https://anthonyterrell.com/blog/i-have-a-lot-of-work-to-do</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/i-have-a-lot-of-work-to-do</guid>
      <pubDate>Tue, 14 Mar 2017 00:00:00 +0000</pubDate>
      <description>A reflection on civic responsibility, fatherhood, and why showing up on election day isn&#039;t enough.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<h3>Preface</h3>

<p><em>I have had these thoughts rolling around in my head for years and not really sure how to formulate them into something remotely comprehensive. That may be in part of growing as an individual and a father. I know very little about how our government works, but I'm learning because <strong>I need to hold myself accountable</strong>.</em></p>

<h3>Prologue</h3>

<p>When I was younger I felt that a lot of our needed progression, while obvious to me, would be automated for me. "The adults will take care of this," or "the <strong>government</strong> will do its thing" were my thoughts.</p>

<p>I grew up in rural New York and at times it was pretty tough. About 95% of the family I grew up around were white, the schools I went to I was absolutely the minority.</p>

<p>My years were spent wading through socially acceptable racism and bigotry. Lets not forget the 90s were chock full of zany racially and homophobically charged movies. Movies like: Nutty Professor, Boat Trip, Norbit, Madea's <em>&#95;&#95; all never sat right with me. I felt even more alienated and like the butt of many jokes. But I'm not going to die on &#95;this</em> hill.</p>

<blockquote>
  <p>Nevertheless I always voted. As soon as I could vote, I did and I loved the empowerment.</p>
</blockquote>

<p>I'll never forget watching Obama being elected. I no longer felt alone -- there was this relatable voice of reason, <strong>strength</strong> and comradery.</p>

<hr />

<p><em>I</em> need to do more. I can't just wait until what a majority of the United States considers "the" election day. There's more than just two important people. There's a host of elections that shape our government, all have parties and their own agendas.</p>

<blockquote>
  <p>The 2016 election was a realization that I needed to do more than just show up and choose between the two Presidential Elects.</p>
</blockquote>

<p>This is the last election where I once again show up at the last minute and tick a box and think I "did my job." While I was embarrassed that we let, our now President, get this far in the election -- I was overly confident he couldn't possibly win. Right?</p>

<p>Our country doesn't honestly think this man should represent us at the Presidential level? Can I understand the idea of having a politician who speaks like a regular citizen, yes. <strong>But this man isn't even a politician.</strong></p>

<p>The moment the results came in, I was in shock. I participated in the rage retweeting and sharing. I in no way could process that this is where we were. Then I started to pay more attention and felt this overwhelming weight of responsibility. There's so many core values that I now see I need to fight for.</p>

<blockquote>
  <p>I'm now the adult that needs to take care of things</p>
</blockquote>

<p>I look at my children and I want things to be different for them, like most parents would. My son for the most part should be fine. He's from a bi-racial family, but the reality is the United States population can consider him "white" on sight. When I look at my daughter I see what seems like decades of hard work and change to help sculpt our society to become an accepting <em>and</em> fair place for her.</p>

<p>I can't say I'm happy that in recent news there has been some social justice for women, because <strong>I'm not</strong>. I'm not happy that women have felt silenced for so long. I'm not happy that the majority responds by trying to justify a villain. I'm not happy that even though some of these villains are getting the repercussions they deserve, because the victims are exposed and have been living those repercussions already.</p>

<p>There's work to do. There's an internal process that's necessary to see these core values that will become my voice and my families voice by extension. I then need to work on transcending those to my children so they can be outstanding citizens. I need to do my part as a parent to show them life outside our safe bubble.</p>

<p>Retweeting isn't enough. Wearing a t-shirt isn't enough. It's way more than that. We need to educate and limit the "thoughts and prayers" behavior. <strong>There has to be results!</strong></p>

<hr />

<h3>Epilogue</h3>

<p>I'm learning all of this as I go, kinda like this dad thing. I was never completely immersed in politics and how I can be a true impact in my community, country and on a global scale. I'm watching, I'm keeping track and trying to find where I can help. I can't and won't be silent.</p>
]]></content:encoded>
    </item>
        <item>
      <title>Hire More Veterans</title>
      <link>https://anthonyterrell.com/blog/hire-more-veterans</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/hire-more-veterans</guid>
      <pubDate>Wed, 20 Jul 2016 00:00:00 +0000</pubDate>
      <description>The story behind launching Hire More Veterans -- a project inspired by my mother and our family&#039;s military background.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>For quite some time I've really wanted to launch this project. As any developer knows, there are a million things we start and very few see launch day. In truth I've never really <strong>officially</strong> launched anything besides a personal website/blog.</p>

<blockquote>
  <p>I drew inspiration from my mother, whom I consider one of the strongest women I know.</p>
</blockquote>

<p>I drew inspiration from my family, our military background. I drew inspiration from my mother, whom I consider one of the strongest women I know. A vet herself, I grew up hearing her stories of traveling and sleeping out in the open in Egypt.</p>

<p>For the first time I can say I've found a problem I personally wanted to solve on a large scale. I hope this project can sustain itself and become a quality community Veterans can rely on to connect with potential jobs. I have a million ideas of what I'd like Hire More Veterans to become, but for now I'd like officially share something I'm very excited about.</p>

<p>For people like my mother, lets get started!</p>
]]></content:encoded>
    </item>
        <item>
      <title>Why you should pay for software</title>
      <link>https://anthonyterrell.com/blog/pay-for-software</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/pay-for-software</guid>
      <pubDate>Wed, 29 Jun 2016 00:00:00 +0000</pubDate>
      <description>A quick take on the Evernote pricing backlash, and why developers of all people should understand that good software costs money.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>I along with thousands of others have received an email from <a href="https://evernote.com/">Evernote</a>, notifying me that the current plan I'm on will be changing. The change will most likely incur a fee based on my usage.</p>

<p>Like a lot of software shops lately, Evernote is rethinking how to prioritize and move their product forward in a way that keeps the lights on. As well as being able to implement features their customers are asking for.</p>

<p>While I don't necessarily agree with <em>how</em> they are changing their plans I <strong>completely agree with companies making changes <em>like</em> these.</strong></p>

<p>Consumers are extremely quick to download a free app, use its service extremely heavily and even promote/praise it. But when it's time to take an action that directly affects the success and stability of the product it's shot down as quick as an email or <a href="https://blog.evernote.com/blog/2016/06/28/changes-to-evernotes-pricing-plans/">blog post</a> is published.</p>

<p>What's disheartening is that my Twitter feed is mostly full of developers and very tech savvy people, those who can grasp that to run a company you must have a business plan and you must <em>make money.</em> I open my feed to see nothing but whining, ranting and negative comments. Other developers pleading for someone to "make an alternative quickly."</p>

<blockquote>
  <p>Someone will make an alternative, it'll show up on all the popular startup sites. Then they'll soon ask for money too because they will be in the same position as Evernote.</p>
</blockquote>

<p>In summary: Evernote's first paid tier is $34.99/yr or $3.99/mo. Basically the cost of parking your car in NYC for 24 hrs or 2/3 the price of your horrible <a href="http://www.starbucks.com/menu/drinks/frappuccino-blended-beverages">Starbucks drink</a> you probably get 3-4 times a week. You can rant all you want, but most likely a few of your non-tech relatives are using Evernote -- which says a lot!</p>

<p><strong>Be nice. If you use the app and want it to stay around, pay for it.</strong></p>
]]></content:encoded>
    </item>
        <item>
      <title>Homestead</title>
      <link>https://anthonyterrell.com/blog/homestead</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/homestead</guid>
      <pubDate>Tue, 29 Mar 2016 00:00:00 +0000</pubDate>
      <description>From MAMP to OS X Apache to Vagrant -- how Laravel Homestead changed my local development setup for the better.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>In the early years, like most other developers, my local environment was configured with <a href="https://www.mamp.info/en/">MAMP</a>. There are OS based equivalents, i.e. <a href="https://www.apachefriends.org/index.html">XAMP</a> and <a href="http://www.wampserver.com/en/">WAMP</a>.</p>

<p>It was pretty straight forward, point and click and you have a stable <a href="https://en.wikipedia.org/wiki/LAMP_(software_bundle)">LAMP</a> stack to develop with.</p>

<p>I actually haven't really reached its limits until I starting working on web applications. These sorts of projects require custom tailored PHP configurations and libraries.</p>

<p>The next setup I used was the internal OS X Apache system. I installed package managers like <a href="http://brew.sh/">homebrew</a> to bolt on packages like MySQL and PHPmyAdmin.</p>

<p>This took forever, for me, and every erase-and-install produced different results. I would do this about every other year. It's my Windows (<em>XP SP2</em>) side still showing.</p>

<p>This setup was pretty solid and I felt "minimal" by not having any applications installed to run my environment. But of course it had its challenges it seemed with every OS X release, and in a couple cases some OS point releases.</p>

<p><a href="https://www.vagrantup.com/">Vagrant</a> was something I was desperate for. At the very least the idea of it. Though with its learning curve, my career path, and family I never seemed to have the allotted time to truly dive in and get my hands dirty.</p>

<p>Luckily I get to work with <a href="https://laravel.com/">Laravel</a> everyday so <a href="https://github.com/laravel/homestead">Homestead</a> changed that. It's unbelievably easy to get setup for both global boxes with multiple projects or as a per-project install. Leveraging Vagrant and <a href="https://www.virtualbox.org/wiki/Downloads">Virtualbox</a> you can spin up an environment with very little work.</p>

<p>Now with a slight addition to my project readme, I can share a few instructions for a fellow engineer to: spin up a virtual server, install Laravel &amp; its dependencies, migrate a database and even seed it with data. We've come a long way from local GUI environment managers.</p>
]]></content:encoded>
    </item>
        <item>
      <title>Migrations and seeds</title>
      <link>https://anthonyterrell.com/blog/migrations-and-seeds</link>
      <guid isPermaLink="true">https://anthonyterrell.com/blog/migrations-and-seeds</guid>
      <pubDate>Wed, 20 May 2015 00:00:00 +0000</pubDate>
      <description>A shift in thinking about Laravel migrations vs seeders -- when to use each, and why seeds should only be for testing.</description>
      <content:encoded xmlns:content="http://purl.org/rss/1.0/modules/content/"><![CDATA[<p>When developing on a long-term application, the data itself becomes an entity you must maintain and look after. You're constantly making releases and if you're "agile", priorities and features often change.</p>

<p>Before I had a simplistic approach to seeds vs migrations. Migrations simply being the architectural change of the database itself, while the seeds place data <em>in</em> the database itself.</p>

<p>Now the edge case for this I've found is when introducing new features where you need to migrate or process data directly after running a schema migration. Often I would do this in one fell swoop within a single migration script.</p>

<blockquote>
  <p>There's no wrong way. These tools are for you and your team to decide on how to use them.</p>
</blockquote>

<p>Lately after dealing with more and more data, as well as a recent episode on the Laravel Podcast, I've had a slightly different mindset and approach.</p>

<p>Migrations, for me and my team, are for three main tasks:</p>

<ol>
<li>Schema changes</li>
<li>Migrating data to new schema changes</li>
<li>Data that is required to properly run the application</li>
</ol>

<p>Seeders are now only ran for testing and faking purposes. This prevents the scenario of <strong>forgetting</strong> to run a seeder after a deployment. It also keeps the data layer <strong>always moving forward</strong>. No need to truncate or touch existing data.</p>

<hr />

<p><strong>Footnotes</strong></p>

<p>While coming to this new mindset I also took a look at Laravel's actual Seed documentation. In the first sentence the word "test" is referenced.</p>

<p>This to me was a moment where I realized that <strong>there is valuable content in documentation besides examples.</strong> Though I may be biased, because Taylor Otwell spends hours on his documentation.</p>

<p><em>Snippet from Laravel documentation</em></p>

<p><em>"Laravel includes a simple method of seeding your database with test data using seed classes. All seed classes are stored in the database/seeds directory"</em></p>
]]></content:encoded>
    </item>
      </channel>
</rss>
