Skip to content
Go back

YAGNI: The Most Useful Word I Learned Building a Laravel App

YAGNI stands for You Aren’t Gonna Need It. The idea is simple: don’t build something until you actually need it. Don’t add a feature because you might want it later. Don’t pick the more complex architecture because it could handle a scenario that doesn’t exist yet.

I’m a founder building a SaaS product (Growth Method) with Laravel, Livewire, and an AI coding agent (Claude Code). I’m not a developer, but I’ve shipped dozens of features, fixed bugs, refactored performance issues, and handled security reports over the past few months.

YAGNI is the word I use the most. It’s become a reflex — whenever the AI proposes something complex, I ask myself: do I actually need this right now? The answer is almost always no.

Here are six real examples of what that looks like in practice.

Table of contents

Open Table of contents

1. Don’t install a package for five lines of code

What I was doing: Refactoring how shared links and team deletion worked. My Idea model uses Laravel’s SoftDeletes trait, which means when you delete an idea, the row stays in the database with a deleted_at timestamp. The problem: foreign key cascades only fire when a row is actually removed, so the related shared_links, stars, and versions were being orphaned.

The AI initially suggested installing dyrynda/laravel-cascade-soft-deletes — a package specifically designed for this problem.

The YAGNI question: I have one model that soft-deletes and needs to clean up three relationships. Is a package dependency worth it for that?

What I did instead: Three lines in the model’s boot() method:

static::deleting(function ($model) {
    $model->sharedLinks()->delete();
    $model->stars()->delete();
    $model->versions()->delete();
});

That’s it. No package, no configuration, no dependency to maintain. If I had fifteen models all needing cascade soft deletes, the calculus would change. But I don’t. YAGNI.

The takeaway: Before adding a package, count how many lines the manual solution takes. If it’s under ten, you probably don’t need the package.

2. Replace a polymorphic relationship with a flat table

What I was doing: My app had a share_user table using Laravel’s polymorphic relationships (morphMany / morphTo) to store share links for both ideas and reports. The shareable_type column stored full PHP class names like App\Models\Idea.

The AI initially treated the polymorphic structure as a given. But when I looked at the actual usage, my app shared exactly two things: ideas and reports. That was it.

What I asked:

ok please plan out moving to a single shared_links table

What I did: Replaced the polymorphic share_user table with a flat shared_links table:

Schema::create('shared_links', function (Blueprint $table) {
    $table->id();
    $table->string('hash', 40)->unique();
    $table->foreignId('idea_id')->nullable()
          ->constrained('ideas')->cascadeOnDelete();
    $table->foreignId('team_id')->nullable()
          ->constrained('teams')->cascadeOnDelete();
    $table->string('period')->nullable();
    $table->string('report_type')->nullable();
    $table->timestamps();
});

The model relationship went from polymorphic to a simple hasMany:

// Before: polymorphic
public function share()
{
    return $this->morphMany(Share::class, 'shareable');
}

// After: direct relationship
public function sharedLinks()
{
    return $this->hasMany(SharedLink::class);
}

The polymorphic design anticipated a future where I’d share many different types of things. After over a year of production use, I shared exactly two things. A simple report_type column handled the distinction without the complexity of storing PHP class names in the database.

The takeaway: Polymorphic relationships solve a real problem — when you genuinely don’t know how many types you’ll need. But if you know the answer today (and it’s two), a flat table with nullable foreign keys is simpler to query, easier to debug, and works with standard database constraints.

3. Skip the vector database

What I was doing: Building a chat feature where users could ask questions about their experiments in natural language. The AI proposed three approaches:

Simple MySQL queriesVector embeddings (pgvector)Hybrid routing
ComplexityLowHighVery high
InfrastructureExisting MySQLRequires PostgreSQL migrationBoth
Query typesFactual and aggregateSemantic similarityAll
Setup timeDaysWeeksWeeks

The AI’s analysis was clear: with hundreds to low thousands of experiments per team, semantic similarity search was solving a problem that didn’t exist yet. MySQL LIKE queries combined with AI interpretation handled keyword-style search adequately at this scale.

What I built: A structured query agent with three tools (QueryExperiments, AggregateExperiments, QueryMetricResults) that query MySQL via Eloquent. No vector database. No embeddings. No PostgreSQL migration.

The architecture was designed so that if I ever needed semantic search, I could add a SimilaritySearch tool to the agent — a one-line change to the tools() method. But I didn’t build it until I needed it.

The takeaway: The fancier technology isn’t always the better choice. At small data scales, simple queries with AI interpretation are nearly as effective as vector search — without the infrastructure overhead.

4. Use what the framework gives you

What I was doing: When an idea was created without a category, the code hardcoded a default channel ID:

// Before: hardcoded database ID
'channel_id' => 21,

This was fragile — it assumed row 21 existed in the channels table and was the right default. The AI proposed three options: a nullable foreign key with withDefault(), a lookup by channel name, or a config file.

What I asked:

I agree with your recommendation, the withDefault() pattern in Laravel was clearly built for this use case, rather than hardcoding a database ID. let’s do that.

What I did:

// After: nullable FK + withDefault()
public function channel(): BelongsTo
{
    return $this->belongsTo(Channel::class)->withDefault([
        'name' => 'Uncategorised',
        'color' => '#F4F3FA',
        'text_color' => '#6A6A9E',
    ]);
}

No extra database query. No dependency on any specific row existing. No config file to maintain. The column was already nullable — withDefault() just uses it as designed.

The lookup-by-name approach would have added a query every time the relationship was accessed. The config approach would have created a new file and a new abstraction. Both were solving problems that didn’t exist.

The takeaway: Before building a custom solution, check if Laravel already has a pattern for it. withDefault() is a documented feature built for exactly this situation. The framework has already solved more problems than you think.

5. Delete the code nobody calls

What I was doing: Cleaning up a Blade component that had a $theme prop supporting both dark and light modes. The component had conditional styling throughout:

// Before: dead code supporting a mode that's never used
@props(['version', 'user', 'theme' => 'dark', 'first' => false])
<li class="py-1 px-2 {{
    $theme === 'dark'
      ? 'hover:bg-gray-700 hover:text-white'
      : 'hover:bg-gray-100 hover:text-gray-900'
  }}">

I checked every call site. theme="light" was never passed anywhere. The only caller always passed theme="dark". The entire ternary logic, the prop definition, and the default value were all dead code.

// After: no prop, no conditional, no dead code
@props(['version', 'user', 'first' => false])
<li class="px-2 text-white">

If someone needs a light theme version row in the future, they can add the prop back. But YAGNI — that future hasn’t arrived, and the dead code was making the component harder to read for no benefit.

The takeaway: Dead code isn’t free. It adds cognitive load every time someone reads the file, and it makes future changes harder because you’re working around logic that doesn’t do anything. If it’s not used, delete it.

6. Reduce the cache TTL instead of building a scheduled job

What I was doing: My dashboard data refreshes roughly six times per day via an external API. The dashboard cache was set to four hours, which meant users sometimes saw stale data.

What I asked:

I want to ensure that if the data refreshes roughly 6 times per day (every 4 hours) that the dashboard is updated around the same time to reflect this refreshed data. what are my options here?

The AI gave me three options: reduce the cache TTL to one hour, build a scheduled job to pre-warm the cache, or both.

The recommendation was the simplest option — change one number:

// Before
Cache::remember($cacheKey, now()->addHours(4), function () use (...) {

// After
Cache::remember($cacheKey, now()->addHours(1), function () use (...) {

The API call is cheap (it goes to a Make webhook), so hitting it more often has no real downside. A scheduled job would guarantee the cache is warm at specific times, but that’s solving a precision problem I don’t have. If I need guaranteed refresh times later, I can add the job then.

The takeaway: The boring solution is usually the right one. Changing a number is less error-prone than building new infrastructure, and it’s easier to adjust if requirements change.

Why YAGNI matters more with AI

Every one of these examples has a pattern: the AI proposed something more complex than what was needed, and I pushed back. This isn’t a criticism of the AI — it’s doing what you’d expect. When you ask an AI to solve a problem, it reaches for the most capable tool, not the simplest one.

That makes YAGNI a critical skill when working with AI coding agents. The AI won’t ask whether you need vector embeddings or whether MySQL is good enough. It won’t check whether a package is worth it for five lines of code. It won’t notice that theme="light" is never used.

You need to ask those questions yourself. And the answer, more often than not, is: you aren’t gonna need it.

As I wrote previously, the most useful prompt I’ve found for learning Laravel is asking what Taylor Otwell would do. The answer almost always points toward the simpler, more idiomatic solution — the one that works with the framework instead of against it. YAGNI is the principle behind that instinct. Don’t build for hypothetical future requirements. Build the simplest thing that works today, and trust that you can change it when the requirements actually arrive.


Back to top ↑