[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-post-thin-controllers-fat-actions":3,"blog-post-rendered-thin-controllers-fat-actions":16,"blog-all-for-thin-controllers-fat-actions":29},{"slug":4,"title":5,"excerpt":6,"body":7,"cover":8,"tags":9,"publishedAt":12,"readingTime":13,"seoTitle":14,"seoDescription":15},"thin-controllers-fat-actions","Thin Controllers, Fat Actions","Controllers should orchestrate, not implement. Moving business logic into dedicated Action classes keeps your Laravel codebase readable, testable, and change-friendly.","## The problem with fat controllers\n\nIt starts innocently enough. A `store` method that validates a request, creates a record, sends an email, and fires an event. Twenty lines. Readable enough.\n\nSix months later that same method is 120 lines. It handles three different code paths depending on the authenticated user's role. The email is conditional on a feature flag. There is a try-catch that swallows exceptions in one branch but re-throws in another.\n\nThis is not a failure of discipline. It is a failure of structure. The controller has no natural boundary to push back against accumulation.\n\n## Actions as the answer\n\nAn Action is a single-purpose PHP class with one public method — `execute` or `handle` — that does exactly one thing. It receives the data it needs as arguments. It performs the work. It returns a result or throws an exception.\n\n```php\nclass SubscribeAction\n{\n    public function execute(string $email, string $locale = 'en'): void\n    {\n        \u002F\u002F create subscriber, send email — nothing else\n    }\n}\n```\n\nThe controller becomes a thin orchestrator:\n\n```php\npublic function store(SubscribeRequest $request, SubscribeAction $action)\n{\n    $action->execute($request->validated('email'));\n    return response()->json(['message' => 'Check your inbox.'], 202);\n}\n```\n\n## What you gain\n\n**Testability.** You can unit-test the Action without booting the HTTP layer. Pass in the arguments, assert the side effects.\n\n**Reusability.** The same Action can be called from a controller, a console command, a job, or a Livewire component.\n\n**Readability.** The controller tells you *what* happens at this endpoint. The Action tells you *how*. The separation of concerns is explicit in the file structure.\n\n**Change safety.** When the subscribe flow changes — say, you add a source tracking field — you change the Action. The controller and its tests are untouched.\n\nThink of Actions as the verbs of your domain. Name them accordingly: `SubscribeAction`, `PublishPostAction`, `ConfirmSubscriptionAction`. Your codebase becomes a vocabulary for the business problem it solves.",null,[10,11],"Engineering","Laravel","2026-07-03T20:27:25+00:00",3,"Thin Controllers, Fat Actions in Laravel","Moving business logic into dedicated Action classes keeps Laravel codebases readable and testable. A practical guide by David Kimani.",{"html":17,"toc":18},"\u003Ch2 id=\"the-problem-with-fat-controllers\">The problem with fat controllers\u003C\u002Fh2>\n\u003Cp>It starts innocently enough. A \u003Ccode>store\u003C\u002Fcode> method that validates a request, creates a record, sends an email, and fires an event. Twenty lines. Readable enough.\u003C\u002Fp>\n\u003Cp>Six months later that same method is 120 lines. It handles three different code paths depending on the authenticated user’s role. The email is conditional on a feature flag. There is a try-catch that swallows exceptions in one branch but re-throws in another.\u003C\u002Fp>\n\u003Cp>This is not a failure of discipline. It is a failure of structure. The controller has no natural boundary to push back against accumulation.\u003C\u002Fp>\n\u003Ch2 id=\"actions-as-the-answer\">Actions as the answer\u003C\u002Fh2>\n\u003Cp>An Action is a single-purpose PHP class with one public method — \u003Ccode>execute\u003C\u002Fcode> or \u003Ccode>handle\u003C\u002Fcode> — that does exactly one thing. It receives the data it needs as arguments. It performs the work. It returns a result or throws an exception.\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-php\">\u003Cdiv class=\"code-block\" role=\"region\" aria-label=\"Code block\">\n  \u003Cdiv class=\"code-block__bar\">\n    \u003Cspan class=\"code-block__lang eyebrow\">php\u003C\u002Fspan>\n    \u003Cbutton\n      class=\"code-block__copy\"\n      type=\"button\"\n      aria-label=\"Copy code\"\n      data-code-index=\"0\"\n    >\n      \u003Cspan class=\"code-block__copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block__content\">\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">class\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\"> SubscribeAction\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">{\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    public\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> function\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\"> execute\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">string\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> $email, \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">string\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> $locale \u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">=\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> 'en'\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">)\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">:\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> void\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    {\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#6A737D\">        \u002F\u002F create subscriber, send email — nothing else\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    }\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n  \u003Ctextarea class=\"sr-only\" aria-hidden=\"true\" data-code-raw=\"class SubscribeAction\n{\n    public function execute(string $email, string $locale = 'en'): void\n    {\n        \u002F\u002F create subscriber, send email — nothing else\n    }\n}\n\" tabindex=\"-1\" readonly>\u003C\u002Ftextarea>\n\u003C\u002Fdiv>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>The controller becomes a thin orchestrator:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-php\">\u003Cdiv class=\"code-block\" role=\"region\" aria-label=\"Code block\">\n  \u003Cdiv class=\"code-block__bar\">\n    \u003Cspan class=\"code-block__lang eyebrow\">php\u003C\u002Fspan>\n    \u003Cbutton\n      class=\"code-block__copy\"\n      type=\"button\"\n      aria-label=\"Copy code\"\n      data-code-index=\"1\"\n    >\n      \u003Cspan class=\"code-block__copy-text\">Copy\u003C\u002Fspan>\n    \u003C\u002Fbutton>\n  \u003C\u002Fdiv>\n  \u003Cdiv class=\"code-block__content\">\u003Cpre class=\"shiki github-dark\" style=\"background-color:#24292e;color:#e1e4e8\" tabindex=\"0\">\u003Ccode>\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">public\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> function\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\"> store\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">SubscribeRequest\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> $request, \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">SubscribeAction\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\"> $action)\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">{\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">    $action\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">->\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\">execute\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">($request\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">->\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\">validated\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">(\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">'email'\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">));\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#F97583\">    return\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\"> response\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">()\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\">->\u003C\u002Fspan>\u003Cspan style=\"color:#B392F0\">json\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">([\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\">'message'\u003C\u002Fspan>\u003Cspan style=\"color:#F97583\"> =>\u003C\u002Fspan>\u003Cspan style=\"color:#9ECBFF\"> 'Check your inbox.'\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">], \u003C\u002Fspan>\u003Cspan style=\"color:#79B8FF\">202\u003C\u002Fspan>\u003Cspan style=\"color:#E1E4E8\">);\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003Cspan style=\"color:#E1E4E8\">}\u003C\u002Fspan>\u003C\u002Fspan>\n\u003Cspan class=\"line\">\u003C\u002Fspan>\u003C\u002Fcode>\u003C\u002Fpre>\u003C\u002Fdiv>\n  \u003Ctextarea class=\"sr-only\" aria-hidden=\"true\" data-code-raw=\"public function store(SubscribeRequest $request, SubscribeAction $action)\n{\n    $action-&gt;execute($request-&gt;validated('email'));\n    return response()-&gt;json(['message' =&gt; 'Check your inbox.'], 202);\n}\n\" tabindex=\"-1\" readonly>\u003C\u002Ftextarea>\n\u003C\u002Fdiv>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"what-you-gain\">What you gain\u003C\u002Fh2>\n\u003Cp>\u003Cstrong>Testability.\u003C\u002Fstrong> You can unit-test the Action without booting the HTTP layer. Pass in the arguments, assert the side effects.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Reusability.\u003C\u002Fstrong> The same Action can be called from a controller, a console command, a job, or a Livewire component.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Readability.\u003C\u002Fstrong> The controller tells you \u003Cem>what\u003C\u002Fem> happens at this endpoint. The Action tells you \u003Cem>how\u003C\u002Fem>. The separation of concerns is explicit in the file structure.\u003C\u002Fp>\n\u003Cp>\u003Cstrong>Change safety.\u003C\u002Fstrong> When the subscribe flow changes — say, you add a source tracking field — you change the Action. The controller and its tests are untouched.\u003C\u002Fp>\n\u003Cp>Think of Actions as the verbs of your domain. Name them accordingly: \u003Ccode>SubscribeAction\u003C\u002Fcode>, \u003Ccode>PublishPostAction\u003C\u002Fcode>, \u003Ccode>ConfirmSubscriptionAction\u003C\u002Fcode>. Your codebase becomes a vocabulary for the business problem it solves.\u003C\u002Fp>\n",[19,23,26],{"id":20,"text":21,"level":22},"the-problem-with-fat-controllers","The problem with fat controllers",2,{"id":24,"text":25,"level":22},"actions-as-the-answer","Actions as the answer",{"id":27,"text":28,"level":22},"what-you-gain","What you gain",[30,32],{"slug":4,"title":5,"excerpt":6,"body":7,"cover":8,"tags":31,"publishedAt":12,"readingTime":13,"seoTitle":14,"seoDescription":15},[10,11],{"slug":33,"title":34,"excerpt":35,"body":36,"cover":8,"tags":37,"publishedAt":39,"readingTime":13,"seoTitle":34,"seoDescription":40},"why-i-always-start-with-the-data-model","Why I Always Start With the Data Model","Before I write a single line of application code, I sit with the schema. Here is why the data model is the most important design decision you will make on any project.","## The schema is the truth\n\nEvery application is, at its core, a set of transformations applied to data. The UI is a view. The API is an interface. But the schema is the truth of what your system believes about the world.\n\nWhen I take on a new project, the first artifact I produce is not a wireframe or a component tree. It is an entity-relationship diagram — even a rough one sketched on paper. I want to understand what *things* the system knows about, how those things relate to each other, and what facts it needs to record.\n\n## Constraints are features\n\nA `NOT NULL` constraint, a `UNIQUE` index, a foreign key — these are not implementation details. They are business rules encoded directly into the database engine. The further a constraint lives from the data, the easier it is to violate.\n\nI have inherited too many codebases where validation lived exclusively in the application layer, and the database was a free-for-all. Orphaned records, duplicate emails, null values in columns the business assumed were always populated. Schema-level constraints prevent entire categories of bugs before you write a single test.\n\n## Design for queries, not inserts\n\nThe shape of your data should be driven by how you need to read it, not just how you write it. If you will always load a post with its tags, model that relationship explicitly. If you will frequently filter subscribers by status, index that column.\n\nStart with the queries the product requires. Work backwards to the schema that makes those queries fast and simple. The insert path is almost always easy; it is the read path that reveals whether your model is right.\n\n## Change is inevitable — make it safe\n\nNo schema survives contact with production unchanged. The question is not whether you will migrate, but whether your migrations are reversible and your team trusts the process.\n\nI write every migration with a `down()` method. I run migrations against a copy of production data before deploying. I version-control every schema change alongside the application code that depends on it.\n\nThe data model is the hardest thing to change once a system is live. Getting it right early is the highest-leverage investment you can make.",[10,38],"Product","2026-06-29T20:27:25+00:00","Before writing application code, David Kimani starts with the schema. Here is why the data model is the most important design decision on any project."]