# Berris.dev (full text)
> Berris.dev is Roy Berris's knowledge base on software architecture, API design, AI agents and .NET, written from hands-on experience as a software architect.
Written by Roy Berris, Software Architect at New Orange. About the author: https://berris.dev/about/. Index of all nodes: https://berris.dev/llms.txt. Nodes are listed newest first.
---
# Clearing my evening thoughts with a personal VPS
- URL: https://berris.dev/nodes/clearing-my-evening-thoughts-with-a-personal-vps/
- Markdown: https://berris.dev/nodes/clearing-my-evening-thoughts-with-a-personal-vps.md
- Author: Roy Berris
- Published: 2026-09-26
- Clusters: AI, AI Agents, Software Architecture
> How I stopped evening work anxiety by wiring up a private VPS, Matrix, and an Obsidian second brain into an always-on personal AI assistant.
The promise of AI-assisted engineering was supposed to be breathing room. In reality, it turned me into a high-bandwidth orchestrator.
When generating implementations, reviewing distributed systems architectures, and debugging complex traces take minutes instead of hours, you don't touch one problem at a time—you touch ten. By the end of the workday, my working memory felt like a distributed system under partition: dozens of dangling references to half-reviewed PRs, edge cases in microservices, and architectural refactors still spinning in my head.
Humans don't have atomic garbage collection. Without a reliable, zero-friction offload mechanism, those open threads follow you into your evening, conversations with family, and sleep. I found myself running an involuntary background process all evening, chewing mental cycles on tomorrow's problems.
I needed a mental cache invalidation mechanism: a private, always-available sink where I could dump raw thoughts from anywhere, trust that they were captured and categorized, and immediately flush my working memory.
Here is how I built that system on a $5 VPS using an owner-operated stack, grounded in the cognitive psychology of offloading mental residue.
## The Cognitive Science: Why Offloading Works
Engineers often treat mental exhaustion as a failure of willpower or focus. In reality, it is a well-documented cognitive bottleneck.
Psychological research on **cognitive offloading**—defined by Risko & Gilbert (2016) as the use of physical actions or external devices to alter the information processing requirements of a task—demonstrates that our biological working memory has strict, unforgiving bandwidth constraints. When an unresolved work problem or future intention stays in our head, the brain continues to expend executive resources rehearsing it.
Furthermore, as research by Morrison & Richmond (2020) and broader studies on prospective memory show, externalizing intentions onto a dependable external store directly relieves working memory load and terminates involuntary retrieval loops. When we don't offload, we suffer from what organizational psychologists describe as **cognitive residue**: attention and working memory capacity remain tethered to an unfinished task, lingering long after we have stepped away from the keyboard and impairing our ability to engage with the present moment.
Writing things down is not just about keeping a todo list—it physically and mentally frees working memory load, neutralizes lingering cognitive residue, and signals to the brain's executive control network that the loop is safely closed.
The catch? If the offloading mechanism introduces friction, the brain defaults to keeping the data in working memory.
## The Architectural Requirements: Zero Friction and Absolute Privacy
Commercial SaaS assistants (ChatGPT, Claude, Notion AI) are impressive, but they failed my personal requirements for two reasons:
1. **Friction kills capture**: If offloading a late-night thought requires unlocking a phone, opening a heavy app, waiting for a web view to load, and navigating menus, I won't do it. Capture must be as low-latency as sending a message to a friend.
2. **Data sovereignty**: I frequently think through proprietary architecture trade-offs, internal client constraints, and unvarnished personal thoughts. Routing those unfiltered into a multi-tenant corporate cloud felt fundamentally irresponsible.
The design constraints became clear: an owner-operated stack running on a modest 2 vCPU, 4 GB RAM Ubuntu server, secured entirely behind a private mesh network, with end-to-end encrypted chat on my phone and instant synchronization into my personal markdown vault.
```mermaid
flowchart TD
subgraph ClientDevices ["Client Devices (MacBook & Phone)"]
A["Matrix Client (Element)"]
B["Obsidian App (LiveSync)"]
end
subgraph VPS ["Private VPS (Tailnet Secured)"]
C["Matrix Server (Continuwuity)"] -->|Webhook / API| D["Hermes Agent Gateway"]
D -->|Append / Query| E["Local Vault (Deno Bridge)"]
E -->|Two-way Sync| F["CouchDB (LiveSync Database)"]
end
subgraph Offsite ["Offsite Disaster Recovery"]
G["Private GitHub Repository"]
end
A -->|E2EE Instant Text / Voice| C
B <-->|Real-time Sync| F
E -.->|Hourly Git Cron| G
```
## The Stack Breakdown
The entire system runs on a cheap cloud node consuming under 450 MB of resident RAM, leaving abundant headroom.
### 1. Transport Layer: Matrix via Continuwuity
Rather than building a bot on Telegram or Discord—which exposes metadata to third parties and lacks native decentralized E2EE—I deployed **Continuwuity** (a lightweight Matrix homeserver) in Docker.
Using Element on my phone and laptop gives me an instant chat interface with push notifications, voice note recording, and full message persistence.
### 2. Orchestration & Agent: Hermes Gateway
The agent logic runs as a managed `systemd` service (`hermes-gateway`). When a message arrives in my private Matrix control room, Hermes processes the intent:
- **Raw dumps**: "Remind me to check the idempotent consumer retry loop on the order service tomorrow morning."
- **Architectural sketches**: Notes on decoupling two domain boundaries.
- **Vault queries**: "What did I decide last month about the JWT expiration strategy?"
The gateway evaluates the message against my system prompt (`SOUL.md`) and routes it accordingly:
```yaml
# hermes-agent/config.yaml snippet
agent:
name: "Hermes"
workspace: "/var/lib/vault/second-brain"
default_inbox: "inbox/daily-dumps.md"
sync_strategy: "append-with-timestamp"
matrix:
homeserver_url: "http://127.0.0.1"
listen_room: "!internal-ops:matrix.local"
```
### 3. Second Brain Storage: Obsidian + CouchDB LiveSync
My primary knowledge base is a local-first Obsidian vault. While Obsidian Sync is great for desktop-to-mobile, headless server integration requires something programmable.
I run CouchDB alongside `couchdb-livesync`. A lightweight Deno bridge keeps the VPS filesystem in sync with CouchDB. When Hermes writes markdown directly to `/var/lib/vault/second-brain/inbox/`, CouchDB pushes the diff to my phone and laptop within milliseconds.
### 4. Zero Public Surface: Tailscale Overlay
None of these services are exposed to the public internet:
- No public domain names or DNS records pointing to the VPS.
- No public reverse proxy or open HTTP/HTTPS ports.
- UFW drops all inbound traffic except Tailscale's WireGuard interface (`tailscale0`) and SSH keys.
Whether I am at my home desk, in the kitchen, or out running errands on mobile data, my phone connects seamlessly over the encrypted Tailnet mesh.
### 5. Disaster Recovery: Hourly Git Commits
Database files can corrupt; physical nodes can disappear. An hourly cron job snapshots the vault state, agent configuration, and prompts into a private, encrypted GitHub repository:
```bash
#!/usr/bin/env bash
cd /var/lib/vault/second-brain && \
git add . && \
git diff-index --quiet HEAD || git commit -m "auto: vault snapshot $(date -u +'%Y-%m-%dT%H:%M:%SZ')" && \
git push origin main --quiet
```
## The Workflow in Practice
Here is what this looks like on a typical evening:
1. **The Kitchen Sanctuary**: At 18:00 PM, I arrive home carrying fresh groceries, ready to decompress. Cooking is one of my favorite hobbies—a tactile, sensory craft that usually pulls me away from terminals and monitors. But as I stand over the hot pan stir-frying chicken, watching the garlic and scallions sizzle in the oil, an unexpected work thought pops into mind: *We missed a race condition in our distributed event processor when tenant updates arrive out of order.*
2. **The 5-Second Offload**: In the past, this was where the evening dissolved into rumination. My brain would clutch the idea, turning it over in an anxious loop while dinner burned. Instead of ruminating, I reach for my phone on the counter with a clean hand and send a quick message to Hermes: *"Check the partition key on the event consumer. If two tenant updates arrive out of order, the state store could get corrupted. Put this on the todo list for tomorrow morning."*
3. **The Agent Handling**: Hermes intercepts the message over Matrix, transcribes the note, tags it `#architecture/concurrency`, and immediately appends it to my actionable todo list for the next day or upcoming week in Obsidian.
4. **The Flush**: Within three seconds, Hermes replies: *"Captured. Added to tomorrow's todo list under Architecture Review."*
5. **Taking the Edge Off**: Having a dependable place to capture the detail doesn't magically wipe my mind clean, but it takes the immediate edge off. Knowing it's safely logged stops the thought from looping in the background, making it easier to step away from work mode and get back to stir-frying my chicken.
> **The Zero-Friction Rule**: If capturing a thought requires more than three taps or more than five seconds, you will hesitate. When you hesitate, you retain the thought in working memory, and working memory ruins your downtime.
## Architectural Lessons & Production Advice
If you are setting up a personal assistant sink, keep these rules in mind:
- **Separate Capture from Execution**: Do not ask your mobile assistant to execute complex refactors or kick off builds while you're offline. Treat it strictly as an intake and retrieval engine during off-hours.
- **Local Markdown is the Ultimate Format**: Avoid proprietary databases for your notes. Plain markdown files with YAML frontmatter guarantee that you can switch tools or replace LLMs ten years from now without data loss.
- **Fail Closed, Not Open**: Keep your AI assistant behind Tailscale. If a model hallucinates or an agent gateway hits an unhandled exception, it should fail quietly on an isolated loopback address, never in a way that leaks data.
## Conclusion
Engineering velocity with AI is only an advantage if you have the discipline—and the infrastructure—to disconnect from it.
Building a self-hosted assistant isn't a magic bullet or instant cure-all, nor is it about hoarding infrastructure or spending weekends writing YAML. It's simply a modest, practical habit backed by an owner-operated safety valve: a dependable sink that takes the edge off lingering work thoughts, keeps ideas from looping, and makes it that much easier to step away from the editor.
---
# The Three-Layer System for Consistent AI Specifications
- URL: https://berris.dev/nodes/the-three-layer-system-for-consistent-ai-specifications/
- Markdown: https://berris.dev/nodes/the-three-layer-system-for-consistent-ai-specifications.md
- Author: Roy Berris
- Published: 2026-09-26
- Updated: 2026-09-26
- Clusters: AI, Software Architecture, Best Practices
> How we use a three-layer compiler pipeline to generate functional design specifications with AI, compiling business truth down to verifiable contracts.
**TL;DR:** We treat the specification as the single source of truth for user intent and business rules, compiling functional design down through three strict layers. Business rules stay in native prose, the domain model structures invariants in English, and the contract or wire schema is simply the final compiled layer. Splitting the process this way stops hallucinations and eliminates schema drift.
When we first asked an AI model to generate a functional specification directly from rough requirements, the result looked convincing on the surface. But when we inspected the logic, the problems jumped out. The model quietly dropped business validation rules, invented defaults out of thin air, and hallucinated system behavior. Trying to fix all of that by stuffing more instructions into one giant prompt just confused the model and made the output drift even further.
People often assume specification generation is just about producing API definitions or code stubs. In our team, we look at it differently. A specification is the functional design of your system, and it is the single source of truth for user intent and business rules. Today's language models cannot bridge human intent and rigid technical contracts in one jump. To solve this, we treat functional design like a compiler chain: three distinct, strictly one-way layers that compile human truth down into a verifiable contract.
## Why Does Single-Shot Specification Lead to Drift?
Most teams start by feeding user stories or meeting notes into a chat prompt and asking for an interface definition or code stubs immediately. Under the surface, the model tries to solve three completely different problems at the same time: understanding business policies, modeling domain relationships, and formatting technical contract syntax. In that single pass, it silently invents enum values, misinterprets domain invariants, and overlooks edge cases just to produce syntactically valid output.
This creates silent drift between what business stakeholders expect and what engineers actually build. The trouble usually shows up late, often when validation fails in staging. When that happens, the temptation is to patch the generated schema or contract by hand.
But the contract is only the final compiled artifact of your functional design. Hand-editing downstream contracts breaks the chain of truth. The next time you run an AI generation tool, it overwrites those manual fixes or diverges further because the source of truth was never updated.
Splitting this workflow into sequential passes introduces a small latency trade-off. Generating intermediate representations takes a few minutes instead of a few seconds. In our experience, that extra time is worth it. You keep the context clean at each step and save yourself painful debugging sessions later.
## How Does the Three-Layer Compiler Pipeline Work?
Instead of treating specification generation as a single prompt, we treat it like a compiler chain. Each stage has a single, clear job, moving strictly from human and business intent down to machine contracts. Upstream artifacts remain the single source of truth, and downstream layers are generated automatically.
```mermaid
flowchart TD
L1["Layer 1: Human and Business Truth Native business rules and user intent"] --> L2["Layer 2: Functional Design Domain model and system invariants"]
L2 --> L3["Layer 3: Verifiable Contract Compiled wire schemas and interfaces"]
style L1 fill:#14432a,stroke:#4ade80,color:#dcfce7
style L2 fill:#0e3a4a,stroke:#67e8f9,color:#e0f7ff
style L3 fill:#3b1f5c,stroke:#c084fc,color:#f3e8ff
```
The system organizes requirements into three distinct stages:
- **Layer 1 (Human and Business Truth):** Written in native business prose to capture stakeholder rules directly. In our projects in the Netherlands, capturing policies in Dutch prevents premature translation errors and preserves regulatory nuances that non-technical domain experts care about. Stakeholders can read, verify, and own this layer directly.
- **Layer 2 (Functional Design and Domain Model):** Expressed as an English architectural specification following [Domain-Driven Design](https://www.domainlanguage.com/ddd/) principles. This layer normalizes business concepts into formal entities, [value objects](/nodes/using-value-objects-in-net/), lifecycle states, and domain invariants without any transport or serialization baggage. It defines what the system does and why, independent of delivery protocols.
- **Layer 3 (Verifiable Contract):** Defined in [TypeSpec](https://typespec.io/), OpenAPI, or schema definitions. This layer handles transport mechanics, status codes, query parameters, header definitions, and serialization, following the principles we described in [Designing APIs for AI Agents](/nodes/designing-apis-for-ai-agents/). Wire contracts and API schemas are not the starting point. They are the final compiled layer of functional design.
This workflow is an automated compiler pipeline rather than a traditional waterfall process. We edit the upstream business rules, run the generation tool, and let the pipeline compile the downstream contracts. Nobody edits the compiled wire contract by hand.
## How Do We Capture and Validate Business Rules?
You cannot expect an AI model or an engineer sitting alone to invent business truth. Layer 1 requires collaborative discovery with domain experts before any code or prompt runs. In our projects, we use three discovery techniques to draw out rules from stakeholders:
- **[Event Storming](https://www.eventstorming.com/):** We gather domain experts and developers in a room to map domain events along a business timeline. We explore what happens across a process, what triggers each action, and which policies govern state changes.
- **[Example Mapping](https://cucumber.io/blog/bdd/example-mapping-introduction/):** We take each user story and break it down into concrete business rules illustrated by realistic examples. Talking through concrete scenarios reveals edge cases and hidden assumptions that abstract bullet points conceal.
- **Stakeholder interviews:** We talk directly with product managers, operational staff, and compliance officers. Capturing their exact words in their native language preserves legal and operational nuances that get lost when developers translate requirements straight into technical jargon.
Capturing fragments of rules is only half the battle. You also need to validate the requirements as a coherent whole before feeding them to an AI compiler.
We run validation sessions with the business to check completeness. We walk through the end-to-end user journey to make sure every failure mode, boundary condition, and lifecycle transition has an explicit rule. Walking through realistic examples surfaces edge cases early, when changing a rule costs nothing.
Once the rules are complete and unambiguous, we secure formal sign-off from business stakeholders. Because Layer 1 uses plain language without HTTP status codes, JSON fields, or database tables, non-technical experts can read every sentence and take full ownership. This sign-off freezes the baseline for Layer 1. Only after the business approves Layer 1 does the AI compiler pipeline turn those rules into downstream models and contracts.
## Tracing an Example Through the Three Layers: A Library Book Loan
To see how this works in practice, consider a classic scenario: a member borrowing a physical book from a library. Walking this feature through the three layers shows how business intent compiles into a technical contract while maintaining complete traceability.
### Layer 1: Business Rules
Layer 1 captures the intent and rules in plain language approved by the library staff:
- **User Intent:** A registered library member wants to borrow a physical book.
- **Rule 1 (Loan limit):** A member can have at most 5 active book loans at any time.
- **Rule 2 (Good standing):** A member cannot borrow books if they have an unpaid fine.
- **Rule 3 (Loan period):** The standard loan period is 21 days from the date of checkout.
There are no endpoints, status codes, or database keys here. Any librarian can read this list and confirm whether it accurately describes how the library operates.
### Layer 2: Domain Model
Next, the compiler pipeline generates the English architectural specification. Layer 2 applies domain-driven design concepts to model entities, invariants, post-conditions, and domain events:
- **Entity:** `Loan` (attributes: `loanId`, `memberId`, `bookId`, `loanDate`, `dueDate`, `status`).
- **Domain Invariant (`BorrowBookPolicy`):** A loan can only be created if the member's current active loans count is less than 5, and the member's unpaid fine balance is zero.
- **Post-conditions:** A new `Loan` is instantiated with `status: Active` and `dueDate` calculated as `loanDate + 21 days`.
- **Domain Event:** The system emits a `BookBorrowed` event containing `loanId`, `memberId`, `bookId`, and `dueDate`.
Layer 2 defines what the domain logic must enforce. It still contains zero HTTP headers or serialization details.
### Layer 3: Verifiable Contract
Finally, the pipeline compiles Layer 2 into a verifiable API contract. Here is the OpenAPI definition for the borrow operation:
```yaml
paths:
/members/{memberId}/loans:
post:
summary: Borrow a book
operationId: borrowBook
security:
- bearerAuth: ["loans:borrow"]
parameters:
- name: memberId
in: path
required: true
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- bookId
properties:
bookId:
type: string
responses:
"201":
description: Book borrowed successfully
content:
application/json:
schema:
$ref: "#/components/schemas/Loan"
"409":
description: Business policy violation
content:
application/json:
schema:
type: object
required:
- error
properties:
error:
type: string
enum:
- loan-limit-reached
- unpaid-fine
```
Notice how this contract structures the response codes. The schema returns a 201 status code with the created loan on success, or a 409 Conflict with a machine-readable error when a domain rule fails.
### Emphasizing Traceability
Look closely at how every line in this Layer 3 contract traces directly back through Layer 2 to Layer 1:
- The path `/members/{memberId}/loans` and operation summary `Borrow a book` trace directly to the plain-language user intent in Layer 1.
- The required permission `loans:borrow` traces to the library policy that the caller must have borrowing privileges.
- The `409` error enum `loan-limit-reached` traces through the Layer 2 `BorrowBookPolicy` invariant back to Rule 1 (limit of 5 active loans).
- The `409` error enum `unpaid-fine` traces through the Layer 2 invariant back to Rule 2 (no unpaid fines allowed).
- The `dueDate` in the returned `Loan` object traces through the Layer 2 post-condition back to Rule 3 (21-day loan duration).
Nothing in the contract appears out of thin air. If a model generates an unexpected error code or an extraneous query parameter, you can flag it instantly because it lacks an upstream parent in Layer 1. Every line in the contract traces directly back to an approved business rule.
## Why Is This Three-Layer Scaffolding Temporary?
We want to be clear about why this system exists today. These three separate stages are practical scaffolding built around the limits of current language models. Right now, models cannot reliably preserve nuance across multiple levels of abstraction in a single inference pass.
We think of this three-layer pipeline as a deliberate engineering hedge. It stabilizes model outputs today without locking our architecture into rigid, permanent workflow machinery. We documented this architectural boundary choice in an [Architecture Decision Record (ADR)](https://adr.github.io/) so our team understands why we enforce these boundaries and under what conditions we can simplify them.
Eventually, models will be capable enough to jump from business discussions to validated wire schemas without intermediate steps. When that shift happens, explicit file-based handoffs between layers will disappear from our daily work. Even then, the underlying separation of concerns between business truth, functional design, and verifiable contracts will remain structurally sound.
## How Does the Pipeline Stay Consistent When Models Change?
Model behavior changes with every new release, but this pipeline keeps our specifications stable. Layer 1 functions as source code, while Layer 2 and Layer 3 act as compiled build artifacts. When business requirements shift, we update the business rules in Layer 1 and trigger a clean compilation rather than patching downstream files.
In our team, we store domain rules, conventions, and architectural constraints inside version-controlled repository instructions and skills. Keeping guidance in git repositories ensures every engineer and continuous integration agent runs the exact same prompts. Ad-hoc chat sessions lose context quickly, but versioned skills keep that knowledge in the repository where everyone can use it.
The architecture remains completely tool-agnostic. You can switch the underlying foundation model or migrate from TypeSpec to another interface definition language whenever you choose. Because your core functional design lives upstream in clean domain models, changing a code generator never forces a rewrite of your business rules. When tools improve, downstream layers are simply regenerated.
## Recommendations for Your Team
Setting up this pipeline requires discipline around layer boundaries and prompt management. If you want to set up this system in your own projects, here is our practical advice:
- **Adopt a spec-first mindset:** Treat functional design as the single source of truth for user intent and business rules, not code stubs or handwritten schemas.
- **Treat Layer 1 as the sole source of truth:** Update business rules in native prose and secure business sign-off before compiling downstream layers.
- **Ensure strict traceability:** Make sure every property, operation, and error status in Layer 3 is derived from and traceable to an approved Layer 1 rule.
- **View the wire contract as the compiled layer:** Treat APIs and schemas as the final compiled representation of functional design, not the starting point.
- **Ground Layer 2 in domain-driven design:** Express domain invariants, entities, and events in clean English before worrying about transport protocols.
- **Regenerate downstream layers when tools improve:** Treat Layer 2 and Layer 3 as build artifacts. When you upgrade models or linters, recompile from Layer 1 instead of hand-patching files.
- **Automate downstream compilation:** Run models in strict one-way passes using automated scripts or continuous integration tasks.
- **Version control your prompt instructions:** Commit architectural rules, ADRs, and skills to your git repository alongside the project code.
- **Validate contracts with deterministic tools:** Use standard TypeSpec compilers or schema linters to catch syntax errors and contract flaws immediately.
- **Never edit generated layers by hand:** If you hand-tweak Layer 2 or Layer 3 outputs, subsequent compilations will wipe out your modifications.
- **Avoid two-way sync:** Never attempt to back-propagate changes from a wire contract back into the domain model. Keep it strictly one-way.
- **Keep transport details out of business rules:** Serialization quirks, status codes, and HTTP headers do not belong in Layer 1 or Layer 2.
## Conclusion
A specification is not just an API contract. It is the functional design that captures user intent and business rules as your single source of truth. By treating functional design as a three-layer compiler pipeline, you stop hallucinations and keep your contracts consistent with what the business actually needs.
Give the model one job at a time, and let the compiler do the rest.
## FAQ
### Is this three-layer system only for APIs?
No. While the final layer often produces API schemas or interface definitions, the pipeline is about generating the functional design as a whole. The specification is the single source of truth for user intent and business rules, and the contract is simply the final compiled layer of that functional design.
### How do you capture business rules before compiling?
We capture business rules through collaborative workshops with domain experts, using Event Storming to discover events, Example Mapping to pin down rules and edge cases, and stakeholder interviews. We validate the rules as a coherent whole and secure formal business sign-off before running the AI compiler pipeline.
### Why write Layer 1 in native language instead of English?
Writing Layer 1 in native business prose, such as Dutch in our projects in the Netherlands, captures policies directly from stakeholders without premature translation. This preserves regulatory nuances and policy details that domain experts care about, before Layer 2 translates and normalizes them into English domain concepts.
### What happens when business requirements change or tools improve?
You update the approved business rules in Layer 1 and recompile downstream layers through the automated pipeline. Because Layer 2 and Layer 3 are compiled build artifacts, you never hand-patch downstream files, and you can regenerate your entire contract whenever your foundation models or toolchain improve.
---
# Designing APIs for AI Agents: Schemas, Security and MCP
- URL: https://berris.dev/nodes/designing-apis-for-ai-agents/
- Markdown: https://berris.dev/nodes/designing-apis-for-ai-agents.md
- Author: Roy Berris
- Published: 2025-10-21
- Updated: 2026-09-26
- Clusters: API Design, AI Agents, Software Architecture, Design Patterns
> AI agents now call our APIs, but few of us design for them. How I use schemas, consistent patterns, security and MCP ideas to serve developers and agents.
**TL;DR:** [Postman's 2025 State of the API report](https://www.postman.com/state-of-api/2025/) shows that 89% of developers use AI in their daily work, but only 24% design APIs with AI agents in mind. My answer is to design for humans and AI agents at the same time: treat the schema as the shared language, put business context in it, keep every pattern consistent, rethink security for automated consumers and design endpoints as tools with clear contracts.
The API development world changed a lot in 2024, and it caught many of us by surprise. While we were busy making APIs better for human developers, a new consumer appeared that works very fast: AI agents. [Postman's 2025 State of the API report](https://www.postman.com/state-of-api/2025/) shows clearly that 89% of developers now use AI tools every day, but only 24% design APIs with AI agents in mind. This gap shows a big problem that needs new design patterns.
## Why Do AI Agents Need Different API Design?
When I design APIs today, I still think about the developer who will read my docs, understand my endpoint patterns, and write code to connect with it. But here's what Postman's research showed that really changed how I think about building APIs: AI agents are already using APIs at huge scale with a 40% increase from last year.
The disconnect is there. While 89% of developers use AI tools for making code and solving problems, most of us keep designing APIs using patterns made for human use. Only 13% design equally for humans and AI agents, while just 7% mainly design for AI agents. This mismatch creates basic problems when AI agents meet APIs that don't have clear schemas, typed errors, and clear behavioral rules.
## How Do You Design an API for Both Humans and AI Agents?
The key thing I've learned is that AI agents are trained on human language, which means we shouldn't design only for machines. Instead, we need to design for both humans and machines at the same time through consistent, well-documented interfaces that show intent and purpose.
### Schema as the Shared Language
I think of the API schema as the shared language between humans and machines. The schema is not just a technical contract. It's a complete way to communicate that shows intent, purpose, and business context in ways both developers and AI agents can understand.
**Semantic Metadata: Intent and Purpose**
The schema should tell a story about what the API does and why it exists. This means using clear field names that show business concepts, useful error codes that help fix problems, and response structures that show how things work together. When an AI agent sees a well-designed schema, it understands not just what data to send, but why that data matters and how it connects to real business work.
**Functional Documentation: Business Context**
Traditional [OpenAPI](https://spec.openapis.org/oas/latest.html) specs focus on technical contracts, but AI agents need business context to make smart decisions. I learned this the hard way when building APIs that agents couldn't use effectively. Now I put functional context directly into schema definitions through custom properties ([OpenAPI specification extensions](https://spec.openapis.org/oas/latest.html#specification-extensions), the `x-` fields) that give semantic meaning, links that connect technical operations to business outcomes, and workflow descriptions that show how endpoints work together to solve real problems.
This approach changes the schema from a purely technical thing into a complete communication tool. Instead of keeping separate technical and functional documentation, the schema becomes one source of truth that shows both implementation details and business intent.
**Navigation Metadata: Easy Discovery**
The schema should help find related functionality through structured navigation data. This includes relationship patterns that mirror real-world workflows, endpoint hierarchies organized around functional use cases rather than technical resource structures, and built-in guidance that helps both humans and AI agents understand when and how to use specific operations.
In my experience, organizing APIs around the problems they solve, rather than just data model relationships, makes them much easier for human developers while giving AI agents clear functional context about usage patterns.
**Example: Traditional vs. Agent-Aware Schema Design**
Here's how the same endpoint looks when designed traditionally versus with AI agents in mind:
```yaml
# Traditional approach - technical focus
/api/users/{id}:
get:
responses:
200:
content:
application/json:
schema:
properties:
id: { type: string }
name: { type: string }
email: { type: string }
# Agent-aware approach - semantic focus
/api/users/{userId}:
get:
summary: "Retrieve user profile for account management"
x-business-context:
purpose: "Account management and user support"
workflows: ["user-lookup", "billing-inquiry"]
parameters:
- name: userId
schema:
type: string
pattern: "^usr_[a-zA-Z0-9]{16}$"
x-semantic-meaning: "Primary user identifier"
responses:
200:
content:
application/json:
schema:
properties:
userId: { type: string }
profile:
type: object
x-business-purpose: "Identity verification"
accountStatus:
type: string
enum: ["active", "suspended", "pending"]
x-business-impact: "Determines available actions"
404:
description: "User not found"
x-remediation: "Verify user ID format"
```
### Consistency as a Design Language
Consistency becomes the design language that both humans and machines can learn and use. This means using the same naming rules across all endpoints, standard HTTP status code usage, the same authentication methods, and error handling patterns that create a predictable interaction model.
I use a pattern where every endpoint follows the same basic template: consistent parameter naming that shows business concepts, standard response envelopes that tell a complete story, the same error response formats that give useful guidance, and predictable resource relationship patterns that mirror real-world workflows. This consistency allows both human developers and AI agents to learn patterns once and apply them across the entire API surface.
**Unified Communication Architecture**
The diagram below shows how schema-driven design creates a unified communication layer that serves both human developers and AI agents:
```mermaid
graph TB
subgraph "API Schema Layer"
Schema[API Schema OpenAPI + Extensions]
Schema --> SM[Semantic Metadata Business Intent]
Schema --> FD[Functional Documentation Use Case Context]
Schema --> NM[Navigation Metadata Workflow Relationships]
end
subgraph "Human Consumers"
Dev[Developer]
DevTools[Development Tools Postman, Insomnia]
Docs[API Documentation Swagger UI, Redoc]
end
subgraph "AI Consumers"
Agent[AI Agent]
LLM[Language Model GPT, Claude]
AutoTools[Automation Tools Zapier, MCP Clients]
end
subgraph "Shared Understanding"
Patterns[Consistent Patterns Naming, Errors, Auth]
Context[Business Context Purpose, Workflows]
Discovery[Discoverability Related Operations]
end
Schema --> Patterns
Schema --> Context
Schema --> Discovery
Patterns --> Dev
Patterns --> Agent
Context --> Dev
Context --> Agent
Discovery --> Dev
Discovery --> Agent
Dev --> DevTools
Dev --> Docs
Agent --> LLM
Agent --> AutoTools
style Schema fill:#0e3a4a,stroke:#67e8f9,color:#e0f7ff
style Patterns fill:#3b1f5c,stroke:#c084fc,color:#f3e8ff
style Context fill:#14432a,stroke:#4ade80,color:#dcfce7
style Discovery fill:#4a2e0e,stroke:#fb923c,color:#ffedd5
```
## How Should API Security Change for AI Agents?
The security implications of AI agents as API consumers present a big challenge, but one that can be addressed with thoughtful design patterns. Postman's research shows that 51% of developers now cite unauthorized agent access as their top security concern, highlighting the need for better security approaches.
Traditional API security designs assumed predictable human behavior, developers making dozens of calls per day, following documented patterns, operating within reasonable rate limits. AI agents challenge these assumptions by operating at high speeds, keeping persistent automated access, and potentially turning a single compromised API key into a gateway for extensive data extraction. However, these challenges create opportunities to build stronger security designs.
### Behavioral Security Architecture
The unpredictable behavior of AI agents makes it hard to tell legitimate automation from attacks using traditional rule-based approaches. While I haven't needed to implement these patterns at scale yet, my team is exploring security designs that move beyond static rules to behavioral analysis systems that can recognize patterns in real-time and adjust responses dynamically.
This approach needs dynamic rate limiting based on behavioral patterns, better monitoring for suspicious activity, and shorter-lived credentials with automatic rotation. The key is building systems that can tell the difference between legitimate automation and potential attacks through behavior rather than static rules.
## What Does the Model Context Protocol (MCP) Change?
The emergence of the [Model Context Protocol](https://modelcontextprotocol.io/) represents an interesting development in API architecture for AI use. While 70% of developers know about MCP according to Postman's research, only 10% use it regularly. This points to growing interest but limited readiness in the ecosystem.
MCP introduces new patterns for structured interfaces between AI models and real-world systems. It addresses critical problems like unified agent access, standard security models, and structured tool definitions that agents can reliably understand. The protocol defines clear boundaries between what AI agents can discover, understand, and invoke.
### Key Principles from MCP
Regardless of whether MCP becomes the standard, the principles it embodies represent the direction we need to move. Structured interfaces with explicit capability declarations, clear tool definitions with typed parameters and responses, standard security models that work across different agent implementations, and discovery mechanisms that allow agents to understand available functionality.
In my projects, implementing these principles improves API architecture even without full MCP adoption. Making APIs agent-consumable through structured interfaces provides immediate benefits and future flexibility, whether MCP succeeds or alternative standards emerge.
**Example: Semantic Extensions in Practice**
Here's how I extend OpenAPI schemas with semantic metadata in my projects:
```json
{
"paths": {
"/customers/{customerId}/support-tickets": {
"post": {
"summary": "Create customer support ticket",
"x-business-context": {
"purpose": "Enable customers to report issues",
"workflow-stage": "issue-reporting",
"related-operations": [
{
"operation": "GET /customers/{customerId}/support-tickets",
"relationship": "list-related"
}
]
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"properties": {
"subject": {
"type": "string",
"x-semantic-purpose": "Primary classification for routing"
},
"priority": {
"type": "string",
"enum": ["low", "medium", "high", "critical"],
"x-business-rules": {
"critical": "Service outage affecting multiple customers",
"high": "Feature broken for paying customer"
}
}
}
}
}
}
},
"responses": {
"201": {
"x-business-outcome": "Customer issue is now tracked",
"content": {
"application/json": {
"schema": {
"properties": {
"ticketId": {
"type": "string",
"x-semantic-purpose": "Primary reference for follow-up"
}
}
}
}
}
}
}
}
}
}
}
```
This shows how semantic extensions (`x-business-context`, `x-semantic-purpose`) provide context for both developers and AI agents.
### Preparing for Protocol Evolution
The low adoption rate suggests practical barriers that the industry needs to address, but the design patterns remain valuable. Building APIs with explicit capability declarations, structured tool definitions, and clear security boundaries positions systems to adapt to whatever standards emerge.
This approach means designing APIs as tool catalogs rather than simple data interfaces. Each endpoint becomes a tool with clear inputs, outputs, and behavioral contracts that both human developers and AI agents can understand and invoke reliably.
## Key Design Priorities for the Future
Based on what I've seen in recent projects and the trends Postman identified, there are big changes coming in API architecture. The shift from human-focused to machine-focused design needs new patterns, different security models, and better documentation strategies. Three design priorities emerge as critical for systems that need to support both human developers and AI agents well.
### Machine-First Interface Design
APIs must be built with AI agents as primary users rather than afterthoughts. This means designing interfaces that provide machine readable schemas, predictable patterns, and comprehensive behavioral specifications from the ground up. The architecture should assume automated use and optimize for reliability at high speeds.
### Adaptive Security Architecture
Security models must evolve beyond traditional patterns to handle high-speed exploitation, persistent automated attacks, and unpredictable behavior. This needs architectural approaches that can tell the difference between legitimate automation and attacks through behavioral analysis rather than static rules.
### Tool-Oriented Design Patterns
API architecture must shift from simple data interfaces to structured tool catalogs where each endpoint represents a well-defined capability with clear inputs, outputs, and behavioral contracts. This design pattern enables both human developers and AI agents to understand and invoke functionality reliably.
## Conclusion
The API landscape stands at a turning point where we must design for both human understanding and machine use at the same time. The key insight is that AI agents are trained on human language patterns, which creates an opportunity to build unified architectures that serve both audiences well.
My experience shows that the practices needed for effective human-machine API use (consistent patterns, schema-driven functional documentation, and discoverable use case context) create better APIs overall. These aren't competing design goals but approaches that work together and strengthen each other.
The future belongs to APIs that work as complete communication systems, where schemas serve as natural language interfaces and functional documentation is discoverable through the same mechanisms that AI agents use for technical discovery. The design patterns we implement today will determine whether our systems can communicate effectively with both human developers and AI agents.
The time to start building these unified communication architectures is now.
## FAQ
### How many developers design APIs for AI agents?
According to [Postman's 2025 State of the API report](https://www.postman.com/state-of-api/2025/), 24% of developers design APIs with AI agents in mind. Only 13% design equally for humans and AI agents, and 7% mainly design for AI agents. At the same time, 89% of developers use AI in their daily work.
### What should an API schema include for AI agents?
Clear field names that match business concepts, useful error codes, and the business context behind each operation. I add that context with custom `x-` extensions such as `x-business-context` and `x-semantic-purpose`, including the workflow an endpoint belongs to and which operations relate to it.
### Do I need MCP to make my API ready for AI agents?
No. MCP adoption is still low, but its principles already help: explicit capability declarations, typed tool definitions, standard security models and discovery. In my projects, applying those principles improves the API even without full MCP adoption.
### How do AI agents change API security?
AI agents work at high speed with persistent automated access, so a single compromised API key can open the door to large-scale data extraction. My team is exploring behavioral analysis, dynamic rate limiting and shorter-lived credentials with automatic rotation instead of static rules. I haven't needed these patterns at scale yet.
---
# AI-Assisted Blogging: When Technology Meets Technical Writing
- URL: https://berris.dev/nodes/ai-assisted-blogging/
- Markdown: https://berris.dev/nodes/ai-assisted-blogging.md
- Author: Roy Berris
- Published: 2025-09-29
- Updated: 2026-09-26
- Clusters: AI, Blogging, Technical Writing
> How I blog with AI as a writing partner, not a ghostwriter: I bring the insights and own every technical claim, AI helps with structure and readability.
**TL;DR:** I use AI as a writing partner, not a ghostwriter. I give it my outline, rough notes and project material, it writes a structured first draft, and then I rewrite section by section until every technical detail is accurate and it sounds like me. The ideas and technical claims are always mine; AI helps with structure and readability.
I'll be honest with you, I'm fundamentally lazy when it comes to certain aspects of writing. Not the thinking part, mind you. I love diving deep into technical problems, architecting solutions, and sharing insights from real-world implementations. But when it comes to polishing my thoughts into coherent, well-structured prose that doesn't make readers want to close their browsers immediately? Well, let's just say I'd rather delegate that task to someone (or something) more capable. Enter AI-assisted blogging: my new favorite productivity hack.
## The Problem: Technical Expertise vs. Communication Skills
As software architects, we face a persistent gap between having valuable technical insights and presenting them effectively. My experience spans numerous .NET implementations and architectural decisions, but transforming these into compelling blog content requires different skills entirely. Traditional technical writing takes a lot of time for structuring, refinement, and making things readable. This overhead makes blogging feel impossible when you're already managing demanding project schedules.
## How Do I Use AI Without Losing My Voice?
I think of AI as my writing partner rather than my ghostwriter. I dump my technical insights, experiences, and half-formed ideas into rough outlines or brain dumps. Sometimes it's just bullet points about a particularly gnarly architecture decision I made, or notes about why a certain implementation approach worked better than expected.
AI takes this raw material and helps me structure it into something readable. It's not generating the ideas or making technical claims, that's all me. Instead, it's figuring out how to organize my thoughts coherently, smoothing out my awkward phrasing, and making sure the whole thing flows without putting readers to sleep. It also helps me identify when I should explain common design patterns or architectural concepts that you might not be familiar with, suggesting where additional context would be helpful.
I maintain complete editorial control throughout the process. Every technical insight gets my stamp of approval, and if AI suggests something that doesn't align with my actual experience or opinion, it gets scrapped. The goal is amplifying my voice, not replacing it with some generic technical writing template.
## What Does My AI Blogging Workflow Look Like?
Here's how this actually works in practice. I start by outlining the subject and identifying the key points I want to cover. Then I gather all my supporting materials, emails from project discussions, PDF documentation, [Lucid](https://lucid.co/) diagrams I've created, [LikeC4](https://likec4.dev/) architectural models, meeting notes, whatever artifacts capture the real story behind the technical decisions.
I dump all of this into AI along with my outline and let it write the first draft of the blog post. This gives me a structured starting point rather than staring at a blank page. But here's where I become the orchestrator, I go through section by section, moving content around, rewriting entire paragraphs, adding my own examples, and making sure the technical details are accurate and reflect my actual experience.
I'm the final editor of every post, even with AI as my writing partner. AI might suggest a flow or help with transitions, but I'm the one deciding what stays, what goes, and what needs to be completely rewritten to sound like me rather than a polished corporate blog written by ChatGPT.
## What Have I Learned from Blogging with AI?
This collaboration has some benefits beyond just saving time. Working with AI has actually improved my own writing skills because I see how it structures sentences and organizes ideas. I've also learned that the more specific I am about who's reading the blog and how technical to get, the better the AI's output becomes.
This approach has turned blogging from something I rarely did into something I might do regularly.
## Recommendations for You
If you're thinking about trying AI-assisted blogging, start with clear rules about who owns what. Keep complete control over technical accuracy and your professional opinions, use AI specifically to help with communication. Set up consistent review processes and document how you work with AI so you get reliable results.
Think of AI as something that multiplies your communication abilities without replacing your expertise. When writing becomes less of a chore, you can share knowledge more often, which benefits both your personal brand and helps other developers learn from your experience.
## Conclusion
AI-assisted blogging offers a practical solution if you have valuable technical insights but struggle with communication barriers. By establishing clear boundaries, I provide the technical expertise, AI enhances the communication, and together we can create content while maintaining authenticity and technical depth.
The result is more frequent, higher-quality content that amplifies rather than replaces your professional voice.
## FAQ
### Does AI write the posts on Berris.dev?
AI writes the first draft from my outline and material, but it doesn't generate the ideas or make the technical claims. I go through every section, rewrite paragraphs, add my own examples and scrap anything that doesn't match my actual experience or opinion.
### What do I give the AI before it writes a draft?
An outline with the key points, plus the material that captures the real story: emails from project discussions, PDF documentation, Lucid diagrams, LikeC4 architectural models and meeting notes.
### How do I keep AI-assisted posts technically accurate?
I keep complete control over technical accuracy and my professional opinions. I'm the final editor of every post, and every technical insight needs my approval before it stays in.
### How do I get better output from AI when writing?
Be specific about who's reading the blog and how technical to get. In my experience, the more specific I am about that, the better the output becomes.
---
*This blog post was created using the AI-assisted approach described within its content. All technical insights and recommendations reflect my direct experience as a software architect, while AI helped refine the structure and readability.*
---
# Standardizing API Conventions with ADRs
- URL: https://berris.dev/nodes/standardizing-api-conventions/
- Markdown: https://berris.dev/nodes/standardizing-api-conventions.md
- Author: Roy Berris
- Published: 2025-09-05
- Updated: 2026-09-26
- Clusters: API, Software Architecture, ADR, Team Collaboration
> How one two-hour DevAlign session and a set of Architecture Decision Records (ADRs) gave my team consistent API naming, versioning, errors and pagination.
**TL;DR:** Inconsistent API design across a growing team led to integration errors, messy error handling and unexpected breaking changes for partners. I fixed it with a two-hour "DevAlign" session with one person each from development, testing, product management and operations, and documented every agreed convention as an Architecture Decision Record (ADR): plural resource names, URI-based versioning, one error format (code, message, details) and cursor-based pagination.
Throughout my experience as a software architect, few challenges have proven as persistent and impactful as maintaining consistency across API design within growing development teams. The absence of standardized conventions creates a cascading effect of integration complexity, developer confusion, and technical debt that compounds over time. Recognizing this critical gap in our organizational practices, I initiated a systematic approach to establish unified API conventions through collaborative decision-making and structured documentation.
## What does inconsistent API design cost a team?
The symptoms of inconsistent API design manifest across multiple dimensions of software development. In our organization, endpoint naming conventions varied significantly between developers, with some employing singular resource names while others utilized plural forms. This inconsistency created unnecessary cognitive overhead during integration work and increased the likelihood of implementation errors.
Error handling presented another significant challenge. Different services returned error responses in disparate formats, complicating client-side error processing and making automated testing more complex. The lack of standardized pagination strategies resulted in performance issues and inconsistent user experiences across services consuming shared data sources.
External partners consuming our APIs expressed frustration with unpredictable versioning schemes that led to unexpected breaking changes. These real-world consequences demonstrated the urgent need for comprehensive, well-documented API conventions to enhance system maintainability and developer productivity.
## How did ADRs and a DevAlign session fix it?
I implemented a two-pronged approach centered on [Architecture Decision Records (ADRs)](https://adr.github.io/) and facilitated cross-functional collaboration. ADRs provide a structured framework for documenting architectural decisions, capturing context, alternatives considered, and rationale behind chosen approaches. This methodology ensures transparency in decision-making while creating institutional knowledge that persists beyond individual team member tenure.
To operationalize this approach, I designed a collaborative workshop format I termed a "DevAlign" session. This structured engagement involved one representative from each functional role (development, testing, product management, and operations), ensuring comprehensive perspective representation in our decision-making process.
The two-hour session combined focused discussion with practical prototyping exercises. This hands-on approach enabled real-time validation of proposed conventions and fostered consensus through active participation rather than passive acceptance of imposed standards.
## Which API conventions did we agree on, and why?
The collaborative process yielded several key conventions that we formally documented through ADRs:
**Resource Naming Standards:** We standardized on plural noun usage for endpoint paths, aligning with RESTful principles and improving API predictability. This decision eliminated ambiguity while conforming to established industry patterns.
**Versioning Strategy:** URI-based versioning was selected to provide explicit visibility of breaking changes to API consumers. While header-based alternatives were evaluated, URI versioning offered superior compatibility with routing infrastructure and caching mechanisms.
**Error Response Structure:** We established a consistent error object format containing code, message, and details fields. This standardization simplified client-side error handling while improving system observability and debugging capabilities.
**Pagination Implementation:** Cursor-based pagination was adopted to enhance performance with large datasets and prevent data consistency issues during concurrent modifications. This approach proved superior to offset-based alternatives in our high-throughput scenarios.
Each decision was thoroughly documented with context, alternatives analysis, and acknowledged trade-offs, ensuring our conventions remained both practical and adaptable to future requirements.
## Practical recommendations for implementation
The success of this initiative depended on several critical factors that other organizations can leverage:
**Stakeholder Engagement:** Involving representatives from all affected roles ensures comprehensive perspective consideration and builds ownership commitment to established conventions.
**Documentation Rigor:** Implementing ADRs as living documents creates accountability and provides historical context for future decision-making processes.
**Incremental Approach:** Beginning with core conventions and expanding systematically prevents overwhelming teams while establishing momentum for broader adoption.
**Governance Framework:** Establishing periodic review mechanisms ensures conventions evolve appropriately with changing technological and business contexts.
## Conclusion: organizational impact and future considerations
The implementation of standardized API conventions through structured decision-making has generated measurable improvements across our development lifecycle. Developer onboarding efficiency has increased significantly due to reduced learning overhead. Documentation quality has improved substantially, with ADRs serving as authoritative references complementing API specifications.
Integration defects related to API inconsistencies have decreased markedly, contributing to enhanced system stability and customer satisfaction. External partner collaboration has become more streamlined due to predictable, well-documented interfaces.
This systematic approach to convention establishment demonstrates the value of combining structured decision documentation with inclusive collaboration processes. The investment in establishing clear guidelines has positioned our organization to scale development capabilities while maintaining high standards of technical quality and developer experience.
## FAQ
### What is a DevAlign session?
It's the workshop format I designed to agree on API conventions: a two-hour session with one representative from each role (development, testing, product management and operations). It combines focused discussion with hands-on prototyping, so proposed conventions get tested on the spot instead of being imposed.
### Why document API conventions as ADRs?
An ADR captures the context, the alternatives considered and the reasoning behind a decision. That keeps decisions transparent and keeps the knowledge in the team when people leave. Our ADRs now serve as the reference next to the API specifications.
### Why URI-based versioning instead of header-based versioning?
URI-based versioning makes breaking changes explicitly visible to API consumers. We evaluated header-based versioning, but URI versioning worked better with our routing infrastructure and caching.
### Why cursor-based pagination instead of offset-based pagination?
Cursor-based pagination performs better with large datasets and prevents data consistency issues when records change during paging. In our high-throughput scenarios it worked better than offset-based pagination.
---
# The Statically Generated Umbraco Website
- URL: https://berris.dev/nodes/the-statically-generated-umbraco-website/
- Markdown: https://berris.dev/nodes/the-statically-generated-umbraco-website.md
- Author: Roy Berris
- Published: 2023-09-08
- Clusters: Umbraco, Hosting
> How I turned my Umbraco website into a static site with xStatic and free Netlify hosting in under an hour, and who should and shouldn't use this approach.
*Originally published on the old Berris.dev blog in September 2023 and moved here with its original text. The screenshots from the original post didn't survive the move.*
Statically generating websites has been a trend for a while but is often overlooked as an strategy to avoid costs. In this blog post I will explain how I have converted this very website in to a statically generated one, making my hosting free. And it only took me under an hour.
## The Costs of Hosting Umbraco
We might've all been creating Umbraco websites since the dawn of time. A good old server-side rendered, Razor website. This is probably the easiest way to start developing an Umbraco website. And actually, that is exactly what this website is. A big good chunk of code. Traditionally this website would have been hosted on an IIS Server with Umbraco 8 and lower. Which means you would have had to rent a Windows VPS. You would have to do server maintenance to make sure your server is up-to-date. This costs a lot of time, and comes with high subscription costs. Previously I was paying 10 euro's a month for a VPS (which is not too bad) but 18 euro's a month for a Windows license. That comes to a total of 38 euro's per month. That is at least a couple of night of diner.
With the modern Umbraco we have access to .NET 6, which means we could run our Umbraco website on a Linux VPS. Decreasing the costs. But this requires some serious knowledge of Linux and hosting websites 'manually'. Something I don't have... But we can make it even cheaper!
## Introducing Netlify
I've heard people talking about Netlify, but I never understood the power. But not anymore, let me introduce you to free hosting. Well... if you have a static website. Netlify offers a starter plan which is free, it allows you to deploy a static website to their Edge. Even allowing you to bind your own domain to it.
But in short; Netlify is a cloud-based platform that simplifies web development and hosting. It offers features like continuous deployment, automatic scaling, and built-in CDN, making it easy to deploy and manage websites. *~ ChatGPT*
## Static Site Generation
We cannot deploy an Umbraco website to Netlify. That is not how it works. What we can do is generate an Umbraco website to a lot of html files and deploy that to Netlify. It is called static site generation. And it is great, for most people. Because it does come with some costs. It means your website is completely static, there are no more server-side calculations on request. There is no Umbraco Forms, no Umbraco Commerce. Well, even no pagination on overview pages.
So how does it work?
Using the Umbraco plugin xStatic we can generate each of our published nodes to an html page. Just like a running Umbraco website it will include everything you would expect. You can also include your assets like CSS and JS. And it will deploy your media as well. And what you're left with is a bunch of folders and HTML, CSS and JS files. Which is basically what your browser is receiving anyway. Why would we have to render the HTML on every request when we can do it once?
## Working with xStatic
It took me under an hour to set-up xStatic, configure Netlify and have my DNS switched from the old VPS to Netlify. For documentation on how to configure xStatic it is best to look at the documentation, this blog might be outdated soon.
Quick tip: if the xStatic dashboard does not show up, you need to add it to the 'Administrators' role's sections from the Users -> Groups page.
When building a profile you should think about the following things:
- Make sure you have selected all of your image roots
- Set your media crop definitions
- Do not use crop aliases from the media picker\* but directly get the crop URL with a width and height
- You can add your assets recursively with `/assets/*`
- Make sure your root hostname is not / but `localhost:[port]` otherwise it won't generate the right URL's
And probably the best thing is that above set-up was done in under an hour. From finding the package, to serving the website in Netlify. I did not have to change a single thing except for the image cropper.
*\* As of September 2023 the media picker crop alias does not yet work with xStatic, issue raised [here](https://github.com/Mulliman/xStatic-for-Umbraco/issues/25). This also means focus points are not working.*
[Go to GitHub page](https://github.com/Mulliman/xStatic-for-Umbraco)
## Who should benefit?
For small websites like these a statically generated website is perfect. No hosting costs, no need to think about hosting or server maintenance. It just exists. But who shouldn't use this strategy? If you want to include any of below features you should probably not consider this way of working.
- You want to work with Forms
- You want to make a web shop with Commerce
- You want to have overview pages with filters or pagination
- You want to have personalization
Basically anything that requires dynamic server responses should stay away from static site generation.
## Where is Umbraco hosted?
One downside of working with static site generation and absolutely no server is that you don't host your CMS. What I did is install uSync and get my media in Git. This way your whole Umbraco database is in Git. Making it great for source control and you can install your website locally on any machine\*. I can now locally develop and edit my website, which is automatically published to Netlify. Which does mean this approach is perfect for single person blog websites, but not for big content platforms. It is not possible for more than 1 person to edit at the same time.
*\* this won't include the xStatic set-up step, which has to be repeated on every new device.*
## Conclusion
The second I saw the plugin I was convinced. At the moment xStatic still has some bugs which you might find here or there, but it is completely functional. I can definitely recommend this way of working if you don't match the above list. Thank you to the creator of xStatic for bringing the community such a great package.
---
# Using Value Objects in .NET
- URL: https://berris.dev/nodes/using-value-objects-in-net/
- Markdown: https://berris.dev/nodes/using-value-objects-in-net.md
- Author: Roy Berris
- Published: 2023-02-08
- Clusters: .NET, C#, Design Patterns
> What a DDD value object is and how to build one in C#, using a hexadecimal color code with equality by value, a guarded factory method and immutable behavior.
*Originally published on the old Berris.dev blog in February 2023 and moved here with its original text.*
When talking about DDD you might've heard of value objects. What are they and how can we use them?
## What is a Value Object?
When we are working with a domain one might've heard the term "Value Object". But what is this exactly? In this post I will not go in the details of the "what", but straight to the "how". The Microsoft Learn page on this is probably much clearer and to the point than this post can be. But after reading this Learn article and working with value objects in my own solution it becomes clear that there are multiple use cases for this.
But to summarize in my own words: a value object is an immutable, key-less object. It's a semantic bundle of fields which together present one thing. They represent a simple entity whose equality is based on value rather than identity.
[Learn more](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/implement-value-objects)
## Dissecting Microsoft's example
Looking at Microsoft's example with an Address value object it's clear what it does. All fields associated with an address are bundled in to one type and, together, they represent one value. Meaning that if we would change a property in address it would not equal the original anymore.
```csharp
public class Business
{
public string Name { get; set; }
public DateTime FoundedDate { get; set; }
public string AddressStreet { get; set; }
public string AddressHouseNumber { get; set; }
public string AddressCity { get; set; }
public string AddressCountry { get; set; }
}
```
If we would have a class representing a business, and it looks something like shown above, it's logical to bind these fields together in to one class called Address. And to make this work in modern C# and .NET we'll need a base class called ValueObject. So why do we need this exactly?
*Implementation as shown [on Microsoft Learn](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/implement-value-objects#value-object-implementation-in-c) below.*
```csharp
public abstract class ValueObject
{
protected static bool EqualOperator(ValueObject left, ValueObject right)
{
if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
{
return false;
}
return ReferenceEquals(left, right) || left.Equals(right);
}
protected static bool NotEqualOperator(ValueObject left, ValueObject right)
{
return !(EqualOperator(left, right));
}
protected abstract IEnumerable