<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Technology &#8211; Rafael Bernard Araujo</title>
	<atom:link href="https://rafael.bernard-araujo.com/categoria/technology/feed" rel="self" type="application/rss+xml" />
	<link>https://rafael.bernard-araujo.com</link>
	<description>desenvolvendo... while(!success){  try(); }</description>
	<lastBuildDate>Sun, 20 Sep 2026 22:29:32 +0000</lastBuildDate>
	<language>pt-BR</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	
<site xmlns="com-wordpress:feed-additions:1">21941730</site>	<item>
		<title>The Principle Is Not the Mechanism: ISP, DIP, and Language Structures in Practice</title>
		<link>https://rafael.bernard-araujo.com/the-principle-is-not-the-mechanism-isp-dip-and-language-structures-in-practice.php</link>
					<comments>https://rafael.bernard-araujo.com/the-principle-is-not-the-mechanism-isp-dip-and-language-structures-in-practice.php#respond</comments>
		
		<dc:creator><![CDATA[rafael]]></dc:creator>
		<pubDate>Wed, 09 Sep 2026 05:54:06 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[dependency-injection]]></category>
		<category><![CDATA[design principles]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[software engineering]]></category>
		<category><![CDATA[SOLID]]></category>
		<category><![CDATA[structural-typing]]></category>
		<category><![CDATA[typescript]]></category>
		<guid isPermaLink="false">https://rafael.bernard-araujo.com/?p=2420</guid>

					<description><![CDATA[A small refactor made me think: how many times do we mix a principle with mechanisms? For example, Interface Segregation and Dependency Inversion are many times seen naturally in OOP, but they are principles and they survive a change of paradigm. TypeScript is this interesting case: it supports both OOP and functional styles in the [&#8230;]]]></description>
										<content:encoded><![CDATA[<p><em>A small refactor made me think: how many times do we mix a principle with mechanisms? For example, Interface Segregation and Dependency Inversion are many times seen naturally in OOP, but they are principles and they survive a change of paradigm. TypeScript is this interesting case: it supports both OOP and functional styles in the same codebase, and both can preserve the I and D from SOLID. This article separates the principles from the mechanisms that carry them, maps how OOP and functional TypeScript preserve the same design responsibilities. And it proposes yet another comparison of things that look tied together, but are separate concepts in the language structures underneath: where structural typing and duck typing share a philosophy but diverge in practice.</em></p>
<h2>Contents</h2>
<ol>
<li>The in-memory repository that made the mapping visible</li>
<li>Preserve the responsibilities, change the mechanisms</li>
<li>Map responsibilities, not syntax</li>
<li>Service factories, closures, and composition</li>
<li>Structural typing: the conformance mechanism</li>
<li>Testing: where duck typing walks back in</li>
<li>Untangling the five concepts</li>
<li>The language field guide</li>
<li>A good way to use them</li>
<li>The honest counterpoint</li>
<li>Checklist</li>
<li>Recap</li>
</ol>
<p><em>Obs.: Check the Glossary at the end if you need clarification on concepts I am using in the article.</em></p>
<h2>The in-memory repository that made the mapping visible</h2>
<p>In a production TypeScript API I work on, the persistence design was still evolving while the Use Cases needed to move forward. We deliberately introduced an in-memory implementation of the domain contract:</p>
<pre><code class="language-typescript">// infrastructure/repository/in-memory.user.repository.ts
import { randomUUID } from &#039;node:crypto&#039;;
import type { NewUser, User, UserRepository } from &#039;../../domain/user&#039;;

const usersByDocument = new Map&lt;string, User&gt;();

async function findByDocument(document: string): Promise&lt;User | null&gt; {
  return usersByDocument.get(document) ?? null;
}

async function create(data: NewUser): Promise&lt;User&gt; {
  const user: User = { id: randomUUID(), ...data, createdAt: new Date() };
  usersByDocument.set(data.document, user);
  return user;
}

export const inMemoryUserRepository: UserRepository = { findByDocument, create };</code></pre>
<p>The services that depended on <code>UserRepository</code> received it as a parameter:</p>
<pre><code class="language-typescript">export async function register(
  input: RegisterInput,
  users: UserRepository,
): Promise&lt;User&gt; {
  const existing = await users.findByDocument(input.document);
  // ...
  return users.create(input);
}</code></pre>
<p>At the call site, the temporary seam was explicit:</p>
<pre><code class="language-typescript">await register(input, inMemoryUserRepository);</code></pre>
<p>That parameter was deliberate scaffolding. It allowed the Use Cases to progress against the contract while making the temporary in-memory choice visible wherever the service was called.</p>
<p>Later, the Postgres implementation of <code>UserRepository</code> became available. We began removing the extra repository parameter and moving toward an agreed convention: import the repository and use it inside the service. That convention itself would become part of the question.</p>
<pre><code class="language-ts">import { postgresUserRepository } from &#039;../infrastructure/repository/postgres.user.repository&#039;;

export async function register(input: RegisterInput): Promise&lt;User&gt; {
  const existing = await postgresUserRepository.findByDocument(input.document);
  // ...
  return postgresUserRepository.create(input);
}</code></pre>
<p>That was the direction of the refactor, not the conclusion of the design. It removed the temporary parameter and restored consistency with the surrounding code, but it also moved the implementation choice directly into the service and removed the visible substitution boundary. In PHP, I knew exactly where each responsibility lived. In TypeScript, the functional style I was working in didn't use those mechanisms.</p>
<p>In the OOP style, each responsibility had an obvious home: a small interface supported Interface Segregation; constructor injection supported Dependency Inversion; a Service Container selected implementations and assembled the graph; tests supplied another implementation of the same interface. The direct-import style looked different enough to raise the real question:</p>
<blockquote>
<p>Am I giving up Interface Segregation, Dependency Inversion, centralised composition, or isolated testing? Is functional TypeScript unsuitable for these principles, or am I looking for their OOP mechanisms instead of their functional equivalents? Should this service become a class, or can the functional style preserve the same design qualities from implementation through testing?</p>
</blockquote>
<p>The refactor did not reveal a missing principle. It revealed a comparison worth making: for every responsibility that had an obvious home in the OOP style, what technique (and what language structure) gave it an equivalent home in the functional style?</p>
<p>That comparison turned out to be a useful exercise in itself. Not just which pattern to use, but what is a principle, what is a mechanism, and what is a language structure. Where each of those pieces actually plays its role. The rest of this article works through that distinction, one responsibility at a time.</p>
<h2>Preserve the responsibilities, change the mechanisms</h2>
<p>The comparison becomes clearer when the creation of one service is separated from the composition of the whole application:</p>
<table>
<thead>
<tr>
<th>Design responsibility</th>
<th>Common OOP mechanism</th>
<th>Functional TypeScript mechanism</th>
</tr>
</thead>
<tbody>
<tr>
<td>Keep the consumer contract narrow</td>
<td>A small, consumer-specific interface</td>
<td>A small structural contract</td>
</tr>
<tr>
<td>Create one service instance</td>
<td>Invoke a class constructor directly or through a dedicated factory</td>
<td>Invoke a service factory such as <code>makeRegistrationService(...)</code></td>
</tr>
<tr>
<td>Retain injected dependencies</td>
<td>Private object fields</td>
<td>The returned functions' lexical closures</td>
</tr>
<tr>
<td>Compose the application graph</td>
<td>Invoke constructors/factories manually at the composition root, or use a Service Container</td>
<td>Invoke service factories manually at the composition root, or use a Service Container</td>
</tr>
<tr>
<td>Expose the wired application</td>
<td>The entry point receives or resolves a root service/application object</td>
<td>The composition function returns an application facade with already-wired operations</td>
</tr>
<tr>
<td>Test a consumer in isolation</td>
<td>Inject a fake/mock implementation through the constructor</td>
<td>Pass a structural or duck-typed double to the service factory</td>
</tr>
</tbody>
</table>
<p>This makes the factory equivalence explicit. <code>makeRegistrationService(users)</code> is doing two familiar jobs at once: its parameters are the functional counterpart of constructor injection, and invoking it is the functional counterpart of constructing the service through <code>new</code> or through a small OOP factory. The returned closure or object of functions is the service instance.</p>
<p>It also keeps <strong>composition</strong>, <strong>composition root</strong>, and <strong>Service Container</strong> distinct:</p>
<ul>
<li><strong>Composition</strong> is the activity of assembling the graph.</li>
<li>The <strong>composition root</strong> is the application location where that activity happens.</li>
<li>A <strong>Service Container</strong> is one optional mechanism for performing composition and resolution, often with lifecycle, scope, or discovery features. It can be used in either an OOP or functional application.</li>
<li>An explicit composition function is manual composition, not a complete replacement for every container capability.</li>
</ul>
<p>The composed result is another role again. The <code>application.register</code> operation on the returned <strong>application facade</strong> is a ready-to-use entry point into the wired graph. It is not a Service Container because callers are not resolving arbitrary services by token, and it is not managing service lifecycles. If application code asks a container for dependencies directly, that moves toward the Service Locator pattern rather than dependency injection.</p>
<p>None of these mechanisms creates a design principle by itself. A large interface still violates ISP. A constructor or factory typed to a concrete class still violates DIP. A container or composition function can centralise poor dependencies as efficiently as good ones. The value comes from assigning each responsibility deliberately.</p>
<p>Import/export syntax is not part of the comparison; both styles use ordinary module boundaries. The meaningful question is <strong>where the concrete implementation is selected</strong>. In the opening refactor, the Use Case selects <code>postgresUserRepository</code> itself and hard-wires the detail. In the mapped design, the composition root selects it and the service factory receives only the <code>UserRepository</code> contract.</p>
<p>The discovery wasn't any of the individual concepts, they were all familiar. It was the <strong>mapping between coding styles</strong>. How do OOP and functional TypeScript use different techniques and language structures to preserve Interface Segregation and Dependency Inversion, from implementation through isolated testing? Asking that comparative question put contracts, constructors, factories, closures, composition, containers, facades, and test doubles in their respective roles. The concepts stayed the same; the mechanisms changed.</p>
<p>That comparison opened a second layer: <strong>what structure in the language makes each technique possible?</strong> Class constructors and object fields create and retain dependencies in the OOP route. First-class functions and lexical closures do the equivalent work in the functional route, while ordinary function application composes the graph. TypeScript's structural type system checks contracts without nominal declarations. JavaScript's dynamic dispatch allows duck-typed doubles at runtime, while TypeScript can check the same doubles structurally before tests run. The organizing chain is therefore not only <em>principle → technique</em>, but <strong>design responsibility → technique → enabling language structure</strong>.</p>
<h2>Map responsibilities, not syntax</h2>
<p>Dependency Inversion and dependency injection are related, but they are not the same thing:</p>
<ul>
<li><strong>Dependency Inversion</strong> (the D in SOLID) is a principle about <strong>direction</strong>: high-level policy must not depend on low-level detail; both depend on abstractions.</li>
<li><strong>Dependency injection</strong> is a technique that serves it: bind a concrete dependency outside the code that consumes it.</li>
</ul>
<p>The OOP version stores an injected dependency in an object:</p>
<pre><code class="language-typescript">class RegistrationService {
  constructor(private readonly users: UserRepository) {}

  async register(input: RegisterInput) {
    const existing = await this.users.findByDocument(input.document);
    // ...
  }
}</code></pre>
<p>The functional version stores the same dependency in a closure:</p>
<pre><code class="language-typescript">function makeRegistrationService(users: UserRepository) {
  return {
    async register(input: RegisterInput) {
      const existing = await users.findByDocument(input.document);
      // ...
    },
  };
}</code></pre>
<p>Both consumers depend on <code>UserRepository</code>, not on a database implementation. Both receive the concrete dependency from somewhere else. The difference is where the language stores the provided value: an object field in one case, a lexical environment in the other.</p>
<p>Composition completes the mapping:</p>
<pre><code class="language-typescript">// composition/application.ts
import { postgresUserRepository } from &#039;../infrastructure/repository/postgres.user.repository&#039;;

function createRegistrationApplication(users: UserRepository) {
  const registration = makeRegistrationService(users);
  return { register: registration.register };
}

const application = createRegistrationApplication(postgresUserRepository);

await application.register(input);</code></pre>
<p>The composition root chooses the Postgres implementation, injects it once, and receives an application facade. Calling <code>application.register</code> does not choose, construct, or locate a repository. In a container-based application, a Service Container may perform this composition at the root. Here the composition function performs it manually. Neither the composition function nor the returned facade is itself a Service Container.</p>
<p>This is also where Interface Segregation fits. <code>UserRepository</code> should describe only what the registration Use Case needs. Structural typing does not guarantee ISP (nothing prevents us from declaring a fat contract), but it removes much of the implementation cost of keeping contracts small. Any value with the required shape can satisfy the consumer's contract without joining an inheritance hierarchy or declaring <code>implements</code>. (More details about <code>implements</code> down in the article.)</p>
<h2>Service factories, closures, and composition</h2>
<p>Three elements do different work here: the <strong>service factory creates</strong> a service, its parameters are the <strong>injection boundary</strong>, and the returned functions' <strong>closures retain</strong> the injected dependencies.</p>
<p>At the service level, calling <code>makeRegistrationService(users)</code> supplies the contract-typed dependency and creates the service. Every returned function then closes over <code>users</code>. This is the direct functional counterpart of a constructor receiving a value, assigning it to a private field, and producing an object instance, without exposing the dependency to every service call.</p>
<p>At the application level, a composition function can invoke several service factories and build the graph. The next example generalises the earlier registration-only case into a composition function for the full application graph:</p>
<pre><code class="language-typescript">interface ApplicationDependencies {
  users: UserRepository;
  mailer: Mailer;
  clock: Clock;
}

function createApplication(dependencies: ApplicationDependencies) {
  const registration = makeRegistrationService(dependencies.users);
  const profile = makeProfileService(dependencies.users);
  const reminders = makeReminderService(dependencies.mailer, dependencies.clock);

  return {
    register: registration.register,
    getProfile: profile.get,
    sendReminders: reminders.send,
  };
}</code></pre>
<p>Call <code>createApplication(...)</code> once at the composition root, and each returned service closes over only the slice of the graph it needs; collectively, those services expose the complete, already-wired application. Production passes database, mail, and system-clock implementations. Tests pass in-memory repositories, recording mailers, and fixed clocks. The consumers remain unchanged.</p>
<p>Closure-based composition can make a Service Container unnecessary for a simple, static graph, but it is not a full replacement for the abstraction. The composition function builds the graph and each returned closure retains its relevant part. Together they cover the container's core composition responsibility: construct the graph centrally, bind implementations to contracts, and expose ready-to-use services. A container may additionally provide autowiring, named bindings, scopes, lifecycle management, decorators, interception, or plugin discovery, and it remains available in functional code when those capabilities earn their cost. In TypeScript, autowiring still needs runtime anchors such as classes or explicit string/symbol tokens; erased structural interfaces cannot be resolved directly. Manual composition is sufficient when those capabilities are not needed.</p>
<p>The two routes can now be summarised without treating module syntax as part of the comparison:</p>
<ul>
<li><strong>OOP:</strong> segregated interface → constructor/factory → dependencies retained in object fields → manual or container-assisted composition → root application object</li>
<li><strong>Functional TypeScript:</strong> structural contract → service factory → dependencies retained in closures → manual or container-assisted composition → application facade</li>
</ul>
<p>The architecture is preserved because each responsibility still has an explicit home. What changes is the default mechanism: lexical scope replaces instance fields, service factories replace construction through <code>new</code>, structural conformance replaces nominal declarations, and explicit composition can replace container resolution when the graph does not need richer container capabilities.</p>
<p>Anton van Straaten's fictional Qc Na koan captures the first equivalence: <em>&quot;objects are a poor man's closures; closures are a poor man's objects.&quot;</em> Both bundle behaviour with retained state. Here that state includes the in-memory repository's <code>Map</code> and, in each service closure, precisely the dependencies that service needs.</p>
<h2>Structural typing: the conformance mechanism</h2>
<p>Service factories and closures answer <em>how one service is created and retains its injected dependencies</em>; the composition root answers <em>where the full graph is assembled</em>. Structural typing answers a simpler question: how does the compiler know a value satisfies the contract its consumer expects? It's what makes the functional mapping safe, not just short.</p>
<p>In a structurally typed system, a value satisfies a type by having the right <strong>shape</strong>: the required members, with compatible types. Names and declarations are irrelevant. <code>UserRepository</code> defines one such shape:</p>
<pre><code class="language-typescript">interface UserRepository {
  findByDocument(document: string): Promise&lt;User | null&gt;;
  create(data: NewUser): Promise&lt;User&gt;;
}</code></pre>
<p>Any value with those two methods, and compatible signatures, has that shape. So when the in-memory repository assigns its object literal to that type. Consider:</p>
<pre><code class="language-typescript">export const inMemoryUserRepository: UserRepository = { findByDocument, create };</code></pre>
<p>That single line <strong>is the conformance check.</strong> It's compile-time, exhaustive, and free: if the object is missing a method, has one with an incompatible signature, or the contract later grows, this line stops compiling. No <code>implements</code>, no registration, no adapter class. And it generalises: any object literal, class instance, or factory result that has the shape <em>is</em> a <code>UserRepository</code>. The contract is decoupled from every implementation's ancestry, which is why an in-memory object literal and a Postgres-backed implementation can be substituted at the composition root without changing a consumer.</p>
<p>You can watch the check evaporate the moment it's done its job. The in-memory repository didn't need <code>implements</code>. A Postgres-backed class can still use it for clarity, but it's optional, and it disappears at runtime. The TypeScript source:</p>
<pre><code class="language-typescript">class PostgresUserRepository implements UserRepository {
  async findByDocument(document: string) { /* ... */ }
  async create(data: NewUser) { /* ... */ }
}</code></pre>
<p>compiles to plain JavaScript with no trace of the contract:</p>
<pre><code class="language-javascript">class PostgresUserRepository {
  async findByDocument(document) { /* ... */ }
  async create(data) { /* ... */ }
}</code></pre>
<p>The <code>interface</code> and <code>implements</code> annotation disappear because they are type-system constructs. The class remains because a class is also a runtime JavaScript construct. Conformance was proven once, before the program ran, and then discarded. It's also why a TypeScript DI container can't autowire by interface the way C# or PHP can: at runtime there's no <code>UserRepository</code> to resolve against, only classes and string/symbol tokens. The same erasure that keeps contracts lightweight is the reason reflection-based containers cannot autowire by interface and must use runtime classes or explicit tokens.</p>
<p>This is also where the <strong>I</strong> of SOLID becomes cheaper to practise. A small contract requires little implementation ceremony (no base class, no <code>implements</code>, no registration), so keeping interfaces role-specific adds less friction. In a nominal OOP language, each new segregated interface adds a declaration that its implementations must adopt; structural typing removes that declaration tax. It does not guarantee Interface Segregation, but it makes the principle easier to apply consistently.</p>
<p>The contrast that makes this concrete is a nominal language, PHP, the reference OOP case here:</p>
<pre><code class="language-php">final class PostgresUserRepository implements UserRepository { /* ... */ }

function register(UserRepository $users): void { /* ... */ }</code></pre>
<p>Here conformance is <em>declared</em>. An object with exactly the right methods but no <code>implements UserRepository</code> is <strong>not</strong> a <code>UserRepository</code>: pass it to <code>register()</code> and PHP throws a <code>TypeError</code>. Same shape, wrong ancestry, rejected. This is one reason adapters and containers become more prominent in nominal ecosystems: conformance is explicitly declared, so wiring becomes a first-class activity with its own tooling.</p>
<p>One distinction worth stating now, because it drives the rest of the post: TypeScript is structural for interfaces, object types, and (mostly) classes, and it checks that conformance <strong>statically</strong>, before the program runs. &quot;Checked statically&quot; is easy to wave past, but it's the entire difference between structural typing and the thing it gets mistaken for. And the fastest place to feel that difference isn't in production at all. It's the moment you sit down to write a test.</p>
<p>And I am a Test-Driven Developer. Therefore...</p>
<h2>Testing: where duck typing walks back in</h2>
<p>The style survives contact with production easily enough. The seam is typed, the closure captures a contract-typed value. Where it gets interesting is the first time you write a test.</p>
<p>To test a service in isolation, you need a stand-in for its dependency, a repository that never touches a database. In JS/TS we've done this for years with an ad-hoc object: just the methods the test exercises, nothing more. And here the old instinct kicks in, reach for a loose double:</p>
<pre><code class="language-typescript">// duck-typed double: no contract in sight
const users = { findByDocument: async () =&gt; null } as any;</code></pre>
<p>This works. The test runs, the service calls <code>findByDocument</code>, the call resolves. That's <strong>duck typing</strong>: the object is accepted because, at the moment of the call, it happens to have the method. Nothing checked its shape ahead of time; the <code>as any</code> explicitly threw the check away.</p>
<p>And it raises the question this whole post circles: <strong>is that the same thing as structural typing?</strong> It feels identical, both say &quot;shape is enough, ancestry is irrelevant.&quot; But the difference is exactly the <em>when</em>. Type the same double against the contract instead:</p>
<pre><code class="language-typescript">// structurally-typed double: checked against the contract, now
const users: UserRepository = {
  findByDocument: async () =&gt; null,
  create: async (data) =&gt; ({ id: &#039;x&#039;, ...data, createdAt: new Date() }),
};</code></pre>
<p>Now the compiler enforces the whole shape <em>before the test runs</em>. Forget <code>create</code>, or let the real <code>UserRepository</code> grow a method, and this double stops compiling; the test tells you it drifted. The <code>as any</code> version compiles happily and only fails later, at runtime, if that path is even exercised.</p>
<p>Same philosophy, opposite failure mode. The loose double (<code>as any</code>) is widely used and genuinely convenient, and it's quietly the reason so many of us assume duck typing and structural typing are one thing. They aren't: typing the double <code>: UserRepository</code> is the small discipline that keeps a test double in the structural camp, and it's worth doing. But the recommendation isn't the point here. The point is that the moment you write a mock is the moment the two concepts visibly separate, which is exactly what the next section pulls apart.</p>
<h2>Untangling the five concepts</h2>
<p>Five ideas have been doing distinct work so far. Placing them side by side makes the boundaries explicit:</p>
<table>
<thead>
<tr>
<th>Concept</th>
<th>What it is</th>
<th>What it is not</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Dependency inversion / injection</strong></td>
<td>DIP: a principle about <em>direction</em>; depend on abstractions (the D in SOLID). DI: a <em>technique</em> serving it; bind concretions elsewhere</td>
<td>A framework, a container, or anything inherently class-shaped</td>
</tr>
<tr>
<td><strong>OOP constructs</strong></td>
<td>Classes, constructors, <code>implements</code>; <em>one mechanism</em> for DI and encapsulation</td>
<td>The definition of DI, or a requirement for Interface Segregation or Dependency Inversion</td>
</tr>
<tr>
<td><strong>Closures</strong></td>
<td>Functions capturing their environment; <em>another mechanism</em> for retained private state, injected dependencies, and explicit graph composition</td>
<td>A Service Container abstraction by definition, or a hack until &quot;real&quot; OOP arrives</td>
</tr>
<tr>
<td><strong>Structural typing</strong></td>
<td><strong>Static</strong> conformance decided by shape; checked before the program runs</td>
<td>A dynamic-language feature; permission to skip contracts</td>
</tr>
<tr>
<td><strong>Duck typing</strong></td>
<td><strong>Runtime</strong> conformance: &quot;if it quacks&quot;; discovered at the call site, or not at all</td>
<td>A synonym for structural typing</td>
</tr>
</tbody>
</table>
<p>Read down the &quot;what it is not&quot; column and the responsibilities separate cleanly. <strong>Dependency injection is not a framework</strong>: an explicit composition function can bind the graph as truly as a container. <strong>OOP constructs are not the definition of DI</strong>: they're one mechanism among several, the one that happens to dominate nominal ecosystems. <strong>Closures are not a hack or a container by definition</strong>: they are a language mechanism that can retain one dependency or the complete composed graph.</p>
<p>The two that cause the most trouble are the last ones: <strong>structural typing vs duck typing.</strong> They share a philosophy, behaviour over ancestry, shape over name, and in TypeScript they even look identical to write, which is why they get treated as one thing. They are not. The difference is <em>when the shape is checked</em>. Structural typing checks it at <strong>compile time</strong>, by a type checker, before the program runs; the failure is a red squiggle in your editor. Duck typing checks it at <strong>runtime</strong>, at the moment of the call, by the call itself; the failure is a thrown error, or worse, silence, if the missing method simply never gets reached on that path.</p>
<p>Which answers the question the testing section left open: if structural typing isn't what <em>makes duck typing possible</em>, what does? The enabler is <strong>dynamic dispatch</strong>, the language resolving <code>x.quack()</code> against whatever <code>x</code> actually is at call time, with no prior type commitment required. That's a property of the runtime, not of the type system, and it's independent of whether conformance is judged by shape or by declaration. The clean proof is Python: it has had duck typing forever, with no structural type system at all; <code>typing.Protocol</code> (PEP 544) later <em>added</em> static structural checking as an opt-in for external type checkers, and the duck-typed runtime underneath didn't change one bit. So duck typing and structural typing aren't two names for one idea, they're independent answers to two different questions, <em>how?</em> and <em>when?</em>, and a language can choose each answer separately.</p>
<h2>The language field guide</h2>
<p>Put the two questions on two axes, <strong>how</strong> conformance is decided (by shape, or by declaration) and <strong>when</strong> it's checked (statically at compile time, or at runtime), and every language lands in a cell:</p>
<table>
<thead>
<tr>
<th></th>
<th>Checked statically (compile time)</th>
<th>Checked at runtime</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>By shape</strong></td>
<td>TypeScript, Go (<em>structural typing</em>)</td>
<td>Python, JS, Ruby (<em>duck typing</em>)</td>
</tr>
<tr>
<td><strong>By declaration</strong></td>
<td>C#, Java (<em>nominal + static</em>)</td>
<td>PHP with type hints (<em>nominal + runtime</em>)</td>
</tr>
</tbody>
</table>
<p>The cells people forget are the interesting ones.</p>
<ul>
<li><strong>TypeScript</strong>: shape, static, and <em>loose</em>. An ad-hoc object literal conforms on the spot, no named type required. The cheapest contracts of any mainstream language. TypeScript provides static structural checking over a dynamically typed JavaScript runtime, so once the types are erased, what's left is duck typing.</li>
<li><strong>Go</strong>: shape, static, and <em>not loose</em>. Interfaces are satisfied implicitly (structural, a type never says <code>implements</code>), but conformance still needs methods declared on a named type; there are no ad-hoc conforming literals and no runtime &quot;try the call and see.&quot; Interface satisfaction operates through named interface types rather than TypeScript-style ad-hoc object compatibility. This is the answer to <strong>&quot;is there structural typing without any duck typing?&quot;</strong>. Go is it, and it isn't a compromise. The idiomatic Go move of defining tiny consumer-side interfaces is Interface Segregation and structural typing working together, straight out of the textbook.</li>
<li><strong>Python / JS / Ruby</strong>: shape, runtime. Classic duck typing, enabled by dynamic dispatch. Python then bolts on optional static structural typing via <code>typing.Protocol</code> (PEP 544) for anyone running a type checker, without touching the duck-typed runtime, the cleanest demonstration that the two are separable.</li>
<li><strong>PHP</strong>: declaration, runtime. Nominal, but enforced at call time. With type declarations, objects of classes that declare <code>implements</code> pass; a mismatch is a <code>TypeError</code> when the call happens, not a compile error (PHPStan/Psalm drag the check earlier, as a lint). Drop the declarations and PHP becomes dynamically typed: no type enforcement on parameters, method calls resolved at runtime, but objects are still class instances, not ad-hoc shapes. That double identity, nominal-when-typed, dynamic-when-not, plus autowiring by type declaration is the same story that made the containers from section 3 inevitable.</li>
<li><strong>C# / Java</strong>: declaration, static. Nominal, checked at compile time. The full ceremony (declare the interface, <code>implements</code> it, register it, inject it) and perfectly capable of clean dependency-inverted designs; the container exists to amortise the wiring cost the nominal type system creates in the first place.</li>
</ul>
<p>The grid shows these are <strong>independent axes</strong>, and real languages occupy all four cells. Go is structural and statically checked, but without TypeScript's ad-hoc object compatibility; TypeScript is structural-and-loose yet fully static; Python is duck-typed with an optional structural upgrade; PHP is nominal without being static, the quadrant people forget exists. &quot;Structural&quot; and &quot;duck&quot; are not synonyms, and &quot;nominal&quot; and &quot;static&quot; are not the same axis. Once the grid is visible, the testing question becomes precise: a TypeScript double can rely on JavaScript's runtime duck typing, or it can be checked structurally before the test runs. The object may look the same; the guarantee is different.</p>
<h2>A good way to use them</h2>
<p>None of this argues against classes, and none of it argues against containers. It argues for choosing the mechanism on purpose. A few defaults fall out of everything above:</p>
<ul>
<li><strong>Keep contracts in the domain, named in the domain's language.</strong> <code>UserRepository</code> belongs in <code>domain/user.ts</code>, not in the infrastructure that implements it; the consumer owns the interface, the implementation conforms to it. This is the Go consumer-defined-interface instinct and Interface Segregation in one move.</li>
<li>For simple functional services, a factory + closure is a natural default. The dependency is captured once and stays private. Keep <strong>parameter passing</strong> for genuinely local composition or deliberate scaffolding. Put the complete graph in an explicit <strong>composition root</strong> (<code>createApplication</code> or equivalent), invoke it once with production implementations, and retain the returned application facade as the wired root object. That is where implementation choices remain centralised and tests gain a single, typed substitution boundary.</li>
<li><strong>Treat class-vs-closure as a mechanism choice, not a moral one.</strong> Pick by team convention and by whether you genuinely need identity or lifecycle semantics, not because one of them is &quot;real&quot; dependency injection. They both are.</li>
</ul>
<p>It's worth naming the design principle underneath, because it's the reason the cheap option is also the good one. In Vlad Khononov's Balanced Coupling model, a shared contract is the <strong>weakest, most distance-tolerant</strong> form of coupling between two components, exactly what you want across a module boundary, where the two sides may change and ship independently. Structural typing makes that weakest level of coupling nearly free to express, so in TypeScript the best-balanced design and the least-ceremony design are the same design. The one discipline the model insists on: keep the wiring <strong>explicit and greppable</strong>, a named seam you can find, because Balanced Coupling's warning is against <em>implicit</em> coupling, and implicit coupling is also structural typing's failure mode. Which is the honest counterpoint.</p>
<h2>The honest counterpoint</h2>
<p>The price of making conformance free is that conformance becomes <em>implicit</em>. Anything with the right shape satisfies a contract, including things that match only by accident. A single-method contract like <code>{ execute(): void }</code> is satisfied by half the objects in a codebase; the type checker will happily accept a wrong one that merely fits. The narrower and more generic the contract, the more coincidental matches it invites, so the defence is to keep contracts <strong>role-named and non-trivial</strong> (<code>UserRepository</code>, not <code>Doer</code>), so that matching the shape actually means matching the intent.</p>
<p>Implicitness costs tooling, too. Without <code>implements</code>, &quot;find all implementations of this interface&quot; is a weaker query than in a nominal language, and renaming a contract member doesn't announce, at the declaration site, everyone who just broke. You lean harder on the compiler and on find-references, and a little less on the class hierarchy telling you who's involved.</p>
<p>The mitigation turns out to be the same thing as the recommendation from the previous section: <strong>an explicit, greppable composition root.</strong> A deliberate conformance point, <code>const impl: Contract = ...</code>, typed against the contract, living in that known location, turns implicit shape-matching back into a visible, findable decision. It's also, not coincidentally, the same discipline that keeps a <em>test double</em> structural rather than duck-typed. The functional style doesn't remove the need to be deliberate about coupling; it relocates the deliberateness from the type declaration to the wiring, and rewards you for keeping that wiring in the open.</p>
<h2>Checklist</h2>
<p>A quick test for &quot;am I doing DI properly in this style?&quot;, whether or not you reached for a class:</p>
<ul>
<li>Does the consumer depend on a <strong>contract</strong>, not a concrete implementation? (Take the contract as a factory parameter and capture it.)</li>
<li>Is the <strong>binding external</strong> to the consumer, captured by a service factory and selected at the composition root, rather than constructed inline?</li>
<li>Is every implementation, <strong>including test doubles</strong>, typed against the contract (<code>: UserRepository</code>), so drift fails at compile time instead of at runtime?</li>
<li>Are contracts <strong>small and role-named</strong>, so structural matching signals intent rather than coincidence?</li>
<li>Is the complete dependency graph <strong>explicit and greppable</strong> in one composition root, rather than having consumers select concrete implementations throughout the application?</li>
<li>Did you pick class-vs-closure for a <strong>real reason</strong> (convention, lifecycle, identity), not out of habit or a belief that one is &quot;real&quot; DI?</li>
</ul>
<hr />
<h2>Recap</h2>
<p>The article started with a refactor that removed a temporary parameter and raised a question: was I giving up Interface Segregation and Dependency Inversion by moving from OOP to functional TypeScript, or was I looking for OOP mechanisms where functional equivalents existed?</p>
<p>The answer turned out to be a comparison worth making. For every responsibility that had an obvious home in OOP, there was a functional technique that gave it an equivalent home:</p>
<ul>
<li><strong>Interface Segregation</strong> lives in small, role-named contracts. Structural typing removes the declaration tax that keeps contracts fat in nominal ecosystems.</li>
<li><strong>Dependency Inversion</strong> lives in depending on abstractions, not on concretions. A factory parameter captures a contract-typed dependency as cleanly as a constructor.</li>
<li><strong>Dependency Injection</strong> lives in binding concretions outside the consumer. A service factory and closure retain injected state as truly as an object field.</li>
<li><strong>Composition</strong> lives in a single root where implementations are selected and the graph is assembled. A composition function does this as truly as a Service Container.</li>
<li><strong>Conformance</strong> lives in shape, not ancestry. Structural typing checks it statically, before the program runs. Duck typing discovers it at runtime, at the call site. Same philosophy, different <em>when</em>.</li>
<li>The language field guide put that distinction on two independent axes:
<ul>
<li><strong>How</strong> conformance is decided: by shape or by declaration.</li>
<li><strong>When</strong> it's checked: statically at compile time, or at runtime.</li>
</ul>
</li>
<li>Real languages occupy all four cells:
<ul>
<li><strong>Go</strong> is structural and statically checked, but without ad-hoc object compatibility.</li>
<li><strong>TypeScript</strong> is structural and loose, yet fully static.</li>
<li><strong>Python</strong> is duck-typed with an optional structural upgrade.</li>
<li><strong>PHP</strong> is nominal without being static.</li>
</ul>
</li>
<li>&quot;Structural&quot; and &quot;duck&quot; are not synonyms, and &quot;nominal&quot; and &quot;static&quot; are not the same axis.</li>
<li>The honest counterpoint: making conformance free makes it implicit. Anything with the right shape satisfies a contract, including things that match only by accident. The defence is role-named, non-trivial contracts and an explicit, greppable composition root. A deliberate conformance point, typed against the contract, turns implicit shape-matching back into a visible, findable decision.</li>
<li>The principles stayed the same. The mechanisms changed:
<ul>
<li><strong>Classes, constructors, and</strong> <code>implements</code> are one set of mechanisms.</li>
<li><strong>Closures, factories, and structural contracts</strong> are another.</li>
</ul>
</li>
<li>Neither set is the definition of the principles they carry. Picking between them is a mechanism choice, not a moral one.</li>
<li>The one discipline that travels across both styles: <strong>keep the wiring explicit and greppable</strong>. A named seam you can find is the difference between deliberate coupling and accidental coupling, whether the seam is a constructor parameter or a factory argument.</li>
</ul>
<hr />
<h2>Glossary</h2>
<ul>
<li><strong>OOP — Object-Oriented Programming:</strong> a programming style that organises state and behaviour around objects. For comparison, this article uses a common class-based arrangement: interfaces, constructors, implementations, and a Service Container.</li>
<li><strong>SOLID:</strong> a family of five design principles associated with maintainable and adaptable software. This article focuses only on its <strong>I</strong> and <strong>D</strong>.</li>
<li><strong>ISP — Interface Segregation Principle:</strong> consumers should not be forced to depend on operations they do not use; prefer small, role-specific contracts.</li>
<li><strong>DIP — Dependency Inversion Principle:</strong> high-level policy should not depend directly on low-level details; both should depend on abstractions, and details should depend on those abstractions.</li>
<li><strong>DI — Dependency Injection:</strong> a technique for supplying dependencies from outside the consumer. DI can support DIP, but the two are not synonyms.</li>
<li><strong>Closure:</strong> a function together with the lexical environment it retains, allowing injected values to remain available after the factory that received them has returned.</li>
<li><strong>Service Container:</strong> a central assembler or registry that selects implementations and wires services, often with additional lifecycle or resolution features.</li>
<li><strong>Composition root:</strong> the single application location where concrete implementations are selected and the dependency graph is assembled.</li>
<li><strong>Seam:</strong> a place in code where behaviour can be varied without editing the code that uses it. Coined by Michael Feathers in <em>Working Effectively with Legacy Code</em>. In this article, the repository parameter is a seam: you swap implementations at the call site without touching the service.</li>
</ul>
<h2>Further reading and references</h2>
<h3>Glossary concepts</h3>
<ul>
<li><a href="https://developer.mozilla.org/en-US/docs/Glossary/OOP">OOP - MDN glossary</a> — concise introduction to object-oriented programming and JavaScript's prototype-based model.</li>
<li><a href="https://en.wikipedia.org/wiki/SOLID">SOLID - Wikipedia</a> — the five-principle family; this article focuses on ISP and DIP.</li>
<li><a href="https://objectmentor.com/resources/articles/isp.pdf">The Interface Segregation Principle - Robert C. Martin</a> (PDF) — the original detailed treatment of cohesive, client-specific interfaces.</li>
<li><a href="https://objectmentor.com/resources/articles/dip.pdf">The Dependency Inversion Principle - Robert C. Martin</a> (PDF) — the original statement that policy and detail should depend on abstractions.</li>
<li><a href="https://www.martinfowler.com/articles/injection.html">Inversion of Control Containers and the Dependency Injection Pattern — Martin Fowler</a> — dependency injection, container wiring, and the separation of configuration from use.</li>
<li><a href="https://blog.ploeh.dk/2011/07/28/CompositionRoot/">Composition Root - Mark Seemann</a> — the application location where modules and concrete dependencies are assembled.</li>
<li><a href="https://www.informit.com/store/working-effectively-with-legacy-code-9780131177055">Working Effectively with Legacy Code - Michael Feathers</a> — the origin of the &quot;seam&quot; concept: a place in code where behaviour can be varied without editing the code that uses it.</li>
</ul>
<h3>Language and design mechanics</h3>
<ul>
<li><a href="https://www.typescriptlang.org/docs/handbook/type-compatibility">TypeScript Handbook - Type Compatibility</a> — TypeScript's structural type system.</li>
<li><a href="https://go.dev/ref/spec">The Go Programming Language Specification - Interface types</a> — implicit, statically checked interface satisfaction.</li>
<li><a href="https://peps.python.org/pep-0544/">PEP 544 - Protocols: Structural Subtyping</a> — Python's opt-in static structural typing.</li>
<li><a href="https://www.php.net/manual/en/language.types.type-system.php">PHP Manual - Type System</a> — PHP's nominal type system and runtime verification.</li>
<li><a href="https://coupling.dev/">Balanced Coupling - Vlad Khononov</a> — integration strength, distance, volatility, and contract coupling.</li>
<li><a href="https://people.csail.mit.edu/gregs/ll1-discuss-archive-html/msg03277.html">The Qc Na closures/objects koan - Anton van Straaten</a> — the equivalence between retained state in closures and objects.</li>
<li><a href="https://www.infoq.com/presentations/Simple-Made-Easy/">Simple Made Easy - Rich Hickey</a> — separating simplicity from familiarity and convenience.</li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://rafael.bernard-araujo.com/the-principle-is-not-the-mechanism-isp-dip-and-language-structures-in-practice.php/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2420</post-id>	</item>
		<item>
		<title>Permissions and Authorisation: Implementing with Cedar</title>
		<link>https://rafael.bernard-araujo.com/permissions-and-authorisation-implementing-with-cedar.php</link>
					<comments>https://rafael.bernard-araujo.com/permissions-and-authorisation-implementing-with-cedar.php#respond</comments>
		
		<dc:creator><![CDATA[rafael]]></dc:creator>
		<pubDate>Thu, 27 Aug 2026 01:35:15 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[Segurança]]></category>
		<guid isPermaLink="false">https://rafael.bernard-araujo.com/?p=2414</guid>

					<description><![CDATA[Disclaimer: This is the point-in-time version of my &#34;Playbook - Permissions and Authorisation: Cedar Implementation&#34;, which I will keep up-to-date. This article is the sequel to &#34;Permissions and Authorisation: A Practical Playbook&#34;, which covers the domain model, enforcement boundaries, and mechanism selection. Read that first. This article assumes the model is built and a policy [&#8230;]]]></description>
										<content:encoded><![CDATA[<blockquote>
<p><strong>Disclaimer:</strong> This is the point-in-time version of my &quot;<a href="https://rafael.bernard-araujo.com/playbooks/playbook-permissions-and-authorisation-cedar-implementation">Playbook - Permissions and Authorisation: Cedar Implementation</a>&quot;, which I will keep up-to-date. This article is the sequel to &quot;<a href="https://rafael.bernard-araujo.com/permissions-and-authorisation-a-practical-playbook.php">Permissions and Authorisation: A Practical Playbook</a>&quot;, which covers the domain model, enforcement boundaries, and mechanism selection. Read that first. This article assumes the model is built and a policy engine is warranted.</p>
</blockquote>
<p>The previous article ended with a guidance: introduce a policy engine when the model combines roles with ownership, relationships, tenant containment, and attributes; when the same decision must be made in several services; or when policy review, simulation, traceability, or controlled change is a real requirement. Cedar is one concrete option when that point is reached. This article walks through how to apply the playbook's model using Cedar specifically and where Cedar stops being the answer.</p>
<h2>What Cedar is</h2>
<p><a href="https://docs.cedarpolicy.com/">Cedar</a> is a policy language and decision engine <a href="https://aws.amazon.com/blogs/security/how-we-designed-cedar-to-be-intuitive-to-use-fast-and-safe/">developed by AWS</a> and released open-source under Apache 2.0. It evaluates an authorisation request with four parts, being <strong>principal, action, resource, context</strong> (called PARC), plus a set of entities representing trusted domain facts and their relationships. It returns <code>Allow</code> or <code>Deny</code> and decision diagnostics.</p>
<p>Cedar's evaluation semantics provide three useful safety properties:</p>
<ol>
<li><strong>Default deny.</strong> No matching <code>permit</code> means <code>Deny</code>. There is no implicit allow.</li>
<li><strong>Forbid wins.</strong> A matching <code>forbid</code> overrides any matching <code>permit</code>. Guardrails are expressible as first-class policy, not afterthoughts.</li>
<li><strong>Errors skip the affected policy.</strong> A policy evaluation error causes that policy to be skipped and reported in diagnostics rather than automatically determining the entire decision. The request continues to be evaluated against remaining policies.</li>
</ol>
<p>The third rule makes validation and diagnostics operationally important. A faulty policy does not itself cause a global outage, but it can be skipped while another <code>permit</code> still allows a request that the faulty policy was meant to restrict. Validate policies against the schema before publication, alert on evaluation diagnostics, and test both policy and schema changes.</p>
<p>A Cedar design normally contains:</p>
<ul>
<li>a <strong>schema</strong> for entity types, attributes, memberships, actions, and context shapes;</li>
<li><strong>entities</strong> representing trusted domain facts and relationships;</li>
<li><code>permit</code> and <code>forbid</code> <strong>policies</strong>;</li>
<li>a policy store and change process;</li>
<li>an application adapter that constructs the PARC request and relevant entity slice, invokes Cedar, records the decision, and enforces it.</li>
</ul>
<h2>What Cedar does not solve</h2>
<p>Cedar is a decision engine, not a complete authorisation system. This is pivotal to understand what is not Cedar's responsibility. It does not:</p>
<ul>
<li>authenticate users or issue/validate JWTs;</li>
<li>decide which entity data is true: your application must assemble trusted, correctly scoped entity data;</li>
<li>filter a database query automatically or protect a storage bucket on its own;</li>
<li>replace validation of input, tenancy checks, audit retention, rate limits, or business invariants;</li>
<li>provide a complete UI, approval workflow, or governance process for users editing policies.</li>
</ul>
<p>If policy authors are customers or non-engineering staff, design policy administration as a product: constrained templates, preview/simulation, approval and rollback, audit history, and strict meta-permissions for who may grant which access.</p>
<h2>When Cedar is a good fit</h2>
<p>Cedar is particularly strong when a system needs combinations of <a href="https://en.wikipedia.org/wiki/Role-based_access_control">RBAC</a>, ownership/relationship rules, tenant containment, and attributes; when decisions are shared across services; or when policy review and explainability matter.</p>
<p><strong>Use groups to represent broad roles and policies to grant role capabilities. Add resource relationships or attributes for rules that roles cannot express cleanly. Keep individual grants and exceptions narrow, scoped to both principal and resource where possible. Prefer fine-grained permissions in the model, then aggregate them into user-facing product roles or screens.</strong></p>
<p>Cedar is not the only policy engine, and it is not always the right one. If your rules are few, stable, and local to one service, centrally tested application code is simpler and clearer. If you need a full relationship-based access control system with deep graph traversal, consider whether Cedar's entity model is expressive enough for your relationship depth or whether a dedicated ReBAC system is more appropriate. Choose based on the behaviour the system needs, not the novelty of the tool.</p>
<h2>Implementing Cedar: a step-by-step approach</h2>
<h3>1. Start with one high-value boundary</h3>
<p>Choose one coherent domain, such as organisation membership, documents, or projects, not the entire legacy permission surface. Keep existing authorisation in place elsewhere. Define the success measure: for example, all document operations are evaluated at one server-side boundary and the decision can be explained from logs.</p>
<p>This mirrors the playbook's general guidance: centralise decisions early enough that the model can evolve deliberately, rather than attempting a big-bang migration of every permission surface at once.</p>
<h3>2. Define the Cedar schema and action vocabulary first</h3>
<p>Define the namespace, entity types, action groups, and action applicability before drafting many policies. Ensure each action identifies the permitted principal and resource types and the specific transient context shape it accepts.</p>
<pre><code class="language-cedar">namespace ExampleApp {
  entity User in [Role, Organisation] {};
  entity Role {};
  entity Organisation {};
  entity Document in [Organisation] = { owner: User, locked: Bool };

  action &quot;document.read&quot; appliesTo {
    principal: [User],
    resource: [Document],
    context: {}
  };

  action &quot;document.update&quot; appliesTo {
    principal: [User],
    resource: [Document],
    context: {}
  };
}</code></pre>
<p>This is illustrative only. The schema must reflect the actual domain and its authoritative data sources. The Cedar schema defines what an authorisation request and its entities are allowed to look like; it does not establish where those facts come from or whether they are trustworthy. That is the responsibility of the adapter and the data contract described in step 5. The action vocabulary is where the playbook's guidance on business actions applies directly: use <code>document.read</code>, <code>document.update</code>, <code>invoice.approve</code> — not HTTP verbs or UI labels. Make the resource explicit even for create and list operations, where the resource is normally the destination container or tenant.</p>
<h3>3. Model policy rules as business rules</h3>
<p>Policies should be specific and readable. A role grant and an owner rule may coexist; a <code>forbid</code> can express a guardrail that must win even if a role policy permits access.</p>
<pre><code class="language-cedar">// Role-based: document readers can read any document in their scope.
// Assumes the user&#039;s role membership is represented in the entity graph.
permit (
  principal in ExampleApp::Role::&quot;document_reader&quot;,
  action == ExampleApp::Action::&quot;document.read&quot;,
  resource
);

// Ownership-based: owners can update their own unlocked documents
permit (
  principal,
  action == ExampleApp::Action::&quot;document.update&quot;,
  resource
)
when { resource.owner == principal &amp;&amp; !resource.locked };

// Guardrail: locked documents cannot be updated, even by the owner
forbid (
  principal,
  action == ExampleApp::Action::&quot;document.update&quot;,
  resource
)
when { resource.locked };</code></pre>
<p>Do not paste this example into production unchanged. In particular, scope policies as tightly as the domain requires and validate all policy text against the deployed schema.</p>
<p>The <code>forbid</code>-wins semantics are what make Cedar's guardrails trustworthy. A locked-document rule expressed as <code>forbid</code> will override any <code>permit</code> including future permits you haven't written yet. This is qualitatively different from expressing the same constraint as a negative condition inside every <code>permit</code>, which silently stops applying when a new <code>permit</code> forgets to include it.</p>
<h3>4. Create one authorisation adapter</h3>
<p>Application code should call a small, well-tested boundary with a request such as:</p>
<pre><code>authorize({ principal, action, resource, context }) → allow/deny + decision metadata</code></pre>
<p>The adapter:</p>
<ul>
<li>resolves canonical IDs (opaque, immutable — not display names or email addresses);</li>
<li>loads the minimum authoritative entity slice (principal, resource, and the relationships the policies might traverse);</li>
<li>calls the Cedar authorizer;</li>
<li>handles diagnostics according to the service's safety requirements;</li>
<li>emits safe audit telemetry (request/correlation ID, principal/resource type and opaque IDs, action, decision, applicable rule identifiers and version, evaluation errors, latency);</li>
<li>blocks execution on denial.</li>
</ul>
<p>The adapter should not accept arbitrary Cedar entities or policy text from an untrusted caller. Entity data comes from trusted application sources; policy text comes from a versioned, reviewed policy store. Never from the request.</p>
<p>This is the same enforcement boundary the playbook describes, now with a concrete implementation: the adapter is where route additions cannot silently bypass the decision, because every protected operation passes through it.</p>
<h3>5. Assemble the entity slice carefully</h3>
<p>Cedar evaluates policies against the entities you provide, not against your database. The adapter must load the minimum authoritative entity slice: the principal, the resource, and the relationships the policies might traverse (group memberships, tenant containment, ownership links).</p>
<p>This is where the playbook's data contract guidance becomes concrete. For each entity attribute and relationship:</p>
<ul>
<li>What is the canonical source?</li>
<li>What is the freshness expectation?</li>
<li>Who owns the data?</li>
</ul>
<p>Keep principal, action, and resource facts in their respective models. Use request context only for transient, action-specific facts such as an MFA assertion, request time, or an IP/network signal not as a duplicate source for identity, role, tenant, or resource ownership. If the entity slice is wrong, the decision is wrong, regardless of how correct the policies are.</p>
<p>For “<em>may Alice read Document X?</em>”, the adapter might supply:</p>
<table>
<thead>
<tr>
<th>Fact</th>
<th>Cedar input</th>
<th>Authoritative source</th>
</tr>
</thead>
<tbody>
<tr>
<td>Who is making the request?</td>
<td><code>User::&quot;alice&quot;</code>, including role and tenant</td>
<td>Identity and membership service</td>
</tr>
<tr>
<td>What is being accessed?</td>
<td><code>Document::&quot;x&quot;</code>, including owner, classification, and locked state</td>
<td>Document service/database</td>
</tr>
<tr>
<td>How are they related?</td>
<td>Alice's group memberships; the document's folder and tenant</td>
<td>Membership and document data</td>
</tr>
<tr>
<td>What is true only for this request?</td>
<td>Verified MFA assertion, request time, client network</td>
<td>Validated session/token and request infrastructure</td>
</tr>
</tbody>
</table>
<p>The policies determine which of those facts are needed. A rule that permits <code>Group::&quot;editors&quot;</code> needs Alice's group membership; a rule that permits only the document owner needs its current owner; a <code>forbid</code> for locked documents needs the current locked state. The adapter assembles and validates this input; the client does not.</p>
<p>Why &quot;carefully&quot;: three failure modes, all silent.</p>
<ol>
<li><strong>Missing relationships.</strong> A policy says <code>principal in Group::&quot;editors&quot;</code>. If you don't load Alice's group memberships, Cedar can't match the policy. It evaluates as if Alice is in no groups. The decision is <code>Deny</code> even though Alice is an editor. The policy is correct; the data slice is wrong.</li>
<li><strong>Stale data.</strong> Alice was removed from the <code>editors</code> group 5 minutes ago, but your entity slice was cached from 10 minutes ago. Cedar allows a request that should be denied. The freshness question above is about this.</li>
<li><strong>Wrong source.</strong> You put Alice's role in the request context (which the caller controls) instead of loading it from your identity store. A malicious or buggy caller passes <code>role: admin</code> in the context and Cedar grants admin access. The guidance above is warning against exactly this: context is for transient facts (MFA, IP, time), not for identity, role, or ownership, because those must come from trusted sources.</li>
</ol>
<p>The &quot;minimum&quot; part: you don't load your entire user database into every authorisation call. You load the slice that the policies might traverse for this request. Too little → wrong decisions. Too much → latency and unnecessary data exposure. The &quot;carefully&quot; is about getting that boundary right.</p>
<h3>6. Handle diagnostics as a first-class concern</h3>
<p>Because evaluation errors are skipped rather than fatal, diagnostics are not optional telemetry, but safety signal. A policy that errors is a policy that is not applying. If that policy was a <code>forbid</code> guardrail, the request may be allowed by a different <code>permit</code> that the <code>forbid</code> was meant to override.</p>
<p>Handle diagnostics according to the service's safety requirements:</p>
<ul>
<li>Alert on evaluation errors at a rate that indicates a policy or entity-slice problem, not a transient blip.</li>
<li>Treat a diagnostics-only decision (no matching <code>permit</code> or <code>forbid</code>, but errors present) as <code>Deny</code> unless the service has a deliberate, documented reason to do otherwise.</li>
<li>Include diagnostic information in audit records so post-incident investigation can reconstruct why a decision was made.</li>
</ul>
<h2>Cedar-specific verification</h2>
<p>In addition to the general test strategy from the playbook, Cedar introduces its own verification surface:</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Verify</th>
</tr>
</thead>
<tbody>
<tr>
<td>Schema validation</td>
<td>Schema compiles, entity types and actions are consistent, context shapes match action definitions</td>
</tr>
<tr>
<td>Policy unit tests</td>
<td>Each <code>permit</code> and <code>forbid</code> produces the expected decision for representative entity slices</td>
</tr>
<tr>
<td>Entity-slice mapping tests</td>
<td>The adapter loads the correct entities and relationships for a given request. No missing relationships, no stale data</td>
</tr>
<tr>
<td>Diagnostics-handling tests</td>
<td>Evaluation errors are surfaced, alerted, and do not silently allow access</td>
</tr>
<tr>
<td>Schema-change tests</td>
<td>Adding, removing, or changing entity attributes or actions does not silently alter existing decisions</td>
</tr>
</tbody>
</table>
<p>For every new policy, include at least one <em>must allow</em>, <em>must deny</em>, and <em>must not leak</em> scenario. Test actions, not UI controls, because direct API calls and background execution bypass the UI. This is the same discipline as the general playbook, now applied to Cedar's <code>permit</code>/<code>forbid</code> semantics.</p>
<h2>Operational checklist for Cedar</h2>
<h3>Before implementation</h3>
<ul>
<li>One high-value boundary chosen, with existing authorisation preserved elsewhere.</li>
<li>Cedar schema reflects the actual domain: entity types, attributes, memberships, actions, and context shapes.</li>
<li>Action vocabulary uses business actions, not HTTP verbs.</li>
<li>Entity data sources identified: canonical source, freshness, and ownership for each attribute and relationship.</li>
<li>Adapter design defined: ID resolution, entity-slice loading, Cedar invocation, diagnostics handling, audit telemetry, and enforcement.</li>
</ul>
<h3>Before rollout</h3>
<ul>
<li>Policies validate against the schema in CI.</li>
<li>Policy changes have review, audit, and rollback paths.</li>
<li>The adapter is the sole entry point for Cedar evaluation — no bypass paths.</li>
<li>Entity-slice loading is tested for correctness and minimal scope.</li>
<li>Diagnostics are alerted on, not just logged.</li>
<li>Existing data/query/storage controls still enforce tenant and ownership boundaries.</li>
<li>Negative and regression tests prove denial causes neither disclosure nor side effect.</li>
</ul>
<h3>After rollout</h3>
<ul>
<li>Inspect allow/deny rates, diagnostics, latency, and unexpected denials.</li>
<li>Confirm no route or job bypasses the adapter.</li>
<li>Review <code>forbid</code> guardrails, stale entity data, and policy-store changes on a defined cadence.</li>
<li>Record incidents and confusing access requests as new regression cases or schema improvements.</li>
</ul>
<h2>Cedar-specific anti-patterns</h2>
<p>In addition to the general anti-patterns from the playbook:</p>
<ul>
<li><strong>Trusting unvalidated entity data:</strong> passing request-supplied attributes or relationships into Cedar without resolving them from a trusted source. Cedar evaluates what you give it — incorrect entity data can produce incorrect authorisation decisions.</li>
<li><strong>Forgetting</strong> <code>forbid</code> <strong>when adding new</strong> <code>permit</code><strong>s:</strong> a new <code>permit</code> that broadens access may override a constraint that was previously enforced by the absence of a permit, not by an explicit <code>forbid</code>. Express guardrails as <code>forbid</code> so they survive new permits.</li>
<li><strong>Ignoring diagnostics:</strong> treating evaluation errors as harmless noise. A skipped <code>forbid</code> is a silent security gap.</li>
<li><strong>Overloading context with entity facts:</strong> using the context map to pass principal roles, tenant membership, or resource ownership instead of modelling them as entities and relationships. Context is for transient, action-specific facts.</li>
<li><strong>Policy-store drift:</strong> changing policies without schema validation, version pinning, or regression tests. A policy that validated against the previous schema may error against the new one.</li>
<li><strong>Entity-slice gaps:</strong> loading the principal and resource but forgetting the relationships the policies traverse (group memberships, tenant containment). A policy that depends on <code>principal in Role::&quot;admin&quot;</code> will deny if the Role entity is not in the slice — silently and without a clear error.</li>
</ul>
<h2>References</h2>
<ul>
<li><a href="https://rafael.bernard-araujo.com/permissions-and-authorisation-a-practical-playbook">Permissions and Authorisation: A Practical Playbook</a> — the preceding article</li>
<li><a href="https://cedarpolicy.com/">Cedar Policy Language</a></li>
<li><a href="https://docs.cedarpolicy.com/auth/authorization.html">How Cedar authorization works</a></li>
<li><a href="https://docs.cedarpolicy.com/schema/schema.html">Cedar schema overview</a></li>
<li><a href="https://docs.cedarpolicy.com/bestpractices/bp-overview.html">Cedar best practices</a></li>
<li><a href="https://docs.cedarpolicy.com/bestpractices/bp-authorization-patterns.html">Cedar authorization patterns</a></li>
<li><a href="https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html">OWASP Authorization Cheat Sheet</a></li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://rafael.bernard-araujo.com/permissions-and-authorisation-implementing-with-cedar.php/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2414</post-id>	</item>
		<item>
		<title>Permissions and Authorisation: A Practical Playbook</title>
		<link>https://rafael.bernard-araujo.com/permissions-and-authorisation-a-practical-playbook.php</link>
					<comments>https://rafael.bernard-araujo.com/permissions-and-authorisation-a-practical-playbook.php#comments</comments>
		
		<dc:creator><![CDATA[rafael]]></dc:creator>
		<pubDate>Thu, 06 Aug 2026 04:13:20 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[Technology]]></category>
		<guid isPermaLink="false">https://rafael.bernard-araujo.com/?p=2395</guid>

					<description><![CDATA[Disclaimer: This is the point-in-time version of my &#34;Playbook - Permission and Authorisation&#34;, which I will keep up-to-date. Authorisation is a domain capability, not a collection of scattered endpoint conditionals. For every protected operation, decide consistently whether a verified principal may perform a business action on a specific resource in a defined context—and enforce that [&#8230;]]]></description>
										<content:encoded><![CDATA[<blockquote>
<p><strong>Disclaimer:</strong> This is the point-in-time version of my &quot;<a href="https://rafael.bernard-araujo.com/playbooks/playbook-permissions-and-authorisation">Playbook - Permission and Authorisation</a>&quot;, which I will keep up-to-date.</p>
</blockquote>
<p>Authorisation is a domain capability, not a collection of scattered endpoint conditionals. For every protected operation, decide consistently whether a verified principal may perform a business action on a specific resource in a defined context—and enforce that decision server-side before data or side effects are exposed.</p>
<p>Choose the simplest mechanism that preserves one consistent, server-side decision model. A few stable rules may be clearer as centrally tested application code; a policy engine becomes valuable when rules are expressive, auditable, shared across services, or need to evolve independently of individual endpoints.</p>
<p>To definitions:</p>
<p><strong>Permission</strong> is a grant or entitlement. </p>
<p><strong>Authorisation</strong> is the request-time decision and server-side enforcement that applies those grants to a principal, action, resource, and context.</p>
<h2>When this matters</h2>
<p>This becomes important when a system has any of the following:</p>
<ul>
<li>users with more than one role or organisation;</li>
<li>tenant, ownership, project, folder, account, or team boundaries;</li>
<li>permissions that depend on resource state, relationships, time, location, MFA, or other trusted context;</li>
<li>customer-configurable or delegated access;</li>
<li>several services or endpoints that must make the same access decision;</li>
<li>audit, compliance, support, or incident needs that require explaining why access was allowed or denied.</li>
</ul>
<p>A small internal tool with a stable administrator/member distinction may need only a simple, centrally tested server-side check. <strong>Do not introduce a policy engine merely to avoid a short, clear rule.</strong></p>
<h2>The mental model</h2>
<h3>Keep authentication and authorisation distinct</h3>
<ul>
<li><strong>Authentication:</strong> who is making this request? Verify identity and session/token integrity.</li>
<li><strong>Authorisation:</strong> may that verified identity perform this action on this resource now?</li>
<li><strong>Entitlements / relationships:</strong> which roles, tenants, ownership relations, subscriptions, and delegations are true?</li>
<li><strong>Enforcement:</strong> where is the decision applied before sensitive data or side effects occur?</li>
</ul>
<p>An authenticated user is not automatically authorised. Client-side checks improve the user experience but never replace server-side authorisation. A role label alone is not a complete permission model.</p>
<h3>Separate permissions from business rules</h3>
<p>Permission answers whether a principal may perform an action on a resource:</p>
<pre><code>Can Alice approve invoice #123?</code></pre>
<p>Business rules answer whether that action is valid in the resource's current state:</p>
<pre><code>Can an approved invoice still be edited?</code></pre>
<p>These are different questions. An authorised user can still fail business validation, and a valid workflow transition can still be forbidden for that user. Workflow state may be a deliberate authorisation input, but permissions must not become a substitute for state-transition validation or domain invariants.</p>
<h3>Ask the same question everywhere</h3>
<p>Express every authorisation decision as:</p>
<pre><code>Can principal P perform action A on resource R in context C?</code></pre>
<p>Use business actions such as <code>invoice.approve</code>, <code>project.member.invite</code>, or <code>document.download</code>, rather than HTTP verbs or UI labels. Make the resource explicit even for create and list operations, where the resource is normally the destination container or tenant.</p>
<h3>Design for default deny and least privilege</h3>
<p>Start with no access. Add narrow, reviewable grants only where a business rule justifies them. Keep explicit prohibitions for non-negotiable guardrails, such as suspended accounts, cross-tenant access, or actions requiring MFA.</p>
<p>Every server-side path that reads protected data, changes state, invokes a privileged integration, or produces a sensitive export needs an enforcement point. Database row-level security, query predicates, and storage access controls remain necessary defence-in-depth; an application authorisation decision does not replace them.</p>
<h2>Build the model before choosing the mechanism</h2>
<h3>1. Map protected capabilities</h3>
<p>Create an authorisation matrix from the domain, not from framework routes.</p>
<table>
<thead>
<tr>
<th>Principal</th>
<th>Action</th>
<th>Resource / container</th>
<th>Conditions</th>
<th>Expected outcome</th>
</tr>
</thead>
<tbody>
<tr>
<td>Staff member</td>
<td><code>case.view</code></td>
<td>a case in their organisation</td>
<td>active employment</td>
<td>Allow</td>
</tr>
<tr>
<td>Case owner</td>
<td><code>case.update</code></td>
<td>their own open case</td>
<td>not locked</td>
<td>Allow</td>
</tr>
<tr>
<td>Organisation admin</td>
<td><code>user.invite</code></td>
<td>their organisation</td>
<td>MFA completed</td>
<td>Allow</td>
</tr>
<tr>
<td>Any user</td>
<td><code>billing.export</code></td>
<td>organisation billing data</td>
<td>no finance role</td>
<td>Deny</td>
</tr>
</tbody>
</table>
<p><strong>Include the negative cases deliberately: cross-tenant access, inactive users, deleted/locked resources, delegated access expiry, and privileged actions without step-up authentication.</strong></p>
<h3>2. Model the stable domain nouns and relationships</h3>
<p>Identify principals, resources, containers, roles/groups, and the relationships that determine access. Use opaque, immutable IDs in policy-facing data; display names, email addresses, and mutable slugs are poor policy identifiers.</p>
<p>For a multi-tenant system, model the tenant/organisation as a first-class container. A resource should have a clear containment path, and a principal’s tenant membership should be supplied from a trusted source. Do not rely on a tenant ID supplied by the browser without resolving and validating it server-side.</p>
<h3>3. Identify the enforcement boundaries</h3>
<p>Document where checks occur for:</p>
<ul>
<li>single-resource reads, updates, deletes, and downloads;</li>
<li>resource creation and listing, against the target container;</li>
<li>batch operations, per resource or via a deliberately designed compound decision;</li>
<li>asynchronous jobs, webhooks, internal APIs, and service accounts;</li>
<li>data stores, object storage, search indexes, and reporting/export paths.</li>
</ul>
<p>A middleware can be a useful adapter, but it is not the model. The authorisation decision must be close enough to the domain operation that route additions cannot silently bypass it.</p>
<h3>4. Define the data contract</h3>
<p>Authorisation is only as trustworthy as its inputs. Define the canonical source, freshness expectation, and ownership for each principal attribute, resource attribute, and relationship. Normalise application data into a single request model before evaluating a rule.</p>
<p>Keep principal, action, and resource facts in their respective models. Use request context only for transient, action-specific facts such as an MFA assertion, request time, or an IP/network signal—not as a duplicate source for identity, role, tenant, or resource ownership.</p>
<h2>Permission implementation practices</h2>
<h3>Address listings, creates, and batches deliberately</h3>
<ul>
<li><strong>Create:</strong> authorise against the verified destination container before creating the resource.</li>
<li><strong>List:</strong> authorise the container-level list action and apply equivalent tenant/visibility constraints in the query. A list decision alone does not make every item safe to reveal.</li>
<li><strong>Batch:</strong> evaluate each target resource or use a deliberately designed compound authorisation rule; do not authorise only the first item.</li>
<li><strong>Move/share:</strong> evaluate all affected source and destination relationships.</li>
</ul>
<h3>Make rule changes a controlled deployment</h3>
<p>Version authorisation rules, their supporting model and mapping code, and regression tests together. Validate rules and their configuration in CI. For dynamically managed rules, use staged rollout, approval, rollback, and versioned audit records. Treat model-contract changes as compatibility changes: a change to attributes, relationships, or resource types can alter existing decisions unexpectedly.</p>
<h3>Observe decisions without leaking data</h3>
<p>Record enough to investigate: request/correlation ID, principal/resource type and opaque IDs where appropriate, action, decision, applicable rule identifiers and version where available, evaluation errors, and latency. Protect logs and avoid raw tokens, sensitive attributes, or full rule payloads unless access and retention are explicitly controlled.</p>
<h3>Test strategy</h3>
<p>Test authorisation as executable security behaviour, not merely syntax or configuration.</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Verify</th>
</tr>
</thead>
<tbody>
<tr>
<td>Decision-model tests</td>
<td>intended grants, explicit prohibitions, default deny, and boundary values</td>
</tr>
<tr>
<td>Authorisation-boundary tests</td>
<td>correct request-to-action mapping, canonical resource resolution, trusted data loading, and error handling</td>
</tr>
<tr>
<td>Endpoint/service tests</td>
<td>denied calls produce no protected data or side effect; allowed calls reach only valid scope</td>
</tr>
<tr>
<td>Data-access tests</td>
<td>queries, storage access, and exports retain tenant and visibility constraints</td>
</tr>
<tr>
<td>Regression tests</td>
<td>known historical incidents and cross-tenant attack cases stay denied</td>
</tr>
<tr>
<td>Change tests</td>
<td>rule/model migrations preserve decisions intentionally and make changed decisions explicit</td>
</tr>
</tbody>
</table>
<p>For every new permission capability, include at least one <em>must allow</em>, <em>must deny</em>, and <em>must not leak</em> scenario. Test actions—not UI controls—because direct API calls and background execution bypass the UI.</p>
<h2>Choose the authorisation mechanism</h2>
<p>Use centrally tested application code when the rules are few, stable, local to one service, and readily understood from the domain model. Keep the decision behind a small authorisation boundary rather than copying checks across routes or handlers.</p>
<p>Introduce a policy engine when the model combines roles with ownership, relationships, tenant containment, and attributes; when the same decision must be made in several services; or when policy review, simulation, traceability, or controlled change is a real requirement. A policy engine does not remove the need for trusted data, enforcement at every boundary, query scoping, or governance of who may change access.</p>
<p>Choose based on the behaviour the system needs, not the novelty of the tool.</p>
<h3>A common evolution path</h3>
<p>Most systems do not need their final permission model on day one. A common progression is:</p>
<pre><code>Simple role checks
        ↓
Central authorisation module or service
        ↓
Resource ownership
        ↓
Tenant isolation
        ↓
Attribute conditions
        ↓
Delegation
        ↓
Shared policies
        ↓
Policy engine, if the demonstrated complexity justifies it</code></pre>
<p>This is not a universal sequence or a maturity scorecard. A product may need tenant isolation before ownership, or never need delegation at all. Treat it as a set of capabilities: adopt the next one only when a concrete product, security, or operational need makes the current model insufficient.</p>
<p>Starting simple is normal. The important discipline is to centralise decisions early enough that the model can evolve deliberately, rather than accumulating inconsistent route-by-route checks.</p>
<h2>Operational checklist</h2>
<h3>Before implementation</h3>
<ul>
<li>Define the decision to centralise and the protected business outcome.</li>
<li>Inventory all entry points and data/side-effect paths for that domain.</li>
<li>Name domain actions, resources, containers, and trusted attributes.</li>
<li>Write allow and deny scenarios, including cross-tenant and stale/disabled-user cases.</li>
<li>Identify source, freshness, and owner of each authorisation fact.</li>
<li>Choose simple central checks or a policy engine based on demonstrated complexity.</li>
</ul>
<h3>Before rollout</h3>
<ul>
<li>Authorisation rules and configuration validate in CI.</li>
<li>Rule changes have review, audit, and rollback paths.</li>
<li>Every protected operation uses the authorisation boundary or an equivalent verified enforcement path.</li>
<li>List, batch, create, move, export, background-job, and service-account paths are covered.</li>
<li>Decision telemetry captures implementation version and evaluation errors safely.</li>
<li>Existing data/query/storage controls still enforce tenant and ownership boundaries.</li>
<li>Negative and regression tests prove denial causes neither disclosure nor side effect.</li>
</ul>
<h3>After rollout</h3>
<ul>
<li>Inspect allow/deny rates, diagnostics, latency, and unexpected denials.</li>
<li>Confirm no route or job bypasses the enforcement boundary.</li>
<li>Review exception policies, stale roles, delegations, and privileged access on a defined cadence.</li>
<li>Record incidents and confusing access requests as new regression cases or model improvements.</li>
</ul>
<h2>Anti-patterns</h2>
<ul>
<li><strong>UI-only permission checks:</strong> hiding a button while leaving the API callable.</li>
<li><strong>Route-by-route conditionals:</strong> duplicating unreviewed logic until equivalent rules disagree.</li>
<li><strong>Role explosion:</strong> adding a new role for every exception instead of modelling the relationship or rule condition.</li>
<li><strong>Tenant IDs from the client:</strong> trusting an unverified selector or URL parameter as access proof.</li>
<li><strong>Unscoped list authorisation:</strong> permitting <code>list</code> but returning objects the caller may not read.</li>
<li><strong>Authorisation as a magic boundary:</strong> assuming one decision protects data paths it does not govern.</li>
<li><strong>Rule/model drift:</strong> changing domain relationships or configuration without revalidating affected authorisation decisions.</li>
<li><strong>Ignoring decision errors:</strong> treating failed or incomplete evaluations as harmless without monitoring their impact.</li>
<li><strong>Mutable identifiers in rules:</strong> binding access to email addresses, names, or human-facing slugs.</li>
<li><strong>No explanation trail:</strong> being unable to answer who granted access, why it applied, and when it changed.</li>
</ul>
<h2>References</h2>
<ul>
<li><a href="https://rafael.bernard-araujo.com/playbooks/playbook-permissions-and-authorisation">Playbook - Permission and Authorisation</a></li>
<li><a href="https://cheatsheetseries.owasp.org/cheatsheets/Authorization_Cheat_Sheet.html">OWASP Authorization Cheat Sheet</a></li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://rafael.bernard-araujo.com/permissions-and-authorisation-a-practical-playbook.php/feed</wfw:commentRss>
			<slash:comments>1</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2395</post-id>	</item>
		<item>
		<title>Building Evolutionary Architectures &#8211; Chapter 2: Fitness Functions</title>
		<link>https://rafael.bernard-araujo.com/building-evolutionary-architectures-chapter-2-fitness-functions.php</link>
					<comments>https://rafael.bernard-araujo.com/building-evolutionary-architectures-chapter-2-fitness-functions.php#respond</comments>
		
		<dc:creator><![CDATA[rafael]]></dc:creator>
		<pubDate>Tue, 19 May 2026 00:58:20 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[fitness functions]]></category>
		<category><![CDATA[software architecture]]></category>
		<guid isPermaLink="false">https://rafael.bernard-araujo.com/?p=2376</guid>

					<description><![CDATA[Chapter 2 introduces the concept of architectural fitness functions, the mechanism that makes &#34;evolutionary&#34; more than a buzzword. The origin: borrowing from evolutionary computing The term comes from genetic algorithm design. In evolutionary computing, a fitness function defines what &#34;better&#34; means so that solutions can gradually emerge through small changes across generations. The classic example: [&#8230;]]]></description>
										<content:encoded><![CDATA[<p>Chapter 2 introduces the concept of <strong>architectural fitness functions</strong>, the mechanism that makes &quot;evolutionary&quot; more than a buzzword.</p>
<h2>The origin: borrowing from evolutionary computing</h2>
<p>The term comes from genetic algorithm design. In evolutionary computing, a fitness function defines what &quot;better&quot; means so that solutions can gradually emerge through small changes across generations. The classic example: when using a genetic algorithm to optimise wing design, the fitness function assesses wind resistance, weight, air flow, and other desirable characteristics. At each generation, the engineer asks: is this closer to or further away from the goal?</p>
<p>Ford, Parsons and Kua borrow this concept for software:</p>
<blockquote>
<p><strong>An architectural fitness function provides an objective integrity assessment of some architectural characteristic(s).</strong></p>
</blockquote>
<p>In software, fitness functions check that developers preserve important architectural characteristics; the &quot;-ilities&quot; architects care about: scalability, security, performance, maintainability, resilience.</p>
<h2>The core idea</h2>
<p>An evolutionary architecture supports <em>guided</em>, incremental change across multiple dimensions. The key word is <strong><em>guided</em></strong>. Without guidance, incremental change is just drift. Fitness functions are what provide the guidance.</p>
<p>The fitness function protects the various architectural characteristics required for the system. These requirements differ greatly across systems and organisations: some require intense security; others require significant throughput or low latency; others need resilience to failure. A crucial early architecture decision is to define which dimensions matter most for a given system, based on business drivers, technical capabilities, and scale.</p>
<h2>Why this matters</h2>
<p>Most teams have implicit architectural goals: &quot;the system should be fast&quot;, &quot;services should be loosely coupled&quot;, &quot;we should be secure&quot;. The problem is that implicit goals erode. Nobody notices the slow degradation until a characteristic has already failed.</p>
<p>Fitness functions make the implicit explicit. They turn architectural aspirations into verifiable checks. Automated where possible, manual where necessary.</p>
<p>A key insight: improving one architectural dimension can accidentally harm another. Improving performance with caching might harm data freshness or security. Fitness functions act as guardrails that detect these tradeoff violations before they reach production.</p>
<h2>Categorising fitness functions</h2>
<p>The book defines several dimensions for classifying fitness functions:</p>
<h3>Atomic vs Holistic</h3>
<ul>
<li><strong>Atomic</strong> — tests one particular aspect of the architecture in isolation. Example: a unit test checking for cyclic dependencies in a package, or a code metric that checks cyclomatic complexity.</li>
<li><strong>Holistic</strong> — tests a combination of architectural aspects, assessing interactions between different concerns. Example: testing the number of concurrent users within a certain latency range while caching is enabled — this simultaneously checks scalability and data freshness. Holistic functions are harder to build but capture what atomic ones miss.</li>
</ul>
<h3>Triggered vs Continuous vs Temporal</h3>
<ul>
<li><strong>Triggered</strong> — executed in response to a specific event: a developer running a unit test, a CI pipeline stage, a QA person performing exploratory testing.</li>
<li><strong>Continuous</strong> — constant verification of architectural aspects. Monitoring and alerting are the classic examples. Netflix's Chaos Monkey — which runs in production and randomly terminates instances — is a continuous holistic fitness function that forces teams to build resilient services.</li>
<li><strong>Temporal</strong> — have a particular time component. Example: a reminder to check whether important security updates have been performed, or a scheduled dependency check that alerts on outdated libraries.</li>
</ul>
<h3>Static vs Dynamic</h3>
<ul>
<li><strong>Static</strong> — fixed predefined acceptable values. Binary pass/fail (a unit test), or a threshold (latency must be &lt; 200ms).</li>
<li><strong>Dynamic</strong> — acceptable values depend on context. Acceptable latency might depend on actual system scale; security requirements might vary based on the regulatory environment.</li>
</ul>
<h3>Automated vs Manual</h3>
<ul>
<li><strong>Automated</strong> — unit tests, deployment pipeline checks, stress tests, chaos engineering. Ideally as much automation as possible.</li>
<li><strong>Manual</strong> — some things can't be automated (legal approval requirements, certain QA processes). Some things aren't automated <em>yet</em>. The goal is to push the boundary toward automation over time.</li>
</ul>
<h2>What fitness functions look like in practice</h2>
<p>Fitness functions encompass existing engineering practices but also extend beyond them:</p>
<table>
<thead>
<tr>
<th>Category</th>
<th>Examples</th>
<th>Type</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Architecture tests</strong></td>
<td>phpat (PHP/PHPStan) or ts-arch (TypeScript) rules checking component dependencies, layer violations, naming conventions, import directionality</td>
<td>Atomic, triggered</td>
</tr>
<tr>
<td><strong>Code metrics</strong></td>
<td>Cyclomatic complexity thresholds, afferent/efferent coupling limits</td>
<td>Atomic, triggered</td>
</tr>
<tr>
<td><strong>Contract tests</strong></td>
<td>API contract verification ensuring requirements are met</td>
<td>Atomic, triggered</td>
</tr>
<tr>
<td><strong>Security scanning</strong></td>
<td>Vulnerability scanning, licence compliance checks for open-source dependencies</td>
<td>Atomic, triggered</td>
</tr>
<tr>
<td><strong>Performance testing</strong></td>
<td>Load tests validating latency SLOs under expected concurrency</td>
<td>Holistic, triggered</td>
</tr>
<tr>
<td><strong>Monitoring &amp; alerting</strong></td>
<td>p99 latency monitors, error rate thresholds, SLO compliance dashboards</td>
<td>Atomic/holistic, continuous</td>
</tr>
<tr>
<td><strong>Chaos engineering</strong></td>
<td>Netflix Simian Army — randomly terminating instances, availability zones, or entire regions</td>
<td>Holistic, continuous</td>
</tr>
<tr>
<td><strong>Security reviews</strong></td>
<td>Quarterly security audits, penetration testing</td>
<td>Holistic, manual/temporal</td>
</tr>
<tr>
<td><strong>Dependency freshness</strong></td>
<td>Scheduled checks for outdated libraries or security patches</td>
<td>Atomic, temporal</td>
</tr>
</tbody>
</table>
<p><strong>The best fitness functions are</strong> <strong>automated and triggered</strong>: they give feedback at the point of change, not weeks later. Place them in the deployment pipeline. Fast atomic functions early, slow holistic functions later.</p>
<h2>Deployment pipelines as the enforcement mechanism</h2>
<p>Fitness functions only work if they're part of the delivery workflow. The deployment pipeline is where they live:</p>
<ol>
<li><strong>Early stages</strong> — fast, atomic checks: architecture tests (phpat, ts-arch), code metrics, linting, security scanning, contract tests.</li>
<li><strong>Middle stages</strong> — integration and performance tests, holistic triggered functions.</li>
<li><strong>Later stages / production</strong> — continuous monitoring, chaos engineering, temporal reminders.</li>
</ol>
<p>As Thoughtworks puts it: <em>&quot;creating the desired fitness functions — and including them in appropriate delivery pipelines — communicates these metrics as an important aspect of enterprise architecture.&quot;</em></p>
<h2>The four layers of fitness (from NILUS)</h2>
<p>A useful framing from practice splits fitness functions across four layers:</p>
<ol>
<li><strong>Structural fitness</strong> — code dependencies, database access patterns, API contracts, service boundaries.</li>
<li><strong>Behavioural fitness</strong> — latency, resilience, throughput, consistency, recovery behaviour.</li>
<li><strong>Operational fitness</strong> — deployment independence, observability coverage, runbook readiness, SLO compliance.</li>
<li><strong>Semantic fitness</strong> — bounded context integrity, event naming quality, policy ownership, domain model consistency.</li>
</ol>
<p>Most teams start at structural (the easiest to automate) and never reach semantic. But <strong>semantic fitness functions</strong> (checking that your domain model remains coherent as it evolves) <strong>are often the most valuable for long-lived systems</strong>.</p>
<h2>Systems thinking</h2>
<p>Dr. Russell Ackoff's quote captures the deeper point:</p>
<blockquote>
<p>A system is never the sum of its parts. It is the product of the interaction of its parts.</p>
</blockquote>
<p>Fitness functions that only measure individual components miss the point. The interesting failures happen at integration boundaries — between services, between teams, between intentions and reality. Holistic fitness functions (end-to-end latency, deployment frequency, change failure rate) capture what atomic ones cannot.</p>
<h2>How I'm applying this</h2>
<p>This connects directly to work I care about:</p>
<ul>
<li><strong>Platform modernisations</strong> I've designed and implemented were operational fitness: bringing reliability through automated deployment pipelines, observability and monitoring, and runbook readiness. I just called it &quot;keeping things running.&quot;</li>
<li><strong>ADRs</strong> capture the <em>decisions</em>. Fitness functions verify those decisions are still holding. Decisions and verification go hand in hand.</li>
<li><strong>Kent Beck's Test Desiderata</strong> is itself a fitness function for test quality — a checklist of characteristics that tests should exhibit (isolated, deterministic, fast, behavioural, structure-insensitive, specific, predictive).</li>
<li><strong>DORA metrics</strong> (deployment frequency, lead time, change failure rate, MTTR) are fitness functions for delivery capability.</li>
<li><strong>Code health metrics</strong> (as described in the Loveholidays case from <a href="https://rafael.bernard-araujo.com/tropecando-120.php">Tropeçando 120</a>) are fitness functions that enabled their AI-first shift — they invested in code health metrics <em>before</em> adopting AI, which is exactly the fitness-function-first approach.</li>
<li><strong>phpat</strong> (PHP, as a PHPStan extension) and <strong>ts-arch</strong> (TypeScript) — writing architecture rules as unit tests that run in CI is the purest implementation of triggered atomic fitness functions.</li>
</ul>
<p>The pattern: define what matters, measure it, enforce it automatically, and revisit periodically. Architecture that can't be verified can't evolve — it can only decay.</p>
<h2>Further reading</h2>
<ul>
<li><a href="https://martinfowler.com/articles/evo-arch-forward.html">Foreword to Building Evolutionary Architectures</a> — Martin Fowler's foreword to the book, framing fitness functions as the mechanism to monitor architectural state in an evolutionary style.</li>
<li><a href="https://www.thoughtworks.com/en-gb/insights/articles/fitness-function-driven-development">Fitness function-driven development</a> — Paula Paul &amp; Rosemary Wang apply the TDD mindset to architecture: write the fitness function first, then develop to pass it.</li>
<li><a href="https://martinfowler.com/articles/fitness-functions-data-products.html">Governing data products using fitness functions</a> — Extending fitness functions into Data Mesh governance (2024).</li>
<li><a href="https://www.thoughtworks.com/en-ca/insights/books/building-evolutionaryarchitectures-second-edition">Building Evolutionary Architectures, 2nd Edition</a> — The book.</li>
</ul>
<hr />
<p><em>Part of my reading notes on <a href="https://rafael.bernard-araujo.com/building-evolutionary-architectures-notes.php">Building Evolutionary Architectures</a> (Ford, Parsons, Kua).</em></p>
]]></content:encoded>
					
					<wfw:commentRss>https://rafael.bernard-araujo.com/building-evolutionary-architectures-chapter-2-fitness-functions.php/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2376</post-id>	</item>
		<item>
		<title>Building Evolutionary Architectures Notes</title>
		<link>https://rafael.bernard-araujo.com/building-evolutionary-architectures-notes.php</link>
					<comments>https://rafael.bernard-araujo.com/building-evolutionary-architectures-notes.php#respond</comments>
		
		<dc:creator><![CDATA[rafael]]></dc:creator>
		<pubDate>Tue, 19 May 2026 00:30:15 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[Technology]]></category>
		<category><![CDATA[software architecture]]></category>
		<guid isPermaLink="false">https://rafael.bernard-araujo.com/?p=2378</guid>

					<description><![CDATA[Notes Chapter 1: Software Architecture Despite our best efforts, software becomes harder to change over time. For a variety of reasons, the parts that comprise software systems defy easy modifications, becoming more brittle and intractable over time. Changes in software projects are usually driven by a reevaluation of functionality and/or scope. But another type of [&#8230;]]]></description>
										<content:encoded><![CDATA[<h2>Notes</h2>
<h3>Chapter 1: Software Architecture</h3>
<blockquote>
<p>Despite our best efforts, software becomes harder to change over time. For a variety of reasons, the parts that comprise software systems defy easy modifications, becoming more brittle and intractable over time. Changes in software projects are usually driven by a reevaluation of functionality and/or scope. But another type of change occurs outside the control of architects and long-term planners. Though architects like to be able to strategically plan for the future, the constantly changing software development ecosystem makes that difficult. Since we can't avoid change, we need to exploit it.</p>
</blockquote>
<p>— On <em>Evolutionary Architecture</em></p>
<blockquote>
<p>An evolutionary architecture supports guided, incremental changes across multiple dimensions.</p>
</blockquote>
<p>— Definition of <em>Evolutionary Architecture</em></p>
<h3>Related: Ralph Johnson on Architecture (via Fowler, 2003)</h3>
<p>These quotes from Ralph Johnson (from [[2026-04-24 - Martin Fowler - Who Needs an Architect|Who Needs an Architect?]]) are foundational to the ideas in this book:</p>
<blockquote>
<p>&quot;In most successful software projects, the expert developers working on that project have a shared understanding of the system design. This shared understanding is called 'architecture.' [...] the architecture only includes the components and interfaces that are understood by all the developers.&quot;</p>
</blockquote>
<p>— Architecture as a social construct, not a diagram.</p>
<blockquote>
<p>&quot;There is no theoretical reason that anything is hard to change about software. If you pick any one aspect of software then you can make it easy to change, but we don't know how to make everything easy to change. Making something easy to change makes the overall system a little more complex, and making everything easy to change makes the entire system very complex. Complexity is what makes software hard to change. That, and duplication.&quot;</p>
</blockquote>
<p>— The fundamental tension that evolutionary architectures try to navigate: change vs complexity.</p>
<blockquote>
<p>&quot;Software is not limited by physics, like buildings are. It is limited by imagination, by design, by organization. In short, it is limited by properties of people, not by properties of the world. 'We have met the enemy, and he is us.'&quot;</p>
</blockquote>
<p>— The constraint is us, not the technology.</p>
<h3>Chapter 2: Fitness Functions</h3>
<blockquote>
<p>An evolutionary architecture supports <em>guided</em>, incremental change across multiple dimensions.</p>
</blockquote>
<p>-- on FItness Functions, chapter 2</p>
<blockquote>
<p>The fitness function protects the various archutectural characteristics required for the system. The specific architectural requirements differ greatly across systems and organizations, based on business drivers, technical capabilities, and a host of other factors. Some systems require intense security; others require significant throughput factors.</p>
</blockquote>
<p>-- on Fitness Functions, chapter 2</p>
<blockquote>
<p>A system is never the sum of its parts. It is the product of the interaction of its parts.</p>
</blockquote>
<p>-- Dr. Russel Ackoff</p>
]]></content:encoded>
					
					<wfw:commentRss>https://rafael.bernard-araujo.com/building-evolutionary-architectures-notes.php/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2378</post-id>	</item>
		<item>
		<title>Introduce Parameter Object &#124; Refactoring Patterns</title>
		<link>https://rafael.bernard-araujo.com/introduce-parameter-object-refactoring-patterns.php</link>
					<comments>https://rafael.bernard-araujo.com/introduce-parameter-object-refactoring-patterns.php#respond</comments>
		
		<dc:creator><![CDATA[rafael]]></dc:creator>
		<pubDate>Wed, 22 Apr 2026 05:45:10 +0000</pubDate>
				<category><![CDATA[Code example]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[Refactoring Patterns]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[refactoring patterns]]></category>
		<category><![CDATA[rust]]></category>
		<category><![CDATA[software architecture]]></category>
		<guid isPermaLink="false">https://rafael.bernard-araujo.com/?p=2357</guid>

					<description><![CDATA[This refactoring pattern involves grouping parameters that naturally go together into a single object. When you see a group of data items that regularly travel together, appearing in function after function, it\'s a sign they should be combined into a single object. Benefits: - Reduces the number of parameters (improved readability) - Groups related data [&#8230;]]]></description>
										<content:encoded><![CDATA[<p>This refactoring pattern involves grouping parameters that naturally go together into a single object. When you see a group of data items that regularly travel together, appearing in function after function, it\'s a sign they should be combined into a single object. Benefits: - Reduces the number of parameters (improved readability) - Groups related data together (better organisation) - Makes relationships between data explicit - Easier to add new related data without changing function signatures - Enables moving behaviour related to the data into the new class - Reduces errors from parameter ordering mistakes When to use: - When you have a group of parameters that often appear together - When you see the same parameters in multiple function signatures - When parameters represent a cohesive concept - When you need to add more related parameters - When parameter ordering becomes confusing See <a href="https://refactoring.com/catalog/introduceParameterObject.html">https://refactoring.com/catalog/introduceParameterObject.html</a> <strong>BEFORE: Multiple primitive parameters passed around</strong> Problems: - Too many parameters (hard to remember and maintain) - Parameters are related, but the relationship is not explicit - Easy to mix up parameter order - Adding new related data requires changing all function signatures - Difficult to add validation or behaviour for the group ```php declare(strict_types=1);</p>
<p>namespace RefactoringPatterns;</p>
<p>class IntroduceParameterObject<br />
{<br />
public function amountInvoicedBefore(<br />
\DateTimeImmutable $startDate,<br />
\DateTimeImmutable $endDate,<br />
string $customerName,<br />
string $customerId,<br />
string $customerEmail<br />
): float {<br />
$amount = 0;<br />
foreach ($this-getInvoices() as $invoice) { if ($this-&gt;isInDateRangeBefore($invoice, $startDate, $endDate) &amp;&amp; $this-&gt;isForCustomerBefore($invoice, $customerName, $customerId, $customerEmail)) { $amount += $invoice[\'amount\']; } } return $amount; } private function isInDateRangeBefore( array $invoice, \DateTimeImmutable $startDate, \DateTimeImmutable $endDate ): bool { return $invoice[\'date\'] &gt;= $startDate &amp;&amp; $invoice[\'date\'] &lt;= $endDate; } private function isForCustomerBefore( array $invoice, string $customerName, string $customerId, string $customerEmail ): bool { return $invoice[\'customerId\'] === $customerId; } public function demonstratePattern(): void { $startDate = new \DateTimeImmutable(\'2024-01-01\'); $endDate = new \DateTimeImmutable(\'2024-03-31\'); $customerName = \'John Doe\'; $customerId = \'C001\'; $customerEmail = \'john@example.com\'; echo BEFORE (5 separate parameters):\n; $amountBefore = $this-&gt;amountInvoicedBefore( $startDate, $endDate, $customerName, $customerId, $customerEmail ); echo Amount invoiced: $ . number_format($amountBefore, 2) . \n; echo Issues: Too many parameters, unclear relationships\n\n; } } <code><code> **AFTER: Using parameter objects for related data** Benefits: - Reduced the chance of parameter ordering errors - Clear, self-documenting function signatures - Related data is explicitly grouped - Easy to add new related fields without changing signatures - Can add behaviour and validation to the parameter objects </code></code>php declare(strict_types=1);</p>
<p>namespace RefactoringPatterns;</p>
<p>class IntroduceParameterObject<br />
{<br />
public function amountInvoicedAfter(DateRange $dateRange, Customer $customer): float<br />
{<br />
$amount = 0;<br />
foreach ($this-getInvoices() as $invoice) { if ($this-&gt;isInDateRangeAfter($invoice, $dateRange) &amp;&amp; $this-&gt;isForCustomerAfter($invoice, $customer)) { $amount += $invoice[\'amount\']; } } return $amount; } private function isInDateRangeAfter(array $invoice, DateRange $dateRange): bool { return $dateRange-&gt;contains($invoice[\'date\']); } private function isForCustomerAfter(array $invoice, Customer $customer): bool { return $invoice[\'customerId\'] === $customer-&gt;id; } public function demonstratePattern(): void { echo AFTER (2 parameter objects):\n; $dateRange = new DateRange($startDate, $endDate); $customer = new Customer($customerId, $customerName, $customerEmail); $amountAfter = $this-&gt;amountInvoicedAfter($dateRange, $customer); echo Amount invoiced: $ . number_format($amountAfter, 2) . \n; echo Benefits: Clear grouping, can add validation and behavior\n\n; } } /<strong> <em> Parameter Object: DateRange </em> Encapsulates a range of dates with validation and behavior */ class DateRange { public function <strong>construct( private readonly \DateTimeImmutable $startDate, private readonly \DateTimeImmutable $endDate ) { if ($endDate &lt; $startDate) { throw new \InvalidArgumentException(\'End date must be after start date\'); } } public function getStartDate(): \DateTimeImmutable { return $this-&gt;startDate; } public function getEndDate(): \DateTimeImmutable { return $this-&gt;endDate; } /<strong> <em> Behavior: Check if a date is within the range </em>/ public function contains(\DateTimeImmutable $date): bool { return $date &gt;= $this-&gt;startDate &amp;&amp; $date &lt;= $this-&gt;endDate; } /</strong> <em> Behavior: Get the duration of the range in days </em>/ public function getDurationInDays(): int { return $this-&gt;startDate-&gt;diff($this-&gt;endDate)-&gt;days; } } /*<em> </em> Parameter Object: Customer <em> Encapsulates customer-related data with validation </em>/ class Customer { public readonly string $id; public readonly string $name; public readonly string $email; public function </strong>construct(string $id, string $name, string $email) { if (empty($id)) { throw new \InvalidArgumentException(\'Customer ID cannot be empty\'); } if (empty($name)) { throw new \InvalidArgumentException(\'Customer name cannot be empty\'); } if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { throw new \InvalidArgumentException(\'Invalid email address\'); } $this-&gt;id = $id; $this-&gt;name = $name; $this-&gt;email = $email; } /</strong> <em> Behavior: Get customer display name </em>/ public function getDisplayName(): string { return {$this-&gt;name} ({$this-&gt;email}); } } <code><code> Another example - Coordinates passed as primitives Before </code></code>php declare(strict_types=1);</p>
<p>namespace RefactoringPatterns;</p>
<p>class IntroduceParameterObject<br />
{<br />
public function calculateDistanceBefore(<br />
float $x1,<br />
float $y1,<br />
float $x2,<br />
float $y2<br />
): float {<br />
$dx = $x2 - $x1;<br />
$dy = $y2 - $y1;<br />
return sqrt($dx <em> $dx + $dy </em> $dy);<br />
}</p>
<pre><code>public function findNearbyLocationsBefore(
    float $centerX,
    float $centerY,
    float $radius,
    array $locations
): array {
    $nearby = [];
    foreach ($locations as $location) {
        $distance = $this-calculateDistanceBefore( $centerX, $centerY, $location[\'x\'], $location[\'y\'] ); if ($distance <= $radius) { $nearby[] = $location; } } return $nearby; } public function demonstratePattern(): void { echo Example 2: Location Distance Calculation\\n; echo -----------------------------------------\\n; $locations = [ [\'name\' => \'Store A\', \'x\' => 10.0, \'y\' => 20.0], [\'name\' => \'Store B\', \'x\' => 15.0, \'y\' => 25.0], [\'name\' => \'Store C\', \'x\' => 50.0, \'y\' => 50.0], ]; echo BEFORE (4 coordinate parameters):\\n; $nearbyBefore = $this->findNearbyLocationsBefore(10.0, 20.0, 10.0, $locations); echo Found  . count($nearbyBefore) .  nearby locations\\n; echo Issues: Easy to mix up x1, y1, x2, y2 parameters\\n\\n; } } ``<code> After </code>``php declare(strict_types=1);</code></pre>
<p>namespace RefactoringPatterns;</p>
<p>class IntroduceParameterObject<br />
{<br />
public function calculateDistanceAfter(Point $point1, Point $point2): float<br />
{<br />
return $point1-distanceTo($point2); } public function findNearbyLocationsAfter( Point $center, float $radius, array $locations ): array { $nearby = []; foreach ($locations as $locationData) { $location = new Point($locationData[\'x\'], $locationData[\'y\']); if ($center-&gt;distanceTo($location) &lt;= $radius) { $nearby[] = $locationData; } } return $nearby; } public function demonstratePattern(): void { // Example 2: Coordinate calculations echo Example 2: Location Distance Calculation\n; echo -----------------------------------------\n; $locations = [ [\'name\' =&gt; \'Store A\', \'x\' =&gt; 10.0, \'y\' =&gt; 20.0], [\'name\' =&gt; \'Store B\', \'x\' =&gt; 15.0, \'y\' =&gt; 25.0], [\'name\' =&gt; \'Store C\', \'x\' =&gt; 50.0, \'y\' =&gt; 50.0], ]; echo AFTER (Point parameter object):\n; $center = new Point(10.0, 20.0); $nearbyAfter = $this-&gt;findNearbyLocationsAfter($center, 10.0, $locations); echo Found  . count($nearbyAfter) .  nearby locations\n; echo Benefits: Point object can now have behavior (distanceTo method)\n\n; echo === Key Benefits ===\n; echo 1. Function signatures are clearer and more maintainable\n; echo 2. Related data is explicitly grouped together\n; echo 3. Behavior can be added to parameter objects\n; echo 4. Easier to extend without breaking existing code\n; echo 5. Reduced chance of parameter ordering mistakes\n; echo 6. Parameter objects can enforce validation rules\n; } } /*<em> </em> Parameter Object: Point <em> Encapsulates 2D coordinates with geometric operations </em>/ class Point { public function __construct( private readonly float $x, private readonly float $y ) { } public function getX(): float { return $this-&gt;x; } public function getY(): float { return $this-&gt;y; } /<strong> <em> Behavior: Calculate distance to another point </em>/ public function distanceTo(Point $other): float { $dx = $other-&gt;x - $this-&gt;x; $dy = $other-&gt;y - $this-&gt;y; return sqrt($dx <em> $dx + $dy </em> $dy); } /</strong> <em> Behavior: Create a new point offset from this one </em>/ public function offset(float $dx, float $dy): Point { return new Point($this-&gt;x + $dx, $this-&gt;y + $dy); } } ```</p>
]]></content:encoded>
					
					<wfw:commentRss>https://rafael.bernard-araujo.com/introduce-parameter-object-refactoring-patterns.php/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2357</post-id>	</item>
		<item>
		<title>Building a Serverless PHP Application with Bref, Symfony, and DynamoDB Session Management</title>
		<link>https://rafael.bernard-araujo.com/building-a-serverless-php-application-with-bref-symfony-and-dynamodb-session-management.php</link>
					<comments>https://rafael.bernard-araujo.com/building-a-serverless-php-application-with-bref-symfony-and-dynamodb-session-management.php#respond</comments>
		
		<dc:creator><![CDATA[rafael]]></dc:creator>
		<pubDate>Tue, 30 Dec 2025 07:48:35 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Programming]]></category>
		<category><![CDATA[Technology]]></category>
		<guid isPermaLink="false">https://rafael.bernard-araujo.com/?p=2283</guid>

					<description><![CDATA[Introduction Serverless apps are fantastic for automatic scaling, but there’s a catch: they expect you to be stateless. Most web applications, however, rely on sessions to remember users and persist state. Traditional PHP session handlers store data on the filesystem, which doesn’t play nicely with ephemeral AWS Lambda instances. Your sessions vanish as soon as [&#8230;]]]></description>
										<content:encoded><![CDATA[<h2>Introduction</h2>
<p>Serverless apps are fantastic for automatic scaling, but there’s a catch: they expect you to be stateless. Most web applications, however, rely on sessions to remember users and persist state. Traditional PHP session handlers store data on the filesystem, which doesn’t play nicely with ephemeral AWS Lambda instances. Your sessions vanish as soon as the instance disappears.</p>
<p>The usual fix? Fire up a Redis cluster. Works, but suddenly you’ve added infrastructure, ongoing maintenance, and extra costs. Your “serverless” app feels a lot less serverless.</p>
<p>What if we could manage sessions <strong>without touching Redis or any other server</strong>?</p>
<p>In this post, we’ll show you how to build a <strong>truly serverless PHP app</strong> using <strong>Bref</strong>, <strong>Symfony</strong>, and <strong>DynamoDB</strong> for session management. Along the way, you’ll see:</p>
<ul>
<li>A <strong>custom DynamoDB-backed session handler</strong> that replaces filesystem sessions</li>
<li>How to deploy your app via <strong>Lambda Function URLs</strong> using AWS CDK</li>
<li>Storing <strong>CSRF tokens in DynamoDB</strong> for fully stateless operation</li>
<li><strong>Single-table design patterns</strong> for efficient multi-entity storage</li>
</ul>
<p>By the end, you’ll know not just <em>how</em> to implement this architecture, but also <em>when</em> it makes sense and what trade-offs you’re accepting.</p>
<h2>The Challenge: Sessions in Serverless</h2>
<p>Before we dive into the solution, let’s understand why traditional PHP sessions fail in serverless environments.</p>
<ol>
<li><strong>Ephemeral Storage</strong>: Lambda instances can vanish at any time. Writing sessions to <code>/tmp</code> is like storing them in sand. They disappear when the instance is recycled.</li>
<li><strong>No Shared Filesystem</strong>: Each Lambda invocation runs on its own instance. User A’s session written by instance 1 is invisible to instance 2. That’s a problem if your user expects to stay logged in.</li>
<li><strong>Horizontal Scaling Woes</strong>: Lambda scales horizontally automatically. Without centralized session storage, each instance is isolated. Consistent session management? Forget it.</li>
</ol>
<h3>The Traditional Solution: Redis/ElastiCache</h3>
<p>Most serverless PHP guides suggest Redis. While it works, it comes with headaches:</p>
<ul>
<li><strong>Infrastructure complexity</strong>: VPCs, subnets, and security groups</li>
<li><strong>Maintenance burden</strong>: Patching, monitoring, capacity planning</li>
<li><strong>Cold start penalty</strong>: VPC-connected Lambdas can take 1–2 extra seconds</li>
</ul>
<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> <strong>Better idea</strong>: DynamoDB. It’s fully managed, serverless, and scales automatically. No Redis cluster, no maintenance, just pay for what you use.</p>
<h2>Books and Authors App (Serverless Style)</h2>
<p>Imagine you’re building a multi-tenant SaaS app, like an internal tool for managing books and authors. Each user needs a session, and each organization manages its own data. DynamoDB’s single-table design can elegantly handle all this. Serverless scaling takes care of traffic spikes automatically.</p>
<p>Here’s what this example demonstrates:</p>
<ul>
<li><strong>Multi-entity relationships</strong>: Books belong to authors</li>
<li><strong>CRUD operations</strong>: Create, read, update, and delete across related entities</li>
<li><strong>Session-dependent workflows</strong>: Adding/editing books requires authentication</li>
<li><strong>Real-world complexity</strong>: More than a simple counter, less than a full e-commerce platform</li>
</ul>
<h3>Connecting to Real Use Cases</h3>
<p>This architecture shines in scenarios like:</p>
<ul>
<li><strong>Unpredictable traffic</strong>: Seasonal spikes when authors release new books</li>
<li><strong>Session management</strong>: Authors need persistent sessions to edit content</li>
<li><strong>Cost efficiency</strong>: During quiet periods, you pay pennies; during spikes, DynamoDB scales automatically</li>
<li><strong>Zero maintenance</strong>: No Redis clusters to monitor, no database servers to patch</li>
</ul>
<p>The book management example proves that this approach isn’t just theoretical: it’s production-ready.</p>
<h2>Architecture Overview</h2>
<p>To build a serverless PHP application that supports sessions, CSRF protection, and persistent data, we follow a <strong>stateful/stateless separation</strong> pattern. This makes the architecture scalable, cost-efficient, and easy to maintain.</p>
<h3>1. Stateful Layer: Persistent Data</h3>
<p>This layer is responsible for storing all data that needs to survive beyond a single Lambda invocation.</p>
<ul>
<li>
<p><strong>DynamoDB Table</strong></p>
<ul>
<li>Uses a <strong>single-table design</strong> to store sessions, CSRF tokens, users, books, and authors.</li>
<li><strong>TTL enabled</strong> for automatic session expiration.</li>
<li><strong>On-demand billing</strong> ensures automatic scaling with traffic.</li>
<li>Built-in <strong>multi-AZ replication</strong> provides high availability.</li>
</ul>
</li>
<li>
<p><strong>Benefits</strong></p>
<ul>
<li>No infrastructure to manage or patch.</li>
<li>Automatically scales with unpredictable traffic.</li>
<li>Centralized storage simplifies queries and operations.</li>
</ul>
</li>
</ul>
<h3>2. Stateless Layer: Application Logic</h3>
<p>This layer runs the application code and handles requests without storing any persistent state locally.</p>
<ul>
<li>
<p><strong>Lambda Function</strong></p>
<ul>
<li>Runs <strong>PHP-FPM</strong> via Bref.</li>
<li>Handles HTTP requests directly using a <strong>Lambda Function URL</strong> (HTTPS endpoint).</li>
<li>No VPC required to access DynamoDB, reducing cold start latency.</li>
</ul>
</li>
<li>
<p><strong>Static Assets</strong></p>
<ul>
<li>Stored in <strong>S3</strong> (optionally served via CloudFront) to keep Lambda stateless.</li>
</ul>
</li>
<li>
<p><strong>Benefits</strong></p>
<ul>
<li>Scales automatically with traffic.</li>
<li>Cost-efficient: pay only for actual requests.</li>
<li>Stateless logic simplifies deployment and updates.</li>
</ul>
</li>
</ul>
<p>This design ensures a <strong>truly serverless PHP application</strong> that handles session state, persistent data, and scalable workloads without the operational overhead of managing Redis or other caching layers.</p>
<h2>DynamoDB Session Handler and CSRF Implementation</h2>
<h3>Session Handler Implementation</h3>
<p>In a serverless PHP application, traditional session storage (files or local memory) doesn’t work because Lambda functions are <strong>ephemeral</strong>. Each invocation may run on a different container, so we need a centralized, persistent session store.</p>
<p>The core of our solution is a custom session handler that implements PHP's <code>SessionHandlerInterface</code>.</p>
<h4>How It Works</h4>
<ul>
<li><strong>Sessions are stored in DynamoDB</strong> instead of the filesystem.</li>
<li>Each session has a unique <code>session_id</code>, which becomes the partition key (<code>PK</code>) in DynamoDB.</li>
<li>Sessions include the serialized PHP session data and an <strong>expiration timestamp</strong> (TTL).</li>
<li>The handler automatically reads/writes session data on <code>session_start()</code> and <code>session_write_close()</code>.</li>
</ul>
<h4>Key Features</h4>
<ol>
<li><strong>Automatic Expiration</strong>
<ul>
<li>DynamoDB TTL ensures sessions are removed automatically after expiration.</li>
</ul>
</li>
<li><strong>Atomic Operations</strong>
<ul>
<li><code>PutItem</code> and <code>UpdateItem</code> guarantee consistent writes, even with concurrent requests.</li>
</ul>
</li>
<li><strong>Scalable</strong>
<ul>
<li>Can handle thousands of concurrent sessions without extra infrastructure.</li>
</ul>
</li>
<li><strong>Serverless-friendly</strong>
<ul>
<li>No local storage, no Redis, fully compatible with Lambda statelessness.</li>
</ul>
</li>
</ol>
<h4>Implementation</h4>
<pre><code class="language-php">&lt;?php

namespace App\Session;

use AsyncAws\DynamoDb\DynamoDbClient;
use AsyncAws\DynamoDb\Input\DeleteItemInput;
use AsyncAws\DynamoDb\Input\GetItemInput;
use AsyncAws\DynamoDb\Input\PutItemInput;
use AsyncAws\DynamoDb\ValueObject\AttributeValue;

/**
 * A minimal DynamoDB-backed PHP session handler using AsyncAws.
 *
 * Table design (single-table compatible):
 *  - PK: &quot;SESSION&quot;
 *  - SK: &quot;SID#&lt;session_id&gt;&quot;
 *  - data: base64-encoded session payload (string)
 *  - expiresAt: unix epoch seconds (number), enable DynamoDB TTL on this attribute
 *
 * Garbage collection is handled by DynamoDB&#039;s TTL, so gc() is a no-op.
 */
class DynamoDbSessionHandler implements \SessionHandlerInterface
{
    private const string PK_VALUE = &#039;SESSION&#039;;
    private const string SK_PREFIX = &#039;SID#&#039;;

    public function __construct(
        private readonly DynamoDbClient $dynamoDb,
        private readonly string $tableName,
        private readonly int $ttlSeconds = 3600,
    ) {}

    public function open(string $path, string $name): bool
    {
        // Nothing to do
        return true;
    }

    public function close(): bool
    {
        // Nothing to do
        return true;
    }

    public function read(string $id): string
    {
        $result = $this-&gt;dynamoDb-&gt;getItem(new GetItemInput([
            &#039;TableName&#039; =&gt; $this-&gt;tableName,
            &#039;Key&#039; =&gt; [
                &#039;PK&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; self::PK_VALUE]),
                &#039;SK&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; self::SK_PREFIX . $id]),
            ],
            // Strongly consistent read to reduce stale sessions
            &#039;ConsistentRead&#039; =&gt; true,
        ]));

        $item = $result-&gt;getItem();
        if (!$item || !isset($item[&#039;data&#039;])) {
            return &#039;&#039;;
        }

        $encoded = $item[&#039;data&#039;]-&gt;getS();
        if ($encoded === null) {
            return &#039;&#039;;
        }

        $payload = base64_decode($encoded, true);
        return $payload === false ? &#039;&#039; : $payload;
    }

    public function write(string $id, string $data): bool
    {
        $expiresAt = time() + $this-&gt;ttlSeconds;

        $this-&gt;dynamoDb-&gt;putItem(new PutItemInput([
            &#039;TableName&#039; =&gt; $this-&gt;tableName,
            &#039;Item&#039; =&gt; [
                &#039;PK&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; self::PK_VALUE]),
                &#039;SK&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; self::SK_PREFIX . $id]),
                &#039;data&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; base64_encode($data)]),
                &#039;expiresAt&#039; =&gt; new AttributeValue([&#039;N&#039; =&gt; (string) $expiresAt]),
            ],
        ]));

        return true;
    }

    public function destroy(string $id): bool
    {
        $this-&gt;dynamoDb-&gt;deleteItem(new DeleteItemInput([
            &#039;TableName&#039; =&gt; $this-&gt;tableName,
            &#039;Key&#039; =&gt; [
                &#039;PK&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; self::PK_VALUE]),
                &#039;SK&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; self::SK_PREFIX . $id]),
            ],
        ]));

        return true;
    }

    public function gc(int $max_lifetime): int|false
    {
        // Rely on DynamoDB TTL to expire items; nothing to scan/delete here.
        return 0;
    }
}</code></pre>
<h4>Why it matters?</h4>
<p>This approach:</p>
<ul>
<li>Keeps your PHP sessions serverless-compatible.</li>
<li>Avoids cold-start pitfalls associated with local or in-memory session storage.</li>
<li>Provides a reliable, scalable, and fully managed solution for stateful data in a stateless environment.</li>
</ul>
<h3>CSRF Token Storage</h3>
<p>In a serverless environment, CSRF tokens must be handled carefully. Because Lambda executions are stateless, tokens cannot be stored in memory or on the filesystem. Instead, CSRF tokens are persisted in DynamoDB alongside session data.</p>
<p>This approach ensures tokens remain valid and verifiable across multiple Lambda invocations.</p>
<h4>How CSRF Tokens Are Stored</h4>
<p>Each CSRF token is stored as a dedicated item in the DynamoDB table:</p>
<ul>
<li>Tokens are associated with a specific action</li>
<li>Each token has a unique identifier</li>
<li>An expiration timestamp is stored for automatic cleanup</li>
</ul>
<p>This makes CSRF token storage consistent, durable, and serverless-compatible.</p>
<h4>Data Model</h4>
<p>CSRF tokens follow the same single-table design pattern used elsewhere in the application.</p>
<table>
<thead>
<tr>
<th>Attribute</th>
<th>Value</th>
</tr>
</thead>
<tbody>
<tr>
<td>PK</td>
<td><code>CSRF</code></td>
</tr>
<tr>
<td>SK</td>
<td><code>TOKEN#&lt;token_id&gt;</code></td>
</tr>
<tr>
<td>session</td>
<td><code>&lt;session_id&gt;</code></td>
</tr>
<tr>
<td>expiresAt</td>
<td><code>&lt;timestamp&gt;</code></td>
</tr>
</tbody>
</table>
<p>Using a distinct partition key avoids contention and allows tokens to scale independently from session traffic.</p>
<h4>Lifecycle</h4>
<ol>
<li>A CSRF token is generated when a form is rendered.</li>
<li>The token is persisted in DynamoDB.</li>
<li>On form submission, the token is retrieved and validated.</li>
<li>After validation or expiration, the token is deleted or allowed to expire via TTL.</li>
</ol>
<p>This lifecycle mirrors traditional CSRF handling while remaining compatible with Lambda’s</p>
<h4>Implementation</h4>
<pre><code class="language-php">class DynamoDbCsrfTokenStorage implements CsrfTokenStorageInterface
{
    private const string PK_VALUE = &#039;CSRF&#039;;
    private const string SK_PREFIX = &#039;TOKEN#&#039;;

    public function getToken(string $tokenId): string
    {
        $result = $this-&gt;dynamoDb-&gt;getItem(new GetItemInput([
            &#039;TableName&#039; =&gt; $this-&gt;tableName,
            &#039;Key&#039; =&gt; [
                &#039;PK&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; self::PK_VALUE]),
                &#039;SK&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; self::SK_PREFIX . $tokenId]),
            ],
        ]));

        $item = $result-&gt;getItem();
        return $item[&#039;value&#039;]-&gt;getS() ?? &#039;&#039;;
    }

    public function setToken(string $tokenId, string $token): void
    {
        $expiresAt = time() + $this-&gt;ttlSeconds;

        $this-&gt;dynamoDb-&gt;putItem(new PutItemInput([
            &#039;TableName&#039; =&gt; $this-&gt;tableName,
            &#039;Item&#039; =&gt; [
                &#039;PK&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; self::PK_VALUE]),
                &#039;SK&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; self::SK_PREFIX . $tokenId]),
                &#039;value&#039; =&gt; new AttributeValue([&#039;S&#039; =&gt; $token]),
                &#039;expiresAt&#039; =&gt; new AttributeValue([&#039;N&#039; =&gt; (string) $expiresAt]),
            ],
        ]));
    }
}</code></pre>
<p>This ensures CSRF protection works seamlessly across multiple Lambda invocations.</p>
<h2>Symfony Configuration</h2>
<p>Configuring Symfony correctly is key for serverless PHP apps to work reliably with Lambda, DynamoDB, and Bref. Here’s how we set it up.</p>
<h3>1. Session Storage</h3>
<p>We replace the default PHP session handler with our <strong>DynamoDBSessionHandler</strong>:</p>
<pre><code class="language-yaml"># config/packages/framework.yaml
framework:
    session:
        handler_id: App\Session\DynamoDBSessionHandler
        cookie_secure: auto
        cookie_samesite: lax
        cookie_lifetime: 3600  # 1 hour</code></pre>
<p>Notes:</p>
<ul>
<li><code>handler_id</code> points to our custom service.</li>
<li><code>cookie_secure: auto</code> ensures HTTPS enforcement on Lambda URLs or custom domains.</li>
<li><code>cookie_lifetime</code> aligns with DynamoDB TTL for consistency.</li>
</ul>
<h3>2. Service definition</h3>
<p>Register the DynamoDB session handler as a Symfony service:</p>
<pre><code class="language-yaml"># config/services.yaml
services:
  App\Session\DynamoDbSessionHandler:
    arguments:
      $tableName: &#039;%book_table_name%&#039;
      $ttlSeconds: &#039;%env(default:session_ttl_seconds:int:SESSION_TTL)%&#039;</code></pre>
<ul>
<li><code>$tableName</code> comes from environment variables to support multiple environments.</li>
<li><code>$ttl</code> matches the session lifetime for automatic garbage collection.<br />
This configuration tells Symfony to use our custom handler for all session operations. The handler is automatically injected with the DynamoDB client through Symfony's autowiring.</li>
</ul>
<h3>3. RequestContextListener</h3>
<p>To handle dynamic Lambda Function URLs, we register a listener:</p>
<pre><code class="language-yaml"># config/services.yaml
services:
    App\EventListener\RequestContextListener:
        tags:
            - { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }</code></pre>
<p>Purpose:</p>
<ul>
<li>Ensures Symfony’s URL generator produces correct URLs.</li>
<li>Sets proper scheme and host for redirects, forms, and CSRF validation.</li>
<li>Essential for Lambda Function URLs where host/scheme changes per invocation.</li>
</ul>
<h4>Why It’s Needed</h4>
<p>Lambda Function URLs:</p>
<ul>
<li>Provide a direct HTTPS endpoint (e.g., <code>https://xyz.lambda-url.us-east-1.on.aws/</code>)</li>
<li>Are <strong>dynamic</strong> and unknown at build time</li>
<li>Require Symfony to know the <strong>scheme and host</strong> at runtime to generate correct URLs</li>
</ul>
<p>Without a listener:</p>
<ul>
<li>Redirects may point to HTTP instead of HTTPS</li>
<li>CSRF tokens may fail</li>
<li>Session cookies might be rejected</li>
<li>OAuth or SSO integrations could break</li>
</ul>
<h4>Implementation</h4>
<pre><code class="language-php">&lt;?php

namespace App\EventListener;

use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\Routing\RequestContext;

#[AsEventListener(event: KernelEvents::REQUEST, priority: 1024)]
class RequestContextListener
{
    public function __construct(private RequestContext $requestContext) {}

    public function onKernelRequest(RequestEvent $event): void
    {
        if (!$event-&gt;isMainRequest()) {
            return;
        }

        $request = $event-&gt;getRequest();

        // Force HTTPS for Lambda URLs and set proper context
        if ($request-&gt;headers-&gt;get(&#039;host&#039;) &amp;&amp; str_contains($request-&gt;headers-&gt;get(&#039;host&#039;), &#039;lambda-url&#039;)) {
            $this-&gt;requestContext-&gt;setScheme(&#039;https&#039;);
            $this-&gt;requestContext-&gt;setHost($request-&gt;headers-&gt;get(&#039;host&#039;));
            $this-&gt;requestContext-&gt;setHttpPort(80);
            $this-&gt;requestContext-&gt;setHttpsPort(443);

            $request-&gt;server-&gt;set(&#039;HTTPS&#039;, &#039;on&#039;);
            $request-&gt;server-&gt;set(&#039;SERVER_PORT&#039;, 443);
            $request-&gt;server-&gt;set(&#039;REQUEST_SCHEME&#039;, &#039;https&#039;);
        }
    }
}</code></pre>
<p><strong>The CDK Output Dilemma:</strong></p>
<pre><code class="language-typescript">// CDK can output the Lambda URL after deployment
new CfnOutput(this, &#039;LambdaURL&#039;, { 
    value: statelessStack.monolithLambdaFunctionUrl.url 
});
// But this value is only known AFTER deployment completes
// You can&#039;t use it as an environment variable in the SAME deployment</code></pre>
<p><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Tip: This listener is only necessary if you use the Lambda Function URL as the production endpoint. If you use a custom domain, this can be simplified or skipped.</p>
<p>With this configuration, Symfony becomes serverless-ready, maintaining sessions, CSRF protection, and routing behavior seamlessly while leveraging DynamoDB and Lambda.</p>
<h2>Single-Table Design Pattern</h2>
<p>All entities (sessions, CSRF tokens, users, books, authors) live in a <strong>single DynamoDB table</strong>. This simplifies the architecture and enables atomic operations across different entities.</p>
<table>
<thead>
<tr>
<th>Entity</th>
<th>PK</th>
<th>SK</th>
</tr>
</thead>
<tbody>
<tr>
<td>Session</td>
<td><code>SESSION</code></td>
<td><code>SID#&lt;session_id&gt;</code></td>
</tr>
<tr>
<td>CSRF Token</td>
<td><code>CSRF</code></td>
<td><code>TOKEN#&lt;token_id&gt;</code></td>
</tr>
<tr>
<td>Book</td>
<td><code>BOOK-METADATA</code></td>
<td><code>AUTHOR#&lt;author_id&gt;#BOOK#&lt;book_id&gt;</code></td>
</tr>
<tr>
<td>Author</td>
<td><code>AUTHOR-METADATA</code></td>
<td><code>AUTHOR#&lt;author_id&gt;</code></td>
</tr>
<tr>
<td>User</td>
<td><code>USER</code></td>
<td><code>EMAIL#&lt;email&gt;</code></td>
</tr>
</tbody>
</table>
<ul>
<li><strong>Why single-table?</strong>
<ul>
<li>Reduces infrastructure complexity.</li>
<li>Simplifies monitoring and backup.</li>
<li>Supports atomic transactions across multiple entity types.</li>
<li>Aligns with AWS best practices for DynamoDB.</li>
</ul>
</li>
</ul>
<h2>AWS CDK Infrastructure with Bref</h2>
<p>Deploying a serverless Symfony app requires some AWS setup. Using <strong>AWS CDK</strong> with <strong>Bref</strong> makes this smooth, maintainable, and repeatable.</p>
<h3>Why CDK?</h3>
<ul>
<li><strong>Infrastructure as code</strong>: Everything is versioned and reproducible.</li>
<li><strong>Integration with Symfony</strong>: Easy to link environment variables, DynamoDB, and Lambda functions.</li>
<li><strong>Bref-friendly</strong>: Deploy PHP Lambda layers without manually configuring Lambda functions.</li>
</ul>
<h3>Stateful Stack: DynamoDB Table</h3>
<pre><code class="language-ts">import { NestedStack } from &quot;aws-cdk-lib&quot;;
import * as ddb from &quot;aws-cdk-lib/aws-dynamodb&quot;;

export class BlogAppStatefulStack extends NestedStack {
  public readonly ddb: ddb.Table;

  constructor(scope: Construct, id: string, props: MyNestedStackProps) {
    super(scope, id, props);

    this.ddb = new ddb.Table(this, &#039;ddb&#039;, {
      tableName: `${id}-table`,
      partitionKey: { name: &#039;PK&#039;, type: ddb.AttributeType.STRING },
      sortKey: { name: &#039;SK&#039;, type: ddb.AttributeType.STRING },
      billingMode: ddb.BillingMode.PAY_PER_REQUEST,
      deletionProtection: props.shared.environment === &#039;prod&#039;,
      timeToLiveAttribute: &#039;expiresAt&#039;,
    });
  }
}</code></pre>
<p>Key features:</p>
<ul>
<li><strong>Generic Key Schema</strong>: <code>PK</code> and <code>SK</code> enable single-table design</li>
<li><strong>TTL Enabled</strong>: <code>expiresAt</code> attribute automatically removes expired items</li>
<li><strong>Production Protection</strong>: Deletion protection enabled for production environments</li>
</ul>
<h3>Stateless Stack: Lambda Function with Bref</h3>
<pre><code class="language-ts">import { packagePhpCode, PhpFpmFunction } from &quot;@bref.sh/constructs&quot;;
import * as lambda from &quot;aws-cdk-lib/aws-lambda&quot;;
import { FunctionUrl } from &quot;aws-cdk-lib/aws-lambda&quot;;

export class BlogAppStatelessStack extends NestedStack {
  public readonly monolithLambda: PhpFpmFunction;
  public monolithLambdaFunctionUrl: FunctionUrl;

  private createLambda(props: MyNestedStackProps, staticAssetsBucket: Bucket, ddb: ddb.Table) {
    const lambdaEnvironment = {
      APP_ENV: props.shared.environment,
      APP_SECRET: appSecret,
      ASSET_URL: `https://${staticAssetsBucket.bucketDomainName}/`,
      AWS_LAMBDA_LOG_FORMAT: &#039;text&#039;,
      BOOK_TABLE_NAME: `${props.shared.stackPrefix}-StatefulStack-table`,
    };

    const monolithLambda = new PhpFpmFunction(this, &#039;App&#039;, {
      handler: &#039;public/index.php&#039;,
      phpVersion: &#039;8.4&#039;,
      code: packagePhpCode(&#039;php&#039;, {
        exclude: [&#039;.env.local&#039;, &#039;bin/&#039;],
      }),
      functionName: `${props.shared.stackPrefix}-App`,
      timeout: Duration.seconds(28),
      memorySize: Size.gibibytes(2).toMebibytes(),
      environment: lambdaEnvironment,
    });

    // Create Function URL with no authentication
    const monolithLambdaFunctionUrl = monolithLambda.addFunctionUrl({ 
      authType: lambda.FunctionUrlAuthType.NONE 
    });

    // Grant DynamoDB permissions
    ddb.grantReadWriteData(monolithLambda);

    return { monolithLambda, monolithLambdaFunctionUrl };
  }
}</code></pre>
<h3>Lambda Function URL Configuration</h3>
<p>Lambda Function URLs provide a simple HTTPS endpoint without needing API Gateway:</p>
<pre><code class="language-ts">const monolithLambdaFunctionUrl = monolithLambda.addFunctionUrl({ 
  authType: lambda.FunctionUrlAuthType.NONE 
});</code></pre>
<p><strong>Benefits of Lambda URLs:</strong></p>
<ul>
<li><strong>Simplicity</strong>: Direct HTTPS endpoint without API Gateway complexity</li>
<li><strong>Cost</strong>: No API Gateway charges</li>
<li><strong>Performance</strong>: One less hop in the request path</li>
<li><strong>Built-in HTTPS</strong>: Automatic TLS certificate management</li>
</ul>
<p><strong>Configuration Options:</strong></p>
<ul>
<li><code>authType: NONE</code>: Public access (suitable for web applications)</li>
<li><code>authType: AWS_IAM</code>: Requires AWS signature (for service-to-service communication)</li>
</ul>
<h3>Main Stack: Orchestration</h3>
<pre><code class="language-ts">export class BlogApp extends Stack {
  constructor(scope: Construct, id: string, props: MyStackProps) {
    super(scope, id, props);

    const stackPrefix = props.shared.envStackPrefix;

    const statefulStack = new BlogAppStatefulStack(
      this, `${stackPrefix}-StatefulStack`, props
    );

    const statelessStack = new BlogAppStatelessStack(
      this, `${stackPrefix}-StatelessStack`, props, statefulStack
    );

    // Output important values
    new CfnOutput(this, &#039;Lambda&#039;, { 
      value: statelessStack.monolithLambda.functionName 
    });
    new CfnOutput(this, &#039;LambdaURL&#039;, { 
      value: statelessStack.monolithLambdaFunctionUrl.url 
    });
    new CfnOutput(this, &#039;DynamoDb&#039;, { 
      value: statefulStack.ddb.tableName 
    });
  }
}</code></pre>
<h3>Deployment with CDK</h3>
<p>With the infrastructure defined, deploying the application becomes a repeatable and predictable process. This section focuses on <strong>how the application is built, deployed, and updated</strong> using AWS CDK.</p>
<h4>Local Development Environment</h4>
<p>Local development mirrors the production setup as closely as possible while remaining lightweight.</p>
<ul>
<li>Docker is used to provide a consistent PHP environment.</li>
<li>A Makefile abstracts common commands to reduce cognitive load.</li>
<li>Symfony runs locally with the same session and configuration logic used in Lambda.</li>
</ul>
<p>You can run:</p>
<pre><code class="language-bash"># Pre-requisite - source your aws profile
make up</code></pre>
<p>You can check logs via <code>make logs</code>. And get into the container with <code>make bash</code>. The application will be available at <code>http://localhost:8000</code>, but it might fail to load as there is no existent DynamoDB to connect with. You can check local <code>.env</code> file for environment variables.</p>
<h4>Deploying</h4>
<p>Deploy the application using standard CDK commands (inside the container):</p>
<pre><code class="language-bash"># Pre-requisite - Bootstrap CDK if this is your first deployment - npx cdk bootstrap aws://&lt;ACCOUNT_ID&gt;/&lt;REGION&gt;
# Install dependencies
npm run deploy</code></pre>
<p>Alternatively, you can use the <code>Makefile</code> command outsite the container:</p>
<pre><code class="language-bash">make deploy</code></pre>
<h4>What Gets Created</h4>
<p>The deployment creates:</p>
<ol>
<li>DynamoDB table with TTL enabled</li>
<li>Lambda function with PHP 8.4 runtime (via Bref)</li>
<li>Lambda Function URL for HTTPS access</li>
<li>S3 bucket for static assets</li>
<li>IAM roles and permissions</li>
</ol>
<p>The output should be similar to:</p>
<pre><code class="language-bash">BlogApp (sandbox-blog-app): deploying... [1/1]
sandbox-blog-app: creating CloudFormation changeset...

 &#x2705;  BlogApp (sandbox-blog-app)

&#x2728;  Deployment time: 148.76s

Outputs:
BlogApp.AssetsBucket = sandbox-blog-app-sandboxbloga-assetsbucket5cb76180-5lu45xsqvuym
BlogApp.DynamoDb = sandbox-BlogApp-StatefulStack-table
BlogApp.Lambda = sandbox-BlogApp-App
BlogApp.LambdaURL = https://kiv7utcwku6gihqgs4bfkeuzma0oaylo.lambda-url.us-east-1.on.aws/
Stack ARN:
arn:aws:cloudformation:us-east-1:973974862728:stack/sandbox-blog-app/9ba92580-e50b-11f0-a602-0afffb8dc1a9

&#x2728;  Total time: 163.03s</code></pre>
<p>In this case, <code>https://kiv7utcwku6gihqgs4bfkeuzma0oaylo.lambda-url.us-east-1.on.aws/</code> is the Lambda public URL.</p>
<p>When you access the URL, you will see a log-in form. You can use the &quot;Register&quot; link to create a login. Use it and you will be able to manage Authors and Books. Try to log out and access the pages directly.</p>
<p><img data-recalc-dims="1" decoding="async" src="https://i0.wp.com/rafael.bernard-araujo.com/wp-content/uploads/2025/12/301225-1.png?w=580&#038;ssl=1" alt="Login" /></p>
<p><img data-recalc-dims="1" decoding="async" src="https://i0.wp.com/rafael.bernard-araujo.com/wp-content/uploads/2025/12/301225-2.png?w=580&#038;ssl=1" alt="Register" /></p>
<p><img data-recalc-dims="1" decoding="async" src="https://i0.wp.com/rafael.bernard-araujo.com/wp-content/uploads/2025/12/301225-3.png?w=580&#038;ssl=1" alt="Main" /></p>
<p>Internally it will execute a series of commands:</p>
<pre><code class="language-bash"># clean
npm run clean &amp;&amp; \
# execute php packaging including composer install and npm build for symfony
npm run package:sandbox &amp;&amp; \ 
# deploy as a sandbox not requiring approval
NODE_ENV=sandbox cdk deploy --require-approval never</code></pre>
<p>There is a prod version executing <code>make deploy:prod</code>.</p>
<h3>Testing the Session Implementation</h3>
<p>The application includes a test endpoint to verify session persistence:</p>
<pre><code class="language-php">#[Route(&#039;/session-test&#039;, name: &#039;session_test&#039;)]
public function test(Request $request): JsonResponse
{
    $session = $request-&gt;getSession();
    $counter = $session-&gt;get(&#039;counter&#039;, 0);
    $session-&gt;set(&#039;counter&#039;, $counter + 1);

    return new JsonResponse([
        &#039;message&#039; =&gt; &#039;Session test&#039;,
        &#039;session_id&#039; =&gt; $session-&gt;getId(),
        &#039;counter&#039; =&gt; $session-&gt;get(&#039;counter&#039;),
        &#039;handler&#039; =&gt; get_class($session-&gt;getMetadataBag()-&gt;getMetadata(&#039;handler&#039;)),
    ]);
}</code></pre>
<p>Test with curl:</p>
<pre><code class="language-bash"># First request creates session
curl -i -c cookie.txt https://your-lambda-url/session-test

# Subsequent requests increment counter
curl -i -b cookie.txt https://your-lambda-url/session-test
curl -i -b cookie.txt https://your-lambda-url/session-test

# outputs
➜ curl -i -c cookie.txt https://your-lambda-url/session-test
{&quot;message&quot;:&quot;Session incremented&quot;,&quot;session_id&quot;:&quot;02b0c08e1ccd5f3ea015a06c69e29d11&quot;,&quot;counter&quot;:1,&quot;handler&quot;:&quot;App\\Session\\DynamoDbSessionHandler&quot;}%

➜ curl -i -b cookie.txt https://your-lambda-url/session-test
{&quot;message&quot;:&quot;Session incremented&quot;,&quot;session_id&quot;:&quot;02b0c08e1ccd5f3ea015a06c69e29d11&quot;,&quot;counter&quot;:2,&quot;handler&quot;:&quot;App\\Session\\DynamoDbSessionHandler&quot;}%

➜ curl -i -b cookie.txt https://your-lambda-url/session-test
{&quot;message&quot;:&quot;Session incremented&quot;,&quot;session_id&quot;:&quot;02b0c08e1ccd5f3ea015a06c69e29d11&quot;,&quot;counter&quot;:3,&quot;handler&quot;:&quot;App\\Session\\DynamoDbSessionHandler&quot;}%</code></pre>
<h2>Performance Considerations</h2>
<h3>Cold Start Optimization</h3>
<ol>
<li><strong>Memory Allocation</strong>: Using 2GB memory reduces cold start times</li>
<li><strong>Composer Optimization</strong>: <code>--no-dev --optimize-autoloader</code> reduces code size</li>
<li><strong>PHP 8.4</strong>: Latest PHP version with JIT compiler support</li>
</ol>
<h3>DynamoDB Performance</h3>
<ol>
<li><strong>Consistent Reads</strong>: Ensures session consistency at the cost of slightly higher latency</li>
<li><strong>On-Demand Billing</strong>: No capacity planning, automatic scaling</li>
<li><strong>TTL</strong>: Automatic cleanup without scan operations</li>
</ol>
<p>The serverless model's primary advantage is alignment of costs with actual usage, particularly beneficial for applications with variable or unpredictable traffic patterns. However, actual costs vary significantly based on traffic patterns, request complexity, and specific use cases. It's recommended to use AWS cost estimation tools and monitor actual usage to understand the financial impact for your specific application.</p>
<h2>Security Best Practices</h2>
<h3>Session Security</h3>
<ol>
<li><strong>Secure Flag</strong>: Ensures cookies only sent over HTTPS</li>
<li><strong>SameSite</strong>: Protects against CSRF attacks</li>
<li><strong>Regenerate ID</strong>: After authentication to prevent session fixation</li>
</ol>
<pre><code class="language-yaml">framework:
    session:
        cookie_httponly: true
        cookie_secure: auto
        cookie_samesite: lax</code></pre>
<h3>DynamoDB Permissions</h3>
<p>The Lambda function requires minimal permissions:</p>
<pre><code class="language-typescript">ddb.grantReadWriteData(monolithLambda);</code></pre>
<p>This grants only:</p>
<ul>
<li><code>dynamodb:GetItem</code></li>
<li><code>dynamodb:PutItem</code></li>
<li><code>dynamodb:DeleteItem</code></li>
<li><code>dynamodb:Query</code></li>
<li><code>dynamodb:Scan</code></li>
</ul>
<p>No administrative permissions are granted to the Lambda function.</p>
<h2>Limitations</h2>
<p>While serverless PHP with DynamoDB sessions offers compelling advantages, it's important to understand the limitations and trade-offs. Here's an honest assessment of where this architecture may not be the best fit:</p>
<h3>1. Cold Start Latency</h3>
<p><strong>The Reality</strong>: Lambda cold starts can add <strong>1-3 seconds</strong> to the first request after a function has been idle. In practice this occurs for less than 1% of the calls.</p>
<p><strong>Mitigation Strategies</strong>:</p>
<ul>
<li><strong>Provisioned Concurrency</strong>: Pre-warm Lambda instances to eliminate cold starts (adds ~$15/month per instance)</li>
<li><strong>Keep-Warm Pings</strong>: Use CloudWatch Events to invoke functions every 5-10 minutes (adds minimal cost but doesn't help with scaling)</li>
<li><strong>Larger Memory Allocation</strong>: We use 2GB memory which provides faster CPUs, reducing cold start duration</li>
<li><strong>Optimize Code</strong>: Minimize dependencies, use PHP preloading, optimize autoloader</li>
</ul>
<p><strong>When it's acceptable</strong>: Background jobs, internal tools, APIs with relaxed SLAs<br />
<strong>When it's problematic</strong>: User-facing e-commerce, real-time chat, gaming applications</p>
<h3>2. Request Timeout Constraints</h3>
<p><strong>The Reality</strong>: Our configuration uses <strong>28 seconds timeout</strong> (API Gateway compatible), though Lambda supports up to <strong>15 minutes</strong>, which Lambda URLs supports.</p>
<p><strong>Not Suitable For</strong>:</p>
<ul>
<li><strong>Long-running batch jobs</strong>: Data exports, report generation, video processing</li>
<li><strong>Large file uploads</strong>: Direct file uploads over 10MB become unreliable</li>
<li><strong>Complex data migrations</strong>: Multi-step transformations requiring minutes to complete</li>
<li><strong>WebSocket connections</strong>: Not supported by Lambda Function URLs (use API Gateway WebSocket instead)</li>
</ul>
<p><strong>Recommended Alternatives</strong>:</p>
<ul>
<li><strong>Keep Lambda URL</strong>: If API Gateway specific features are not needed, we can use custom domain with Lambda URLs and process up to 15 minutes</li>
<li><strong>AWS Step Functions</strong>: Orchestrate long-running workflows across multiple Lambda invocations</li>
<li><strong>ECS/Fargate</strong>: For truly long-running processes (hours), use containers instead</li>
<li><strong>Presigned S3 URLs</strong>: For large file uploads, let clients upload directly to S3</li>
<li><strong>SQS + Background Workers</strong>: Offload heavy processing to asynchronous queues</li>
</ul>
<h3>3. Session Consistency Edge Cases</h3>
<p><strong>The Reality</strong>: DynamoDB is eventually consistent by default, but we use <code>ConsistentRead: true</code> to mitigate this.</p>
<p><strong>Why We Use ConsistentRead</strong>:</p>
<pre><code class="language-php">&#039;ConsistentRead&#039; =&gt; true,  // Ensures we always get the latest session data</code></pre>
<p><strong>Rare Race Conditions</strong>:<br />
Even with consistent reads, race conditions can occur when:</p>
<ul>
<li><strong>Simultaneous Writes</strong>: User opens multiple tabs, both modify session simultaneously—last write wins</li>
<li><strong>Write-then-Read Timing</strong>: Session written in one Lambda, immediately read by another—minimal delay possible</li>
<li><strong>Cross-Region Scenarios</strong>: If using Global Tables, replication lag can cause stale reads in remote regions</li>
</ul>
<p><strong>Practical Impact</strong>: In 99.9% of cases, consistent reads solve the problem. Edge cases typically affect power users opening many tabs or distributed teams across continents.</p>
<p><strong>Mitigation</strong>: For critical operations (e.g., payment processing), use DynamoDB conditional expressions to ensure atomic updates and detect conflicts.</p>
<h3>4. DynamoDB Costs at Scale</h3>
<p>DynamoDB's pay-per-request pricing is cost-effective at low-to-moderate traffic but pricing characteristics change at high scale.</p>
<h4>Assumptions</h4>
<p><strong>DynamoDB</strong>:</p>
<ul>
<li>On-demand billing: $1.25 per million reads/writes</li>
<li>1KB session item size</li>
<li>1 read + 1 write per request</li>
</ul>
<p><strong>Redis (ElastiCache)</strong>:</p>
<ul>
<li>t4g.medium: $0.037/hr (~$27/month)</li>
<li>1 node sufficient for low-medium traffic</li>
<li>High traffic may require bigger node(s)</li>
</ul>
<h4>Cost Table</h4>
<table>
<thead>
<tr>
<th>Traffic</th>
<th>Requests / Month</th>
<th>DynamoDB Cost</th>
<th>Redis Cost</th>
<th>Notes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Low</td>
<td>1M</td>
<td>$2.50</td>
<td>$27</td>
<td>DynamoDB far cheaper at low traffic</td>
</tr>
<tr>
<td>Medium</td>
<td>10M</td>
<td>$25</td>
<td>$27</td>
<td>Costs roughly similar; DynamoDB slightly lower ops</td>
</tr>
<tr>
<td>High</td>
<td>50M</td>
<td>$125</td>
<td>$108 (cache.m5.large 3 nodes)</td>
<td>Redis may become cheaper with large, sustained traffic, but ops complexity rises</td>
</tr>
</tbody>
</table>
<p><strong>When Fixed Infrastructure (like Redis/ElastiCache) May Become More Cost-Effective</strong>:</p>
<ul>
<li>Sustained high traffic volumes where fixed costs are fully utilized</li>
<li>Long-lived sessions with more reads than writes</li>
<li>Advanced caching features needed beyond simple session storage</li>
</ul>
<p><strong>Hidden DynamoDB Cost Factors</strong>:</p>
<ul>
<li>Consistent reads cost more than eventually consistent reads</li>
<li>Session writes on every request (even if session data unchanged)</li>
<li>AWS free tier limitations after 12 months</li>
</ul>
<p>Start with DynamoDB for simplicity and operational efficiency. Monitor costs monthly as traffic grows. If costs become a concern at high scale, evaluate whether fixed infrastructure or caching optimizations make sense for your specific use case.</p>
<hr />
<p>These limitations are not dealbreakers but they're <strong>trade-offs</strong>. For the right use cases (bursty traffic, cost-sensitive, minimal ops), the benefits far outweigh the drawbacks.</p>
<h2>Conclusion</h2>
<p>Building serverless PHP applications doesn't require sacrificing familiar frameworks or patterns. By implementing a custom DynamoDB session handler, we achieve:</p>
<ul>
<li><strong>Truly serverless architecture</strong>: No Redis, no EFS, pure AWS managed services</li>
<li><strong>Production-ready session management</strong>: Consistent, scalable, and secure</li>
<li><strong>Cost-effective</strong>: Pay only for actual usage</li>
<li><strong>Developer-friendly</strong>: Standard Symfony application with minimal modifications</li>
<li><strong>Type-safe infrastructure</strong>: AWS CDK with TypeScript</li>
<li><strong>Modern PHP</strong>: PHP 8.4 with all latest features</li>
<li><strong>Local development</strong>: Docker-compose for local testing</li>
</ul>
<p>The combination of Bref for Lambda PHP support, Symfony for application framework, and DynamoDB for stateful storage creates a robust, scalable, and maintainable serverless application architecture.</p>
<h3>When Should You Use This Architecture?</h3>
<p><strong>Choose this approach when:</strong></p>
<ul>
<li>Traffic is unpredictable or bursty (blogs, seasonal apps, internal tools)</li>
<li>Cost optimization matters more than absolute performance</li>
<li>Zero operational overhead is a priority</li>
<li>You need automatic scaling without capacity planning</li>
</ul>
<p>Common use cases are:</p>
<ul>
<li>CMS - Blogs, documentation sites, and knowledge bases with infrequent or sporadic traffic, when sudden spikes are scaled automatically and quite periods costs pennies</li>
<li>Admin Panels and Internal Tools - Dashboard interfaces, internal reporting tools, and back-office applications with sporadic usage patterns. DynamoDB maintains session state without requiring Redis or similar infrastructure.</li>
<li>Multi-Tenant SaaS Applications - B2B platforms where each tenant has independent traffic patterns. DynamoDB's single-table design efficiently manages sessions across all tenants without cross-tenant interference.</li>
<li>API Services with Session Requirements - REST APIs that need stateful operations like OAuth flows, multi-step workflows, or temporary data caching. No Redis clusters to maintain, no session cleanup cron jobs to manage. DynamoDB TTL handles everything automatically.</li>
<li>Seasonal Applications - Event registration systems, holiday campaign sites, tax filing applications, and other time-bound services.</li>
<li>Microservices Requiring Session State - Distributed systems where individual services need temporary state management across invocations.</li>
</ul>
<p><strong>Consider alternatives when:</strong></p>
<ul>
<li>You require consistent sub-100ms response times</li>
<li>Traffic is predictable and sustained at high levels (&gt;10M requests/month)</li>
<li>Long-running processes or WebSocket connections are needed</li>
</ul>
<h3>The Bigger Picture</h3>
<p>This implementation demonstrates that <strong>serverless and stateful aren't mutually exclusive</strong>. While serverless advocates often emphasize &quot;stateless functions,&quot; real-world applications need state management. The key is choosing the right state storage mechanism, and DynamoDB proves that managed, serverless databases can handle session management as effectively as traditional infrastructure, with far less operational burden.</p>
<p>Whether you're building a content management system, an internal admin panel, or a multi-tenant SaaS application, this architecture provides a production-ready foundation. Start simple, monitor costs and performance, and scale confidently knowing your infrastructure will grow with your application without requiring a dedicated ops team.</p>
<h2>Resources</h2>
<ul>
<li><a href="https://bref.sh/">Bref Documentation</a></li>
<li><a href="https://github.com/brefphp/constructs">Bref CDK Constructs</a></li>
<li><a href="https://async-aws.com/clients/dynamodb.html">AsyncAws DynamoDB Client</a></li>
<li><a href="https://symfony.com/doc/current/session.html">Symfony Session Documentation</a></li>
<li><a href="https://docs.aws.amazon.com/lambda/latest/dg/lambda-urls.html">Lambda Function URLs</a></li>
<li><a href="https://www.alexdebrie.com/posts/dynamodb-single-table/">DynamoDB Single-Table Design</a></li>
</ul>
<h2>Source Code</h2>
<p>The complete source code for this application is available at: <a href="https://github.com/rafaelbernard/serverless-php-with-bref-symfony-and-dynamodb-session-management/">rafaelbernard/serverless-php-with-bref-symfony-and-dynamodb-session-management/</a></p>
<p>For detailed technical implementation notes, test coverage reports, and deployment validation, see <a href="https://github.com/rafaelbernard/serverless-php-with-bref-symfony-and-dynamodb-session-management/blob/master/IMPLEMENTATION_SUMMARY.md"><code>IMPLEMENTATION_SUMMARY.md</code></a> in the repository. This document covers:</p>
<ul>
<li>Complete test suite (112 tests across PHP and CDK)</li>
<li>Infrastructure validation details</li>
<li>Code quality metrics</li>
<li>Deployment procedures and best practices</li>
</ul>
<h3><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/1f4a1.png" alt="💡" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Bonus: Guide to Custom Domain Configuration with Route53</h3>
<p>Lambda Function URLs provide a quick way to expose your Lambda function over HTTPS, but the auto-generated URL (e.g., <code>https://abc123xyz.lambda-url.us-east-1.on.aws/</code>) isn't branded or memorable. But you can add a Simple CNAME Mapping: Direct Route53 CNAME to Lambda Function URL (easiest, limited SSL control).</p>
<p>This is the <strong>quickest and easiest</strong> method. Just create a CNAME record pointing to your Lambda Function URL. Best for internal tools, prototypes, and non-production environments.</p>
<h4>Prerequisites</h4>
<p>Before configuring custom domains, ensure you have:</p>
<ol>
<li><strong>Domain registered in Route53</strong> (or another registrar with ability to update nameservers)</li>
<li><strong>Hosted Zone created in Route53</strong> for your domain</li>
</ol>
<h4>Implementation with CDK</h4>
<p>Here's how to add a custom domain CNAME record pointing to your Lambda Function URL using AWS CDK:</p>
<pre><code class="language-typescript">import * as route53 from &#039;aws-cdk-lib/aws-route53&#039;;
import * as route53Targets from &#039;aws-cdk-lib/aws-route53-targets&#039;;
import * as lambda from &#039;aws-cdk-lib/aws-lambda&#039;;
import { Construct } from &#039;constructs&#039;;

// Assuming you have a Lambda function with Function URL enabled
const myFunction = new lambda.Function(this, &#039;MyFunction&#039;, {
  // ... function configuration
  functionUrlOptions: {
    authType: lambda.FunctionUrlAuthType.NONE, // or AWS_IAM
  },
});

// Get the hosted zone for your domain
const hostedZone = route53.HostedZone.fromLookup(this, &#039;HostedZone&#039;, {
  domainName: &#039;yourdomain.com&#039;,
});

// Create CNAME record pointing to Lambda URL
new route53.CnameRecord(this, &#039;LambdaUrlCname&#039;, {
  zone: hostedZone,
  recordName: &#039;api&#039;, // Creates api.yourdomain.com
  domainName: cdk.Fn.parseDomainName(myFunction.functionUrl), // Extracts hostname from URL
  ttl: cdk.Duration.minutes(5),
  comment: &#039;CNAME to Lambda Function URL&#039;,
});

// Output the custom domain
new cdk.CfnOutput(this, &#039;CustomDomainUrl&#039;, {
  value: `https://api.yourdomain.com`,
  description: &#039;Custom domain URL for Lambda function&#039;,
});</code></pre>
<h4>Testing Your CNAME Setup</h4>
<p>After creating the CNAME record, verify it works:</p>
<pre><code class="language-bash"># Check DNS propagation
dig api.yourdomain.com

# Test the endpoint
curl -i https://api.yourdomain.com/

# Verify SSL certificate
openssl s_client -connect api.yourdomain.com:443 -servername api.yourdomain.com | grep subject</code></pre>
<p><strong>Expected Results</strong>:</p>
<ul>
<li>DNS query returns Lambda Function URL hostname as CNAME target</li>
<li>HTTP request succeeds with same response as Lambda URL</li>
<li>SSL certificate shows AWS-managed certificate (not your custom domain)</li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://rafael.bernard-araujo.com/building-a-serverless-php-application-with-bref-symfony-and-dynamodb-session-management.php/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2283</post-id>	</item>
		<item>
		<title>Principles in Refactoring &#8211; Slowing Down New Features?</title>
		<link>https://rafael.bernard-araujo.com/principles-in-refactoring-slowing-down-new-features.php</link>
					<comments>https://rafael.bernard-araujo.com/principles-in-refactoring-slowing-down-new-features.php#respond</comments>
		
		<dc:creator><![CDATA[rafael]]></dc:creator>
		<pubDate>Wed, 22 Oct 2025 02:59:39 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[software engineering]]></category>
		<guid isPermaLink="false">https://rafael.bernard-araujo.com/?p=2252</guid>

					<description><![CDATA[The whole purpose of refactoring is to make us program faster, producing more value with less effort. and But I think the most dangerous way that people get trapped is when they try to justify refactoring in terms of &#34;clean code&#34;, &#34;good engineering practice&#34;, or similar moral reasons. The point of refactoring isn't to show [&#8230;]]]></description>
										<content:encoded><![CDATA[<blockquote>
<p>The whole purpose of refactoring is to make us program faster, producing more value with less effort.</p>
</blockquote>
<p>and</p>
<blockquote>
<p>But I think the most dangerous way that people get trapped is when they try to justify refactoring in terms of &quot;clean code&quot;, &quot;good engineering practice&quot;, or similar moral reasons. The point of refactoring isn't to show how sparkly a code base is -- it is purely economic. We refactor because it makes us faster -- fastor add features, faster to fix bugs.</p>
</blockquote>
<p>-- From <em>Refactoring: Improving the Design of Existing Code</em> (Martin Fowler and Kent Beck), page 56</p>
]]></content:encoded>
					
					<wfw:commentRss>https://rafael.bernard-araujo.com/principles-in-refactoring-slowing-down-new-features.php/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2252</post-id>	</item>
		<item>
		<title>The Rule of Three</title>
		<link>https://rafael.bernard-araujo.com/the-rule-of-three.php</link>
					<comments>https://rafael.bernard-araujo.com/the-rule-of-three.php#respond</comments>
		
		<dc:creator><![CDATA[rafael]]></dc:creator>
		<pubDate>Sun, 19 Oct 2025 19:56:20 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[software engineering]]></category>
		<guid isPermaLink="false">https://rafael.bernard-araujo.com/?p=2243</guid>

					<description><![CDATA[The first time you do something, you just do it. The second time you do something similar, you wince at the duplication, but you do the duplicate thing anyway. The third time you do something similar, you refactor. -- Don Roberts]]></description>
										<content:encoded><![CDATA[<blockquote>
<p>The first time you do something, you just do it. The second time you do something similar, you wince at the duplication, but you do the duplicate thing anyway. The third time you do something similar, you refactor.</p>
</blockquote>
<p>-- Don Roberts</p>
]]></content:encoded>
					
					<wfw:commentRss>https://rafael.bernard-araujo.com/the-rule-of-three.php/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2243</post-id>	</item>
		<item>
		<title>Domain-Driven Design &#8211; DDD</title>
		<link>https://rafael.bernard-araujo.com/domain-driven-design-ddd.php</link>
					<comments>https://rafael.bernard-araujo.com/domain-driven-design-ddd.php#respond</comments>
		
		<dc:creator><![CDATA[rafael]]></dc:creator>
		<pubDate>Fri, 18 Oct 2024 08:09:26 +0000</pubDate>
				<category><![CDATA[Programming]]></category>
		<category><![CDATA[ddd]]></category>
		<category><![CDATA[domain-driven design]]></category>
		<guid isPermaLink="false">https://rafael.bernard-araujo.com/?p=2107</guid>

					<description><![CDATA[Domain-driven development (DDD) is an approach to software design that focuses on the core domain and the logic that drives a business. The idea is to model the software based on real-world business concepts, ensuring that the code closely reflects the domain it is meant to serve. Key aspects of DDD include: Domain Model: A [&#8230;]]]></description>
										<content:encoded><![CDATA[<p>Domain-driven development (DDD) is an approach to software design that focuses on the core domain and the logic that drives a business. The idea is to model the software based on real-world business concepts, ensuring that the code closely reflects the domain it is meant to serve.</p>
<p>Key aspects of DDD include:</p>
<ol>
<li>
<p><strong>Domain Model</strong>: A shared understanding of the business logic, defined in terms meaningful to domain experts and developers.</p>
</li>
<li>
<p><strong>Ubiquitous Language</strong>: A common language shared by technical and non-technical stakeholders to describe the domain, ensuring clarity and reducing miscommunication.</p>
</li>
<li>
<p><strong>Bounded Contexts</strong>: Distinct areas within a larger system where a specific domain model applies. Each context can evolve independently while being integrated with others.</p>
</li>
<li>
<p><strong>Entities and Value Objects</strong>: Entities have unique identities, while value objects are immutable and are defined only by their properties.</p>
</li>
<li>
<p><strong>Aggregates</strong>: Clusters of related objects treated as a unit, ensuring consistency in business operations.</p>
</li>
<li>
<p><strong>Repositories and Services</strong>: Repositories handle data access, while services implement business operations that don’t belong to a single entity.</p>
</li>
</ol>
<p>DDD emphasizes collaboration between developers and domain experts to ensure software design mirrors business processes and terminology.</p>
<blockquote>
<p>A particularly important part of DDD is the notion of Strategic Design - how to organize large domains into a network of Bounded Contexts. [1]</p>
</blockquote>
<p><strong>Why is this important for your business?</strong></p>
<p>The design it proposes puts our focus on the core domain and the business logic, which makes our product relevant and where it differentiates from competitors. The DDD design boosts the understanding of <strong>what our application does</strong> instead of <em>which technology (framework, dependencies) it uses</em>.</p>
<blockquote>
<p>Domain-Driven Design is an approach to software development that centers the development on programming a domain model that has a rich understanding of the processes and rules of a domain. The name comes from a 2003 book by Eric Evans that describes the approach through a catalog of patterns. Since then a community of practitioners have further developed the ideas, spawning various other books and training courses. The approach is particularly suited to complex domains, where a lot of often-messy logic needs to be organized. [1]</p>
</blockquote>
<p>We will see more about how this translates to our code as we understand the key aspects to be expanded in future posts.</p>
<p>Related:<br />
[1] <a href="https://martinfowler.com/bliki/DomainDrivenDesign.html">Domain-Driven Design by Martin Fowler</a><br />
[2] <a href="https://en.wikipedia.org/wiki/Domain-driven_design">Domain-Driven Design on Wikipedia</a></p>
]]></content:encoded>
					
					<wfw:commentRss>https://rafael.bernard-araujo.com/domain-driven-design-ddd.php/feed</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		<post-id xmlns="com-wordpress:feed-additions:1">2107</post-id>	</item>
	</channel>
</rss>
