# Rico Fritzsche > Independent Software Architect and Problem Solver | Event Thinking, Agentic Coding & Architecture Strategy | Author of “Autonomous Domain Capabilities” Public Ghost content for AI and LLM tooling. This file includes a bounded export of public pages first, then recent public posts. Append `.md` to any post or page URL to get the content in Markdown (for example, `/example-post.md`). ## Pages ### Legal Notice URL: https://ricofritzsche.me/legal-notice/ Last updated: 2026-07-07T14:21:23.000Z *Last updated: 7 July 2026* This Legal Notice applies to [https://ricofritzsche.me](https://ricofritzsche.me/?utm%5Fsource=chatgpt.com) and related subdomains operated by Rico Fritzsche. ## 1\. Identification of the Service Provider In accordance with Spanish Law 34/2002 on Information Society Services and Electronic Commerce (LSSI-CE), the owner of this website is: **Rico Fritzsche** NIF/DNI: 8036606Y Address: C/ dels Apuntadors 4, 07012 Palma, Balearic Islands, Spain Email: info (at) ricofritzsche (dot) de ## 2\. Purpose of the Website This website provides articles, information, and resources on software architecture, software development, event sourcing, domain modeling, and related professional topics. It may also offer access to exclusive member content, newsletters, and other professional services. ## 3\. Terms of Use Access to and use of this website implies full acceptance of the terms and conditions set out in this Legal Notice. The user agrees to use the contents and services offered appropriately. ## 4\. Intellectual Property All content on this website (texts, images, code examples, etc.) is the property of Rico Fritzsche or third parties who have authorized its use. Reproduction, distribution, or modification of the content without express authorization from the owner is prohibited. ## 5\. Data Protection The processing of personal data is governed by the Privacy Policy available on this website. ## 6\. Applicable Law and Jurisdiction This Legal Notice is governed by Spanish law. For any dispute arising from the use of this website, the parties submit to the Courts and Tribunals of Palma de Mallorca, Spain. ### Privacy Policy URL: https://ricofritzsche.me/privacy-policy/ Last updated: 2026-07-07T14:23:24.000Z Last updated: July 2026 ## 1\. Who we are This website is operated by Rico Fritzsche, an independent software architect based in Spain. This Privacy Policy applies to the website [https://ricofritzsche.me](https://ricofritzsche.me/) and related subdomains operated by Rico Fritzsche. Contact details: Rico Fritzsche C/ dels Apuntadors 4 07012 Palma, Balearic Islands Spain Email: info (at) ricofritzsche (dot) de ## 2\. What data we collect and why We collect personal data when you voluntarily interact with this website, for example when you: - subscribe to the newsletter; - create or access a member account; - access member-only content; - contact us by email or through a form; - make payments or manage memberships through Stripe. The personal data we may collect includes your name, email address, membership status, payment-related information, and the content of messages you send to us. We use this data to: - send newsletters and updates; - provide access to member content; - manage memberships and subscriptions; - respond to your inquiries; - process payments securely; - operate, protect, and improve this website. The lawful bases for processing are: - **Consent** — for newsletter subscriptions and other optional communications. - **Contract** — for creating and managing member accounts, paid memberships, and access to member-only content. - **Legal obligation** — for accounting, tax, and invoicing records where required by law. - **Legitimate interest** — for operating, securing, maintaining, and improving this website, including privacy-friendly analytics. You can withdraw your consent for newsletters at any time by using the unsubscribe link in the email or by contacting us. ## 3\. Analytics We use self-hosted Umami for website analytics. Umami is configured to be privacy-friendly. It does not use cookies, does not track users across websites, and only collects anonymized usage data, such as pages visited, browser type, device type, referrer, and general location. We use this information to understand how the website is used and to improve its content and performance. ## 4\. Cookies This website does not use cookies for advertising or cross-site tracking. Essential cookies or similar technologies may be used where necessary for login, membership access, security, payment-related functionality, or to operate the website correctly. ## 5\. Third-party services and processors To provide certain features, we use the following third-party services and resources: - **Ghost** — content management system, membership platform, and newsletter functionality. - **Stripe** — payment processing for memberships and subscriptions. - **cdnjs / Cloudflare** — content delivery network used to load Prism.js for syntax highlighting and Font Awesome for brand icons. - **Self-hosted Umami** — privacy-friendly analytics, as described above. - **Email delivery services** — used to send newsletters, login links, and transactional emails. These services may receive limited personal or technical information, such as your email address, IP address, browser data, payment information, or membership status, only where necessary to provide the relevant service. We do not sell your personal data. ## 6\. International transfers Some service providers may process data outside the European Economic Area. Where this happens, we rely on appropriate safeguards, such as standard contractual clauses or other legally recognized transfer mechanisms. ## 7\. Your rights under GDPR You have the following rights regarding your personal data: - right to access your data; - right to rectify inaccurate data; - right to erase your data; - right to restrict processing; - right to object to processing; - right to data portability; - right to withdraw consent at any time; - right to lodge a complaint with a supervisory authority. To exercise any of these rights, please contact us at: Email: info (at) ricofritzsche (dot) de You also have the right to lodge a complaint with the Spanish Data Protection Agency, the **Agencia Española de Protección de Datos (AEPD)**. ## 8\. Data retention We keep your personal data only for as long as necessary for the purposes described in this policy. Newsletter data is kept until you unsubscribe or ask us to delete it. Membership data is kept for as long as your account or membership exists and for a reasonable period afterwards where necessary for security, support, or legal purposes. Payment, invoice, accounting, and tax-related records may be kept for the period required by applicable law. Technical logs and analytics data are kept only as long as necessary to operate, secure, and improve the website. ## 9\. Changes to this policy We may update this Privacy Policy from time to time. The most recent version will always be available on this page. ### Terms of Service URL: https://ricofritzsche.me/terms-of-service/ Last updated: 2026-07-07T14:21:09.000Z Last updated: July 2026 ## 1\. Introduction These Terms of Service ("Terms") govern your access to and use of the website located at [https://ricofritzsche.me](https://ricofritzsche.me/) (the "Website") and related subdomains, operated by Rico Fritzsche ("we", "us", or "our"), an independent software architect based in Palma, Spain. By accessing or using the Website, you agree to be bound by these Terms. If you do not agree, please do not use the Website. ## 2\. Description of the Service The Website provides articles, resources, and information on software architecture, software development, and related topics. We may also offer optional membership access to exclusive content, resources, or other digital content. ## 3\. Memberships and Payments Certain content on the Website may be available only to paid members. Memberships are managed through Ghost and payments are processed securely by Stripe. By subscribing to a membership, you agree to pay the applicable fees. Memberships are recurring unless cancelled. You can cancel your membership at any time through your account settings. Cancellations take effect at the end of the current billing period. No refunds are provided for partial billing periods, except where required by law. ## 4\. Digital Content and Right of Withdrawal Paid memberships may provide access to digital content immediately after purchase. By subscribing to a paid membership and accessing member-only content, you agree that the digital content may be made available to you immediately. Where permitted by applicable consumer law, you acknowledge that your statutory right of withdrawal may be lost once access to the digital content has started with your consent. This does not affect any mandatory rights you may have under applicable law. ## 5\. Intellectual Property All content on this Website, including but not limited to articles, code examples, images, and resources, is the intellectual property of Rico Fritzsche or its licensors. You may read and share links to public articles for personal, non-commercial use. You may not: - copy, reproduce, distribute, or modify our content without prior written permission; - use our content for commercial purposes without explicit authorization; - share, republish, or resell member-only content; - use member-only content to train, fine-tune, or build commercial AI systems without explicit written permission. ## 6\. Acceptable Use You agree not to: - use the Website for any unlawful purpose; - attempt to gain unauthorized access to member-only content or accounts; - share your membership login credentials with others; - post or transmit harmful, abusive, misleading, or illegal content through comments, forms, or other interactive features, if available; - interfere with the security, availability, or proper operation of the Website. We reserve the right to suspend or terminate access to any user who violates these Terms. ## 7\. Limitation of Liability The Website and its content are provided "as is" without warranties of any kind. The content is provided for general informational and educational purposes only. It does not constitute legal, financial, professional, or consulting advice. To the maximum extent permitted by law, we shall not be liable for any indirect, incidental, special, or consequential damages arising from your use of the Website or membership services. Nothing in these Terms limits any liability that cannot be excluded or limited under applicable law. ## 8\. Termination We may terminate or suspend your access to the Website or your membership if we believe you have violated these Terms. Upon termination, your right to access member-only content will end. Termination does not affect payment obligations already incurred or any rights that must remain available under applicable law. ## 9\. Changes to These Terms We may update these Terms from time to time. The most recent version will always be available on this page. Continued use of the Website after changes constitutes acceptance of the updated Terms. For material changes affecting paid memberships, we will take reasonable steps to inform affected members where required. ## 10\. Governing Law These Terms are governed by the laws of Spain. If you are a consumer resident in the European Union, you may also benefit from mandatory consumer protection rights available under the laws of your country of residence. Any disputes arising from these Terms shall be subject to the courts of Palma de Mallorca, Spain, unless mandatory consumer protection law provides otherwise. ## 11\. Contact If you have any questions about these Terms, please contact us at: Email: info (at) ricofritzsche (dot) de Rico Fritzsche C/ dels Apuntadors 4 07012 Palma, Balearic Islands Spain ### I don't sell an architecture. I find the one your domain needs. URL: https://ricofritzsche.me/software-architecture-consulting-workshops/ Last updated: 2026-07-22T15:15:53.000Z The business describes its work one way; the software is structured another. Every requirement crosses that gap, and the crossing is where the cost lives: studies attribute half or more of all defects and the majority of rework not to bad code, but to software that doesn't match how the business actually works. This is also the part AI tools don't fix. Coding agents accelerate code, and Gartner expects 80% of technical debt to be architectural by 2027\. The gap is structural, and from inside, it's invisible. ## Why this happens Many systems are designed and modeled from the wrong starting point. Teams begin with data structures: tables, entities, a snapshot of things. The business doesn't work in snapshots. Business people think in flows: things that happen in real business processes, such as "an order is placed" or "a contract is terminated". These are decisions and their consequences. When the software is structured around static data models and technical layers while the business runs on behavior, the mismatch is built in from the first minute. I call what follows domain drift. Every new requirement arrives in the language of behavior and has to be translated into a structure that doesn't speak it. The translation happens in developers' heads, is never written down, and gets a little harder each time. Inside the team, nobody notices, because everyone has learned to translate. What they notice is the estimates getting worse. My work starts on the other side. Before any technical question, I analyze what actually happens in the business: the events, and the decisions behind them. The analysis is always the same. The architecture that comes out of it is not. I don't recommend Event Sourcing because it's trending, and I don't sell it as dogma. It has to be justified by the domain. **The architecture is the output of the diagnosis, not the input.** ## Why me Domain drift is hard to see from inside a team and easy to see from outside, if you've watched it happen often enough. I have built software for more than 30 years, in many industries, and the same patterns repeat everywhere: the shortcut that looks harmless, the abstraction that quietly becomes a dependency, the model that stopped matching the business two years ago. I recognize them early because I've seen how each one ends. I'm not an advisor who left the code behind. I still design and build systems myself, from domain models to distributed architectures and high-volume data pipelines, and I've worked with Domain-Driven Design long enough to know what holds up in practice and what only works in books. That depth matters for one reason: my recommendations have to survive contact with real code, real data, and a real team under deadline. Advice that can't is worse than no advice. What you get from me is a diagnosis you can act on, in plain language, with the reasoning laid open so your own people can carry it forward. ## Where my thinking is public I don't ask you to take the diagnosis on trust. My reasoning is published and you can check it before we ever speak. I write regularly about software architecture at ricofritzsche.me, where I work through real design problems in the open: consistency boundaries, domain modeling, distributed systems, the trade-offs behind event-driven designs. Together with Ralf Westphal, I developed Command Context Consistency, an approach that scopes consistency to the facts a business decision actually needs. I'm also building FACTSTR, an event store, because I hold my own ideas to the standard of running software. Read a few articles. If the way I reason about systems doesn't convince you, no call will. Most systems are modeled from the wrong starting point. Teams begin with data: tables, entities, a snapshot of things. The business doesn't work in snapshots. But business works different. Business people think different. They think in flows of things that happens in the real world business processes, i.e. "an order is placed", or "a contract is terminated". These are decisions and their consequences. When the software is structured around static data models and technical layers while the business runs on behavior, the mismatch is built in from the first minute. I call what follows domain drift. Every new requirement arrives in the language of behavior and has to be translated into a structure that doesn't speak it. The translation happens in developers' heads, is never written down, and gets a little harder each time. Inside the team, nobody notices, because everyone has learned to translate. What they notice is the estimates getting worse. My work starts on the other side. Before any technical question, I analyze what actually happens in the business: the events, and the decisions behind them. The analysis is always the same but the architecture that comes out of it is not. I do not provide Event Sourcing as a dogma and because it's trending currently. It must be justified with for a good reason. The architecture is the output of the diagnosis, not the input. Complex software rarely fails because one pattern is missing. It fails because important decisions stay unclear for too long: what belongs together, what should be separated, where critical business state is protected, how teams can change the system safely, and what really happens when processes fail, retry, overlap, or run for days. These problems are easy to miss in demos, prototypes, and early delivery. They become expensive in production. I help CTOs, engineering leaders, and developer teams challenge architectural assumptions before they turn into operational risk. With more than 30 years in software development and architecture, I bring an external view focused on clarity, consequences, and practical decisions — not methodology for its own sake. The goal: make the architecture easier to reason about, expose hidden risks, and give your team a clearer basis for the next decision. **Ready to get an independent perspective before committing to a major path?** [Book a Free 20-Minute Architecture Discovery Call](https://outlook.office.com/book/SoftwareArchitectureConsulting@ricofritzsche.de/?ismsaljsauthenabled&ref=ricofritzsche.me) ## When This Helps Bring me in when an architectural decision is important enough that guessing is too expensive. Typical situations: - Moving from prototype, PoC, or early delivery into real production - The system works, but it is becoming harder to change safely - Teams disagree about boundaries, ownership, data, or responsibilities - A modernization effort risks adding another layer of complexity - AI-assisted development is increasing delivery speed, but also the need for clear architectural direction - Leadership needs an independent senior perspective before committing to a major technical path I do not replace your team’s judgment. I help sharpen it. ## What I Review I focus on the parts of the architecture where mistakes become expensive — not whether the diagram looks clean or follows a fashionable pattern. The questions that matter: - Are system boundaries clear enough? - Does each part have a clear responsibility? - Is critical business data owned and protected in the right place? - Can teams change important parts without creating hidden side effects? - What happens when processes fail, retry, overlap, or run longer than expected? - Where is the architecture adding unnecessary complexity? - Which decisions are reversible, and which will be expensive to change later? - Does the architecture fit the organization that has to build and operate it? The goal is to expose the few decisions that really determine whether a system stays understandable, adaptable, and safe to operate over time. ## How the Review Works A useful architecture review does not require weeks of preparation. It needs the right context, the right people, and a clear question: *What decision are you about to make, and what could go wrong if the architecture is not strong enough?* The process is simple: 1. **Discovery Call** — Short free call to understand your situation, the importance of the decision, and whether I can add value. 2. **Context** — You share existing material: diagrams, short descriptions, decision notes, or problem areas. No polished architecture document required. 3. **Review** — We review the architecture together with the people responsible. I challenge assumptions and focus on risks that are easy to miss from inside the project. 4. **Outcome** — You leave with a clearer view of architectural risks, trade-offs, and next decisions — so your team can move forward with more confidence. ## Ways to Work Together Every engagement starts with a free discovery call to clarify the situation and decide whether an external review is the right next step. ### Focused Architecture Review Best when you already have a system, proposal, prototype, or direction and want to challenge it before going further. - Remote session - Prepared from existing material - Focused discussion with the responsible people - Clear feedback on risks, trade-offs, and next decisions ### Architecture Review Workshop Best for larger or mission-critical decisions where the architecture needs review with the senior team. - Half-day or full-day workshop (online or in person) - Review of current architecture and key assumptions - Discussion of risks, alternatives, and consequences ### Ongoing Architecture Advisory Best when architectural decisions are continuous and the team benefits from regular external input. - Regular advisory sessions - Review of important decisions - Feedback on emerging risks - Support for keeping the architecture understandable and changeable Written summaries or follow-up sessions can be added when useful. ## Why Work With Me I have spent more than 30 years building, designing, reviewing, and challenging software systems. My strength is not a specific framework or technology stack — it is helping teams see the architectural consequences of their decisions more clearly: - What will become hard to change? - Where will complexity accumulate? - Which assumptions are risky? - Which boundaries are unclear? - Where does the system depend on coordination, discipline, or luck? - What will fail first when scale, time, or organizational pressure increases? I bring an independent senior perspective to systems where the cost of being wrong is high. The goal is not to impress your team with theory. The goal is to help your team make better decisions. **Ready to challenge your architecture before it becomes expensive to change?** Book a free 20-minute Architecture Discovery Call. We’ll clarify your situation, the decision or risk you’re facing, and whether an external review would be useful. This call is a focused conversation to understand what you’re building, why the decision matters now, what concerns you already see, and what kind of review format would make sense. If there’s a good fit, we define the scope and next step. [Book Your Free Discovery Call](https://outlook.office.com/book/SoftwareArchitectureConsulting@ricofritzsche.de/?ref=ricofritzsche.me) ### Contact URL: https://ricofritzsche.me/contact/ Last updated: 2026-08-31T10:49:10.000Z ### Let me know how I can help you! - [ rico@ricofritzsche.me ](mailto:rico@ricofritzsche.me) - [ (+49) 176 1432 1000 ](tel:+4917614321000) Name Email Message Send ## Posts ### How Event Sourcing Grows With the Business URL: https://ricofritzsche.me/how-event-sourcing-grows-with-the-business/ Last updated: 2026-08-30T21:49:46.000Z The database schema is the answer to a question that usually cannot be answered at the start of development. Tables that were originally intended to store only data records often end up with additional status, audit, and date columns over time. This is the typical outcome when modeling begins with an entity-centric approach and it is gradually realized that the business has a process. [In my last article,](https://ricofritzsche.me/why-the-entity-model-is-an-illusion/) I wrote about how much entity-centric thinking has shaped software development over the past few decades. This approach contrasts with process-oriented approaches, which focus on business events. In the entity-centric approach, more or less rigid data structures define the business. In many cases, this leads to thinking in terms of typical database operations—Create, Read, Update, and Delete (CRUD)—because the behavior is tied to a data structure that must be kept up-to-date and consistent. As a result, the focus quickly shifts away from business processes and the domain language. I will continue to use the example from the car rental industry that I discussed in my last article. Upon closer, process-oriented examination, the typical, general, database-oriented requirement “Create something” broke down into four steps: ``` Acquire → Receive → Infleet → Release ``` This article adds a step after the system is already up and running: ``` Acquire → Receive → Inspect → Infleet → Release ``` That shows how systems in the real world emerge and grow, and it lets you count what has to change and what does not. ## The Problem of Mutable Data In many applications, the database schema takes center stage. At this point, I no longer distinguish between the data model in the database and the so-called domain model in the code. They are two representations of the same thing. As soon as an ORM maps the object to a row, both have the same operations, and both are based on the idea of adding, updating, or deleting data. Systems built this way rely on redundancy—duplicating the same schema and, for every operation, loading data into memory, mutating it, and then writing it back to the physical data storage. This raises the legitimate question: Why not just do this directly in the database? Rules alone do not justify an application around a few tables; modern databases can enforce constraints too, and a thin CRUD app can expose the tables over HTTP. The only question is whether the business consists of such operations. The vehicle from the last article initially looked like that. CRUD is based on the principle of maintaining the most recent state. This most recent state must fit completely within the schema at all times. As a result, the schema must be constantly modified. I know we have good tools to manage this. But it introduces technical dependencies into development that we actually wouldn’t need at all if data were fundamentally immutable. A table, such as “Vehicle” from my last article, with columns—including a status column—encodes, in one place, the possible states and the data associated with a vehicle. Pat Helland explained in 2015 why this is necessary: Normalization exists to prevent update anomalies. A schema designed to handle updates must therefore be complete before the first record is written. This is not necessary for immutable data. ## Too Late for the Schema The problem is that development almost always begins with assumptions. As I pointed out in my last article, one might well think that a new vehicle simply needs to be created as a data record in the system. But that’s not how it works. Three decades of experience have shown this time and again. Even if the expert thinks we just need to create the vehicle record and doesn’t mention status transitions, these eventually come to light because it becomes clear that the process wasn’t mapped at all. What happens then? You start adding columns to the table—such as date columns showing when the vehicle was ordered, when it was delivered, and so on. Some people will surely say: So what? That’s simple. Just business as usual, so to speak. Technically speaking, I agree. But the process isn’t visible. No one can tell from the schema in what order the date columns are filled or which ones are allowed to remain empty. That’s specified in the code, scattered across all the places where the table is written. Every new step is a change to a shared structure, because the database represents a global, shared, mutable state. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2026/08/Vehicle-Acquisition-and-2026-08-30-174950.png) Figure 1: Table versus event types over three revisions. Martin Kleppmann made this point back in 2014: developers have long since done away with global variables, but the database is one giant global variable. The typical discovery made when dealing with a business comes too late for the centralized schema. Always! ## Event Modeling The solution is to take a truly iterative approach: talk to the domain experts about their business in their language, identify and model domain events, and implement them in small, manageable units. Event modeling offers the right approach here. Adam Dymitruk described the method in 2019\. You create a timeline and use the domain language to document what happens in the process: a vehicle was procured, it was delivered, it was added to the fleet, it was made available for rental. Only then does the question arise as to which action triggers an event and who wants to see what information afterward. Every action that leads to an event is a domain capability. *ReceiveVehicle* is one; *InfleetVehicle* is another. The names come in pairs. For example, the capability *InfleetVehicle* produces the *VehicleInfleeted* event, and the *InspectVehicle* capability produces the *VehicleInspected* event. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2026/08/Capability-Event-Flow-Model-2026-08-30-180917.png) Figure 2: The names come in pairs. A domain capability is one ability a domain provides. This capability is implemented as a Request Processing Unit (RPU). In this article I stay with *capability*, because the point here is what the domain can do and what data it keeps, not how the unit is built. These can be implemented without everything having to be known in advance. *AcquireVehicle* can be built and delivered before anyone knows what the release for rental will look like later. The data structures then each refer to a capability and are immutable. ## The Fold Each domain capability comes with its own command context. The context defines the scope and what is needed to process a request. To get the relevant context from the Application State (the immutable chronologically ordered sequence of events), the capability has to query a relevant list of events and derive the state from it. This functional operation is called a fold: the chronological list of events is gradually reduced to a single, current state. Greg Young wrote in 2012: *"Current State is a Left Fold of previous behaviours."* ```csharp var state = events.Aggregate(InfleetState.Empty, (s, e) => e switch { VehicleReceived r => s with { Received = true, Vin = r.Vin }, VehicleInfleeted => s with { Infleeted = true }, _ => s }); ``` For *InfleetVehicle*, the only relevant information is whether the vehicle has been delivered and whether it is already in the fleet. The derived state does not need to know anything else in this context, because that information is not required to process the command. Events that the rules do not read are not taken into account. The state exists for as long as the command is being processed: it is created, the rules are applied to it, and the result is one or more new events. For *ReleaseVehicleForRental*, different information is required in the state, which means the fold is based on different event records, even though both could, in principle, read the same events. There is no shared vehicle object that both would need to share. ## The Fifth Step For events, the schema consists of a set of event types, and that set only grows. A new process step represents an addition; existing facts do not need to be reinterpreted. Adding *VehicleInspected* involves defining an event type, implementing a new specific function, and modifying existing functions only if the information is relevant to the context. This could be *InfleetVehicle*, for example. ```csharp // fold in InfleetVehicle VehicleReceived r => s with { Received = true, Vin = r.Vin }, VehicleInspected => s with { Inspected = true }, // new VehicleInfleeted => s with { Infleeted = true }, // rule in InfleetVehicle if (!state.Received) return Rejected("vehicle_not_received"); if (!state.Inspected) return Rejected("vehicle_not_inspected"); // new ``` To integrate the event into an existing capability, you simply need to adjust the fold so that the state contains this new information, allowing the processing to work with it. *AcquireVehicle* and *ReceiveVehicle*, on the other hand, remain unchanged, since they do not require the information from the new event type. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2026/08/Vehicle-Acquisition-Pipeline-2026-08-30-174415.png) Figure 3: The timeline with the inserted step. Event records are persistent and accumulative, not subject to deletion or overwriting. Rich Hickey put it this way in 2012, referring to accounting, court records, and any other record that is meant to be trusted. This is the mechanism on which growing systems should be based. Extending a domain to include additional capabilities does not alter existing event records. They do not become invalid as a result. In the case of our “Vehicles” domain, this means that vehicles already in the fleet simply do not have a *VehicleInspected* event. The “Fold” function must, if necessary, account for this omission in the function where this information is required. For vehicles already added to the fleet, however, this information is no longer relevant because the *InfleetVehicle* process step has already been completed. New vehicles that have been delivered but not yet added to the fleet must now wait for inspection, which was the intent of this process step. This eliminates the need for data migration, since there is nothing to correct. By that I mean the stored data; it is not necessary to rewrite events or enter them retroactively. A projection for displaying the inspection, on the other hand, is new code with its own rebuild, and the functions do not depend on it. For a status column, however, you must specify which of the new values should be assigned to the individual old rows. The reason why extending an application with new process steps is so simple is easy to see: each function derives its own state from the events. The autonomy of the functions and the immutability of the events are two sides of the same coin: events are additive, since no reader needs to know the big picture. Functions, on the other hand, remain small because they derive their specific perspective precisely from the events. ## History Is a Consequence, Not a Cause In my experience, the use of event sourcing is often justified on the grounds that it provides a complete history. Conversely, it is argued that event sourcing would not be necessary if a history were not needed. I consider this to be fundamentally wrong. Why? Because the fact that the history is preserved is a consequence of that very property, but it is not the reason for it. Anyone who views event sourcing as an audit function has misunderstood its purpose. For me, the real motivation is to provide domain capabilities that are independent of one another and not tied to a central, shared data structure. At the same time, this avoids entity-oriented thinking. By using events, the domain language is brought to the forefront, and the actual processes become apparent. In the example, we saw that adding a new domain capability—via a new command, a new event type, and new logic—is done autonomously and with minimal risk, without having to modify existing functionality. This is only necessary if the new event is required within an existing capability to extend the context-bound state. This intervention is minimal. Extensions are therefore additive. Schema migrations, as we know them from centralized, entity-based systems, are not required. It was therefore not necessary to modify existing data in order to introduce a new capability. *Cheers*! Sources and Links: - Pat Helland, [Immutability Changes Everything](https://queue.acm.org/detail.cfm?id=2884038&utm%5Fsource=ricofritzsche.me) (CIDR 2015, reprinted in ACM Queue) - Martin Kleppmann, [Turning the database inside-out](https://martin.kleppmann.com/2015/03/04/turning-the-database-inside-out.html?utm%5Fsource=ricofritzsche.me) (transcript of the 2014 Strange Loop talk) - Rich Hickey, [The Value of Values](https://www.infoq.com/presentations/Value-Values/?utm%5Fsource=ricofritzsche.me) (GOTO Copenhagen 2012, InfoQ recording) - Adam Dymitruk, E[vent Modeling: What is it? ](https://eventmodeling.org/posts/what-is-event-modeling/?utm%5Fsource=ricofritzsche.me)(June 2019) - Greg Young, [Functional Domain Models and Event Sourcing (October 2012)](https://gregfyoung.wordpress.com/2012/10/01/functional-domain-models-and-event-sourcing/?utm%5Fsource=ricofritzsche.me) [![ai-free.io text label](https://ai-free.io/AI-free.io-TEXT.png)](https://ai-free.io/?ref=ricofritzsche.me) ### Why the Entity Model Is an Illusion URL: https://ricofritzsche.me/why-the-entity-model-is-an-illusion/ Last updated: 2026-08-26T20:23:03.000Z The field of enterprise software development is characterized by centralized data models and structures, object-oriented programming in the style of languages like C++, Java, and C#, bloated frameworks, relational databases, and, of course, a whole lot of unnecessary complexity. At first, this might sound like I’m just spouting off. But I come from this world and know the struggles it causes. And regardless of the possibilities for deploying AI agents, complexity—especially unnecessary complexity—is the main reason why software development remains cumbersome, slow, and expensive. #### Conditioned to Central Data Models In my day-to-day work, I still see how deeply developers, teams, and often even business experts are conditioned to think in terms of central data models. It seems that the prevailing belief is that once we’ve stored the data in a normalized database, the problem is largely solved. But is that really the case? I am now firmly convinced, more than ever before, that for years we’ve been laboring under the fallacy that we must model objects that represent the real world, endow them with properties and behavior, and make them capable of changing on their own. Trapped in this way of thinking, we often forget that things happen and that business processes consist of more than just an input form. Things that happen often have an impact on various other things, without the events themselves having any idea what they’re causing. I don’t think we need entities that we have to define in advance. That limits us too much. It makes systems rigid and inflexible. We can’t just start with the knowledge we have and use real feedback to see if we’re building the right thing. The standard software development process is still, to this day, a process based on assumptions. And the longer the process takes, the more the product deviates from reality. #### Two Worlds And it doesn’t help much if the customer can only test the system with test data. Why? Because they don’t care whether it works with test data. I’ve seen this happen too often: the bugs weren’t noticed until the system went into production and was handling real data. That’s because business people think differently and see value in their data and workflows. Software developers live in a different world. They want to apply patterns, use new technology, and write beautiful code. But if the system follows SOLID and the Hexagonal Architecture, then it must be good. That’s the point! It doesn’t matter which patterns are used. Added value must be created. And when two worlds collide—worlds with completely different worldviews that exchange Jira tickets for months on end—the disconnect can only grow over time. Okay, what am I getting at? We need fewer dependencies at various levels. Thinking in terms of flows, processes, and events is a central element. The real world consists of events that occur somewhere and at some point; their consequences are unknown to the event itself. #### What Actually Happens Let’s say we want to build a system for managing and renting vehicles. We can discuss many things with the business experts here, but we don’t need to know everything to start building the product. The core idea is to deliver capabilities in small, independent steps and receive immediate feedback. Perhaps we’ll first talk with the domain expert about adding new vehicles. At this stage, we need to avoid using data-processing terminology. Of course, I know from experience that you’ll often end up in contact with the IT people, who naturally already think they know that this needs to be stored in the “Vehicle” table and has already prepared a data model. That’s counterproductive because it immediately takes you into the “how” phase. It often takes a lot of effort to make it clear at this stage that it doesn’t matter how the data is persisted. We’re mapping processes, not a central data model. To have a finished data model, we’d need to know every aspect—which is rarely the case. And we’d also have to anticipate things that aren’t even relevant right now. This limits flexibility, even for future changes. So the correct question are: What does the initial registration of a new vehicle look like? What actually happens there? Here, you’ll quickly realize that it’s not “Create Vehicle”, because that merely describes a database operation anyway. After all, the car rental company doesn’t actually “create” a vehicle. Perhaps if you set aside all these typical data-processing terms, you’ll come to understand the real process. Presumably, the registration of a new vehicle turns out to be a sequence of domain events: ```plaintext VehicleAcquired → VehicleReceived → VehicleInfleeted → VehicleReleasedForRental ``` With each event, the system is enriched with new information. #### The Vehicle Is a Projection Now one might ask, but what exactly is the “Vehicle”? From my current perspective, it is not an entity in our software. It is something that can appear in different forms as a projection. And that’s very simple. Because perhaps someone in the company asks for a view of the system where all received vehicles should be listed. In that case, there is a “Received Vehicles” projection with the relevant data. This perspective on software development has significant implications. We don’t need to know how information should be evaluated or displayed. We can address that when it’s requested. We can focus on a single capability, implement it in a targeted manner, and then immediately gather user feedback. A domain capability is something like “AcquireVehicle” or “ReceiveVehicle.” They are independent of one another. They are autonomous, self-contained, and coherent. They share only the Application State—in this implementation, an Event Store and the event definitions. However, they know nothing about each other. But how do we know which processes belong together? Let's say we start by implementing "Acquire Vehicle," and when the vehicle arrives, we need a mapping—a reference to the vehicle that was ordered. Well, that's quite easy to solve. The reference can be an artificial identifier, such as an ID, or an immutable, unique, natural attribute. For a vehicle, this could be the so-called VIN. ```plaintext { "eventType": "VehicleAcquired", "occurredAt": "2026-08-20T10:15:00Z", "data": { "vehicleId": "VEH-1042", "vin": "WVWZZZCDZRW123456", "acquisitionType": "Lease", "vehicleModel": "Volkswagen Golf" } } ``` In my opinion, the existence of a unique identifier does not automatically constitute an entity in the sense of a specific data object. The vehicle itself is an entity because it physically exists. In our domain, it is a reference. From the events associated with the reference, one can deduce the state that possibly describes the physical entity at a specific point in time. #### Conclusion As we can see, data structuring remains important—but in a different way. Data is immutable and can only be superseded by a new version. The key point is that retrieving entities from a database to mutate them in memory and then write them back is an ineffective approach because it costs us two things: 1) It destroys the information about what happened, 2) it turns every concurrent request into a conflict we have to defend against. So why do I say the entity is an illusion? The database contains one row for each vehicle. The row resembles the vehicle itself, and is actually only the result of the last write. It holds the values that survived the last update and says nothing about how they got there. The reason why almost every system has audit tables, history tables, status columns, timestamps and change logs is a workaround to keep this information. What matters is the connection to the real world, which is established through an artificial or natural reference. Against that reference we record what happened. Within a domain, there are different interests, which is why perspectives on a “thing” vary and there isn’t just one “right” way. To stick with the example of a vehicle, it’s simply a different matter from the perspective of purchasing versus that of the repair shop. Every form that someone needs is derived from these events. It is a projection. *Cheers*! [![ai-free.io text label](https://ai-free.io/AI-free.io-TEXT.png)](https://ai-free.io/?ref=ricofritzsche.me) ### Why SOLID Is Outdated URL: https://ricofritzsche.me/why-solid-is-outdated/ Last updated: 2026-08-01T21:19:02.000Z I recommended SOLID for years. I was wrong. The principles were priced for a world of hour-long compiles and risky releases. That world ended many years ago; the rules stayed. Enterprise software development has treated these five letters as settled truth for twenty years: they shape code reviews, job interviews, and training material in C#, Java, and TypeScript alike. In my own use, AI coding agents reproduce the same structures unless the repository gives them a different design policy. A beginner sees `IRepository` and learns that an interface proves good design. A reviewer sees a changed `switch` and asks for a strategy hierarchy. The abstractions arrive before the code contains a boundary worth protecting. The precise claim: > **SOLID is wrong as a general design doctrine and as a default checklist.** The tooling argument names the smaller problem. The deeper defect was present from the beginning: the principles never contained the facts required to decide where an abstraction pays for itself. Several ideas may remain useful under very specific constraints, and I no longer accept a bare principle as a review argument. I make the case in C#, where I know the habits best; the argument does not depend on the language. If this challenged a default, subscribe. The next one will too. [Subscribe for free ](#/portal/signup/free) ## Five Letters from Different Eras SOLID combines ideas written at different times for different problems. Barbara Liskov described the rule for behavioral sub-typing now called the Liskov Substitution Principle (LSP) in 1987, later developed with Jeannette Wing. Bertrand Meyer named the Open/Closed Principle (OCP) in the 1988 first edition of *Object-Oriented Software Construction*. Robert C. Martin collected OCP and LSP together with his own Dependency Inversion Principle (DIP) and Interface Segregation Principle (ISP) in his 2000 paper *Design Principles and Design Patterns*, and gave the Single Responsibility Principle (SRP), which he had named in the late 1990s, its own chapter in his 2002 book *Agile Software Development, Principles, Patterns, and Practices*. Martin later recalled that Michael Feathers pointed out the SOLID ordering in an email around 2004. The acronym puts a sub-type contract, a cohesion rule, an extension policy, an interface technique, and a dependency rule at the same level. They address different problems and need different preconditions. Four of the five share a second defect: no failure signal. A team applying SRP, OCP, ISP, or DIP to the letter gets no indication when the rule is damaging the design; every split, every extension point, and every interface counts as compliance. A rule that calls every outcome compliance guides nothing. LSP is the exception. A broken sub-type contract produces wrong behavior a test can catch. Martin argued in 2020 that SOLID remains current because software still consists of sequence, selection, and iteration. That defense covers what programs are made of. The original case for the principles rested on what change cost, and that cost depends on the surrounding tool chain. A language-aware rename can update statically known symbol references within a solution; modern version control makes small edits recoverable; CI can build and run `dotnet test` on every pull request. Configuration strings, reflection, generated code, and external consumers still need separate checks. These tools shorten feedback on revised source. Tests report only the behavior they exercise. ## Open/Closed Is a Bet on Prediction Martin's 2000 paper uses a slow compile and a source-control check-in taking hours as examples of what it calls environment viscosity. It also presents OCP as the most important object-oriented design principle: change what a module does by adding code while leaving its existing source alone. Dan North connects OCP directly to that economics. In the 1990s, large C++ builds were expensive, automated refactoring was uncommon outside [Smalltalk](https://en.wikipedia.org/wiki/Smalltalk?ref=ricofritzsche.me), and file-based version control made renames painful. Preserving working source was a sensible risk policy. The prescription built on that policy was weaker from the start. Closing a module requires knowing where change will arrive, and Martin's 1996 article concedes the point: closure *"must be strategic"*, and choosing what to close against *"takes a certain amount of prescience derived from experience"*. Prescience is a guess with no test. An extension point built for a variation that never came was dead structure in 1996 exactly as it is today; the era's economics only decided which mistake cost more. Modern tooling has reduced the cost of changing team-owned source. A published NuGet API, a plug-in contract, an event schema, or a service consumed by another team can still be expensive to change because its callers cannot move with it. Those boundaries justify versions and extension points. Martin made a similar qualification in 2013: get the code into a position where behavior changing in expected ways does not force sweeping changes across the system's modules. He pointed back to his 1996 article, which describes strategic closure against certain kinds of change. The design questions are concrete: can the module and all its callers ship together? Must several behaviors coexist at runtime? How often has this variation occurred? ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2026/08/Screenshot-2026-08-01-at-19.49.11.png) One yes is enough to justify the contract; otherwise direct change is cheaper. Inside one application, speculative closure has a recurring cost. A second discount type is predicted, so `IDiscountRule`, a selector, registrations, implementations, and mock-based tests appear around one calculation. The predicted variation may never arrive. A later rounding change edits the implementation anyway. The wrong guess stays in the code-base, together with every concept it brought. North calls this the "Cruft Accretion Principle". I change the local code first and introduce an extension point when callers change independently, several behaviors must coexist, or the same variation returns often enough to establish a stable contract. ## "One Reason to Change" Decides Nothing SRP says that a module should have *"one and only one reason to change"*. Apply that sentence to invoice code and count the reasons: tax rules change, the PDF layout changes, the database that stores the invoices changes, finance requests one adjustment and compliance requests another. A team can split the code by technical concern, stakeholder, business capability, or deployment ownership and claim SRP each time. The sentence permits all of these boundaries. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2026/08/Screenshot-2026-08-01-at-19.53.06.png) Each boundary satisfies "one reason to change"; the sentence does not choose between them. In common .NET code bases and tutorials, the typical approach is (and always has been): one class per technical concern; for example, a calculator class, a validator class, a mapper class, a formatter class and a repository class. One product change then crossed all five classes. The code had many responsibilities according to the folder structure and one responsibility according to the business request. The rule approved that structure. A sentence that accepts every boundary cannot reject a harmful one. Martin's 2014 explanation makes the stakeholder interpretation explicit: *"This principle is about people."* A module should answer to one tightly coupled group that requests changes. That is more specific than the slogan, and it still leaves a choice when one product team owns pricing, presentation, and storage together. David Parnas gave a concrete decomposition criterion in 1972\. Start with the difficult design decisions and the decisions likely to change. Give each module one of those decisions to hide. For the invoice example, *"how we calculate the amount owed"* is such a decision. Its inputs, rules, and outcome belong together even when the implementation needs more than one small class. John Ousterhout adds a useful test: a good module hides substantial complexity behind a simple interface. Every class and method introduces another interface for a reader to learn. Splitting code improves the design when the new module hides knowledge; splitting related knowledge across shallow classes increases navigation. ## Every Interface Needs a Reason Martin's 2000 statement of DIP says dependencies should point toward abstractions. The paper then calls a literal ban on concrete dependencies "draconian" and allows stable concrete modules as a mitigating case. The nuance often disappears in .NET teaching. "Depend on abstractions" becomes "prefix every service with `I`". .NET's built-in container accepts concrete registration directly: ```csharp services.AddScoped(); ``` The interface mapping is justified when it enforces an architectural dependency direction, narrows the exposed contract, or supports an alternative implementation, proxy, decorator, or independently built consumer: ```csharp services.AddScoped(); ``` A local pair with the same owner, lifecycle, and dependency direction adds a type, a name, usually a file, a registration mapping, and another navigation step. A mock often makes tests assert calls on that duplicated shape. The production code still contains one behavior. Local decision logic can be tested through its result: ```csharp public enum CustomerTier { Standard, Preferred } public sealed record Invoice(decimal Subtotal, CustomerTier Tier); public static class InvoicePricing { public static decimal Total(Invoice invoice) { var discount = invoice.Tier == CustomerTier.Preferred ? 0.10m : 0m; return decimal.Round( invoice.Subtotal * (1m - discount), 2, MidpointRounding.ToEven); } } ``` The input is an immutable value. The calculation has no I/O, and a test compares the returned decimal. A remote tax provider changes independently, so an interface can isolate its API. The pricing calculation remains a direct call. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2026/08/Screenshot-2026-08-01-at-19.54.39.png) Each extra type adds another name and navigation step. Microsoft's current C# guidance describes an interface as a contract that multiple types can implement, including unrelated types. Before adding an interface, I ask two questions: Which code will depend on it? What should be able to change without changing that code? ## LSP and ISP Have Bounded Scope LSP is the most precise member of the set. Liskov and Wing define behavioral sub-typing so that properties established for a super-type continue to hold for its sub-types. A C# implementation must honor the contract its consumer expects. This remains a sound correctness condition wherever genuine sub-type relationships exist. Its scope ends at the sub-type contract; invoice module boundaries remain a separate decision. It is also a different kind of statement: a correctness condition from type theory, standing in a list of heuristics and lending them a rigor they did not earn. Its scope ends at the sub-type contract; invoice module boundaries remain a separate decision. ISP addresses clients forced to depend on interface members they do not use. That matters for a large public interface, separately compiled consumers, or an existing class used by different client roles. A sole consumer can still have an ISP problem when the interface it depends on contains members it never calls. A consumer that uses the whole small contract gains nothing from a further split; the split adds names without reducing the dependency surface. A 2024 controlled experiment ran three independent trials with 100 data scientists. Groups reading SOLID-restructured industrial machine-learning code rated it easier to understand than groups reading the original code, with statistically significant results in aggregate. The comprehension measures were self-ratings on questionnaires, results for individual principles varied across the trials, and the authors call for replications. The study measured short-term comprehension of selected ML code; its scope excluded long-term maintenance and interface-heavy enterprise applications. ## Start the Review with the Change I now start a design review with the change itself. Which business decision is changing? Who requests it? Can the module and its callers ship together? Which dependency has its own lifecycle or more than one current implementation? The answers determine whether a boundary is justified. For decision code, I prefer immutable inputs and a function whose result states the outcome. I keep the [facts](https://ricofritzsche.me/choosing-storage-is-choosing-what-your-system-forgets/) and rules for one decision in the same module. The module exposes one small operation and keeps those rules together. I add an abstraction for a published contract, an independently changing effect, or multiple runtime behaviors. North's CUPID proposal from 2022 replaces the five principles with five properties: composable, following the Unix philosophy, predictable, idiomatic, and domain-based. A property holds by degree, so a team can assess one in context and improve it without creating another type. SOLID addressed real costs. A compile could run for hours, a check-in could take longer, and every modification risked a shipped binary. Under those conditions, leaving working code alone was rational risk management. Predicting the right extension points was a gamble even then. The costs that excused wrong guesses fell, the rules stayed, and a rule that outlives its cost turns into ritual. The correction costs one question per abstraction: which concrete change does this boundary make cheaper, and who asks for that change? A contract another team builds against answers it. A tax service that releases on its own schedule answers it. An interface with one implementation, one owner, and one caller has no answer, and every reader pays for it on every navigation until someone deletes it. If a tutorial today still teaches the five letters SOLID as current best practice, the tutorial is outdated. Same for the training deck, the interview question, and the code review comment. *Cheers*! ## Sources 1. Robert C. Martin: [Design Principles and Design Patterns (2000)](https://web.archive.org/web/20150906155800/http://www.objectmentor.com/resources/articles/Principles%5Fand%5FPatterns.pdf) 2. Bertrand Meyer: [Object-Oriented Software Construction, second edition](https://bertrandmeyer.com/OOSC2/?ref=ricofritzsche.me) 3. Barbara H. Liskov: [Data Abstraction and Hierarchy (1987)](https://doi.org/10.1145/62138.62141?ref=ricofritzsche.me) 4. Robert C. Martin: [The Open-Closed Principle (1996)](https://www.cs.utexas.edu/~downing/papers/OCP-1996.pdf?ref=ricofritzsche.me) 5. Robert C. Martin: [Clean Architecture (2017)](https://www.informit.com/store/clean-architecture-a-craftsmans-guide-to-software-structure-9780134494319?ref=ricofritzsche.me) 6. Robert C. Martin: [An Open and Closed Case (2013)](https://blog.cleancoder.com/uncle-bob/2013/03/08/AnOpenAndClosedCase.html?ref=ricofritzsche.me) 7. Robert C. Martin: [The Single Responsibility Principle (2014)](https://blog.cleancoder.com/uncle-bob/2014/05/08/SingleReponsibilityPrinciple.html?ref=ricofritzsche.me) 8. Robert C. Martin: [Solid Relevance (2020)](https://blog.cleancoder.com/uncle-bob/2020/10/18/Solid-Relevance.html?ref=ricofritzsche.me) 9. Dan North: [CUPID: the back story (2021)](https://dannorth.net/blog/cupid-the-back-story/?ref=ricofritzsche.me) 10. Dan North: [CUPID: for joyful coding (2022)](https://dannorth.net/blog/cupid-for-joyful-coding/?ref=ricofritzsche.me) 11. David L. Parnas: [On the Criteria To Be Used in Decomposing Systems into Modules (1972)](https://doi.org/10.1145/361598.361623?ref=ricofritzsche.me) 12. Barbara H. Liskov and Jeannette M. Wing: [A Behavioral Notion of Subtyping (1994)](https://www.cs.cmu.edu/~wing/publications/LiskovWing94.pdf?ref=ricofritzsche.me) 13. John Ousterhout: [Modular Design, Stanford CS 190 lecture notes (2018)](https://web.stanford.edu/~ouster/cgi-bin/cs190-winter18/lecture.php?topic=modularDesign&ref=ricofritzsche.me) 14. Microsoft: [Service registration in .NET dependency injection](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection/service-registration?ref=ricofritzsche.me) and [C# interface guidance](https://learn.microsoft.com/en-us/dotnet/csharp/fundamentals/types/interfaces?ref=ricofritzsche.me) 15. Microsoft: [Rename and move refactorings](https://learn.microsoft.com/en-us/visualstudio/ide/reference/refactoring-rename-move?view=visualstudio&ref=ricofritzsche.me) and [test validation with GitHub Actions](https://learn.microsoft.com/en-us/dotnet/devops/dotnet-test-github-action?ref=ricofritzsche.me) 16. Raphael Cabral et al.: [Investigating the Impact of SOLID Design Principles on Machine Learning Code Understanding (2024)](https://arxiv.org/abs/2402.05337?ref=ricofritzsche.me) ## ### Who Owns a Rule Shared Across Domain Capabilities? URL: https://ricofritzsche.me/who-owns-a-rule-shared-across-domain-capabilities/ Last updated: 2026-07-30T11:31:40.000Z After publishing [Why the Domain Is Defined by Domain Capabilities, Not Object Models](https://ricofritzsche.me/why-the-domain-is-defined-by-domain-capabilities-not-object-models/), I received this comment on [Medium](https://medium.com/@delecch/capability-ownership-is-compelling-because-it-aligns-code-with-change-but-shared-invariants-can-31e5ca10f6bf?sharedUserId=rico-fritzsche&ref=ricofritzsche.me): > Capability ownership is compelling because it aligns code with change, but shared invariants can still cut across capabilities. How do you prevent a localized RPU from duplicating or drifting on rules such as identity, money, or compliance without rebuilding a central service layer? I took the question seriously because it tests the boundary of capability ownership. Every Request Processing Unit owns its complete decision path. A system can still contain knowledge with one authority across several capabilities. Poor handling produces inconsistent decisions or a shared service that gradually collects business behavior again. The examples in the question also need more precise terms. Identity, money, and compliance describe different kinds of knowledge. Treating all three as shared invariants hides the design decision we need to make. ## An invariant is a property of state An invariant has a strict meaning. [Leslie Lamport defines it](https://lamport.azurewebsites.net/tla/inductive-invariant.pdf?ref=ricofritzsche.me) as an assertion that is true in every reachable state. For this discussion, the relevant states are committed states of the application. For Application State (S), an invariant (I), and an accepted state transition (T), the obligation is: ``` I(S) and T(S) = S' => I(S') ``` Every accepted command must preserve the invariant. Some business rules govern only a decision. “The guest must be at least 18” can be an eligibility rule for one booking capability. It becomes an invariant when the domain promises that every committed reservation has an adult guest. This distinction fits the definitions in my [Architecture Knowledge Base](https://architecture.ricofritzsche.me/?ref=ricofritzsche.me). A [Domain Capability](https://architecture.ricofritzsche.me/concepts/domain-capability/?ref=ricofritzsche.me) owns one business responsibility, its rules, and its possible outcomes. An [RPU](https://architecture.ricofritzsche.me/concepts/request-processing-units/?ref=ricofritzsche.me) implements exactly one capability. [Application State](https://architecture.ricofritzsche.me/concepts/application-state/?ref=ricofritzsche.me) holds the authoritative facts used by all capabilities. [Command Context Consistency](https://architecture.ricofritzsche.me/concepts/command-context-consistency/?ref=ricofritzsche.me) requires that a command's outcome is recorded only while the facts the decision is based on still hold. The examples from the comment now separate into distinct responsibilities: | Kind of knowledge | Example | Architectural home | | ------------------- | ------------------------------------------------------------ | ---------------------------------------------- | | State invariant | A listing night can belong to at most one active reservation | Application State, enforced at commit | | External fact | A subject authenticated at a stated assurance level | Provider, optionally recorded as a fact | | Stable semantics | Money has an amount and a currency | Small pure type or generated local code | | Shared policy | AML-42 release 2026-07 applies in one jurisdiction | Versioned policy authority; applied by the RPU | | Capability decision | Release or reject this payout | Owning RPU | A [Reactor](https://architecture.ricofritzsche.me/concepts/reactors/?ref=ricofritzsche.me) coordinates an interaction when several capabilities or external operations must participate. It stays outside these ownership decisions. ## Shared code and shared knowledge are different Identical conditions in two RPUs can express separate knowledge. The DRY chapter in [The Pragmatic Programmer](https://media.pragprog.com/titles/tpp20/dry.pdf?ref=ricofritzsche.me) makes this point with two validators that happen to contain identical code. One validates age and the other validates order quantity. Their current implementation is the same. Each rule belongs to a different source of knowledge and can change for a different reason. The same applies to RPUs. A deposit amount and a refund amount may both have to be greater than zero today. The deposit capability could later permit corrective entries. The refund capability could add a minimum processing amount. Sharing one *PositiveAmountRule* would couple two responsibilities that happen to agree at the moment. Capability-local duplication is reasonable when the rules have different owners or reasons to change. The code remains close to the decision it explains. The situation changes when several implementations claim to apply one regulation, one contractual policy, or one standard with the same authority and effective date. That is duplicated knowledge. It needs one authoritative representation and a way to prove that every evaluator applies it correctly. That representation can be a database constraint, a versioned policy artifact, a standards dataset, generated code, or a conformance specification. A synchronous service is only one implementation choice. ## Hard invariants are enforced at commit Consider *BookStay* and *ChangeReservationDates*. Both capabilities can occupy nights for a listing. The relevant invariant is: > A listing night can belong to at most one active reservation. Both RPUs could call the same *is\_night\_available()* function and still violate that invariant. Two commands run at the same time. Both read the night as available before either has recorded anything. Both decide functions produce an accepted outcome, and both RPUs attempt to record it. The storage mechanism must reject the second commit. In PostgreSQL, this could use a unique or exclusion constraint over the occupied nights. With an event store, the append must be conditional on the relevant context remaining unchanged. The [PostgreSQL constraint documentation](https://www.postgresql.org/docs/current/ddl-constraints.html?ref=ricofritzsche.me) makes the same distinction between preliminary checks and integrity that the database maintains. Each RPU still owns the domain response. *BookStay* may return *StayUnavailable*. *ChangeReservationDates* may return *RequestedDatesUnavailable*. The storage mechanism protects the state, and the capability gives the conflict its business meaning. A shared helper can keep implementations textually consistent. Concurrent write safety comes from the commit mechanism. Research on [invariant confluence](https://arxiv.org/abs/1402.2237?ref=ricofritzsche.me) reaches the same result formally: independent execution is safe only when independently valid results remain valid after they are combined. Some invariants require coordination. An invariant can also require one atomic change across two capabilities. The commit point is then the coordination boundary for that invariant. Either one capability owns the whole transition, or the storage mechanism enforces the invariant atomically at commit. A Reactor cannot take this role. It sequences calls to RPUs after each of them has decided and committed its own outcome. It has no atomic commit authority, and it holds no domain decision. ## Identity has several owners Identity can refer to domain identity, identifier integrity, identity proofing, authentication, or authorization. Each has a different responsibility. Domain identity concerns continuity and sameness over time. The domain must say when two representations describe the same customer, reservation, or parcel. An identifier represents that decision; its format alone cannot define it. Identifier uniqueness is a state concern. A rule such as “one account per issuer and subject” can be enforced through a unique constraint over the canonical identity key. Identity proofing and authentication commonly belong to an external identity provider. The current [NIST Digital Identity Guidelines](https://pages.nist.gov/800-63-4/sp800-63.html?ref=ricofritzsche.me) separate identity proofing, authentication, and federation. The relying application then uses the resulting identity and assurance information for its authorization decisions. In an RPU architecture, a [Provider](https://architecture.ricofritzsche.me/concepts/providers/?ref=ricofritzsche.me) validates the external assertion and supplies explicit facts such as the subject, issuer, authentication time, and assurance level. The RPU decides whether that evidence is sufficient for its capability. Releasing a payout may require stronger assurance than reading a reservation. A generic *IdentityService.isAllowed()* mixes authentication with domain authorization and hides the facts that explain its answer. ## Money carries stable semantics Martin Fowler’s [Money pattern](https://martinfowler.com/eaaCatalog/money.html?ref=ricofritzsche.me) defines a monetary value through an amount and a currency. It also addresses two common errors: combining different currencies and applying implicit rounding. A small immutable *Money* type can own those semantics. It can reject mixed-currency addition, use an exact numeric representation, and require an explicit rounding operation. [ISO 4217](https://www.iso.org/iso-4217-currency-codes.html?ref=ricofritzsche.me) supplies standard currency codes and their minor-unit relationships. Pricing, tax, refunds, deposits, and settlement still have their own business rules. Cancellation-fee rounding and exchange-rate selection belong to the relevant capabilities. A balanced journal entry is a state invariant. The ledger’s commit boundary must reject an unbalanced posting. A pure *Money* type is a small shared dependency whose responsibility ends with monetary representation and arithmetic. Business operations and transaction coordination stay in the RPUs. Another option is to generate capability-local types from one specification and verify them with the same conformance tests. ## Compliance needs explicit policies “Compliance” covers many kinds of obligation. [ISO 37301](https://www.iso.org/standard/75080.html?ref=ricofritzsche.me) treats compliance as an ongoing management responsibility that includes implementation, evaluation, maintenance, and improvement. Inside software, one obligation may prohibit a payout to a sanctioned party. Another may require identity evidence. Privacy law can require a lawful basis, a retention period, or deletion. Some cases need human judgment and recorded approval. Using a general *ComplianceService.isAllowed(command)* removes the information needed to understand the result. Which policy applied? Which jurisdiction? Which release? Which facts were considered? Which obligation caused the rejection? A policy assessment needs an explicit shape: ```text PolicyAssessment { policy_id: "AML-42", release_id: "2026-07-15", jurisdiction: "EU", effective_from: ..., result: prohibited, reason_codes: ["SANCTIONS_MATCH"], observed_at: ..., valid_until: ... } ``` The RPU loads that assessment into its command context, and its decide function produces the capability outcome. A payout RPU may record *PayoutRejected* with the assessment reference and its own reason. Current information such as sanctions, revoked credentials, or account freezes may require an external Provider call in the Imperative Shell. An assessment with explicit observation and expiry times can be recorded as a fact and consumed by later RPUs. In both cases, the Functional Core receives explicit data and produces the final decision. ## One policy can have many local evaluators A shared policy can have one authority and many local evaluators. The policy authority publishes an immutable release containing its identifier, content digest, jurisdiction, effective period, and source references. Each RPU can evaluate that release through generated code, a pure package, or a locally distributed policy bundle. Activation needs its own authoritative fact. An *ActivatePolicyRelease* capability can record the active release in the Application State after the required evaluators have received and verified it. Each affected RPU loads that fact into its command context. If the active release changes before the outcome commits, the conditional commit fails, and the RPU decides again on the facts that now hold. The recorded outcome retains the policy release and reason codes used for the decision. The effective-time rule determines which release applies. Historical review uses the release recorded for the original decision. Every release should include a conformance suite with accepted cases, rejected cases, boundary values, missing facts, expired evidence, and jurisdiction-specific examples. The same cases run against every evaluator and RPU integration. The result is semantic consistency with local execution. Deployment needs the same precision. New evaluators must understand a release before it becomes active. An instance that cannot evaluate the active compliance policy should reject the command explicitly. Silent use of an older release produces decisions that can no longer be explained. [Open Policy Agent bundles](https://openpolicyagent.org/docs/management-bundles?ref=ricofritzsche.me) show one technical form of this approach: policies and data are authored centrally and distributed to local evaluators. The documentation also states that distribution is eventually consistent. A hard legal cutoff therefore needs a controlled activation process that verifies evaluator readiness. ## Clarifying what capabilities share My current [Application State](https://architecture.ricofritzsche.me/concepts/application-state/?ref=ricofritzsche.me) documentation calls it "the only thing" capabilities share. Its scope is domain ownership. RPUs share no mutable domain model, internal decision functions, or calls to other RPUs. A *Money* type, identifier primitive, generated currency catalogue, or policy evaluator is a different kind of dependency. A more precise formulation is: > Each capability retains ownership of its business decisions. Shared semantic primitives and authoritative policy artifacts stay small, pure, explicit, and versioned. Teams that want absolute code independence can keep the implementations local and use generation plus conformance tests. Teams that share stable semantic primitives must acknowledge the coupling. Both choices are coherent when the boundary stays explicit. ## Ownership follows the decision Capability autonomy gives every command-to-outcome decision one owner. Currency codes, authenticated identities, policy releases, and state integrity still have their own authorities. The RPU owns the command-to-outcome decision. The Application State holds persistent invariants, and its storage mechanism enforces them at commit. Providers supply external facts. A policy authority publishes one identifiable release. Local evaluators and shared conformance cases keep its interpretation consistent. [Fowler’s Service Layer](https://martinfowler.com/eaaCatalog/serviceLayer.html?ref=ricofritzsche.me) defines an application boundary through its available operations and coordinates the application’s response. Here, each RPU is the complete processing boundary for one capability. Shared components stop at semantic representation, fact supply, policy publication, or commit integrity. A rule can have one authority and many local evaluators. Capability autonomy depends on keeping the final decision with the capability that owns it. *Cheers*! ## Sources 1. [Leslie Lamport, *Inductive Invariants*](https://lamport.azurewebsites.net/tla/inductive-invariant.pdf?ref=ricofritzsche.me) 2. [David Thomas and Andrew Hunt, *The Pragmatic Programmer*, 20th Anniversary Edition, DRY chapter excerpt](https://media.pragprog.com/titles/tpp20/dry.pdf?ref=ricofritzsche.me) 3. [PostgreSQL documentation, *Constraints*](https://www.postgresql.org/docs/current/ddl-constraints.html?ref=ricofritzsche.me) 4. [Peter Bailis et al., *Coordination Avoidance in Database Systems*](https://arxiv.org/abs/1402.2237?ref=ricofritzsche.me) 5. [NIST SP 800-63-4, *Digital Identity Guidelines*](https://pages.nist.gov/800-63-4/sp800-63.html?ref=ricofritzsche.me) 6. [Martin Fowler, *Money*, Patterns of Enterprise Application Architecture catalog](https://martinfowler.com/eaaCatalog/money.html?ref=ricofritzsche.me) 7. [ISO 4217, *Currency codes*](https://www.iso.org/iso-4217-currency-codes.html?ref=ricofritzsche.me) 8. [ISO 37301, *Compliance management systems*](https://www.iso.org/standard/75080.html?ref=ricofritzsche.me) 9. [Open Policy Agent documentation, *Bundles*](https://openpolicyagent.org/docs/management-bundles?ref=ricofritzsche.me) 10. [Martin Fowler, *Service Layer*, Patterns of Enterprise Application Architecture catalog](https://martinfowler.com/eaaCatalog/serviceLayer.html?ref=ricofritzsche.me) ### Choosing Storage Is Choosing What Your System Forgets URL: https://ricofritzsche.me/choosing-storage-is-choosing-what-your-system-forgets/ Last updated: 2026-07-29T12:16:17.000Z In 1998, Hugh Darwen wrote that a database is best thought of as *"a repository not just for data, but rather for facts"*, for true propositions. In 2012, Rich Hickey said about facts: *"You cannot update a fact, because you can't change the past."* I agree with both sentences. Every relational database ships with UPDATE, and an UPDATE changes rows. If rows hold facts, and facts cannot change, what does an UPDATE change? Answering takes four words, held apart: state, fact, event, and record. Each gets a definition that stands on its own, and an example after it. The examples support the definitions; they are no part of them. **Note:* The examples use a platform for booking stays, in the style of Airbnb or Booking.com. Hosts publish listings: a place to stay, with a nightly price, a maximum number of guests, and a calendar of nights. Guests reserve nights. A reservation gets confirmed, and it is sometimes cancelled later. Hosts can also block nights in their own calendar, for repairs or private use. That is all the domain knowledge this text needs.* ## State State is what holds at a moment. The state of a domain does not depend on anyone describing it. It is as it is. A query can read it, a sentence can describe it, and it holds either way. What state does is change: after every happening, a different state holds. One thing state cannot do: a state holds, it never happens. Gilbert Ryle and Zeno Vendler drew this line in their analyses of verbs and time: a state is homogeneous and may extend over time; an event occurs and can culminate. In short form: a state *s*; after a happening, a later state *s′*. An example: On July 28 at noon, one listing is in this condition: the nights of August 10 to 14 are free, the nightly price is 120 euros, the guest maximum is 4\. That is state. It holds at noon whether anyone looks at it. When a guest books those nights an hour later, a different state holds: the same listing, the nights now taken. ## Event An event is something that happened: a change of state at one moment. Where a state holds, an event occurs, and it occurs once. Georg Henrik von Wright described a change as an ordered pair of states, the one before and the one after: *c = (s, s′)*. The pair leaves a gap, and the gap is the important part: two different happenings can produce the same pair. The states before and after a change do not say which happening lies between them. That meaning belongs to the event: its type says which happening changed the state. The event *e* is the happening between the two states; its type carries what the pair lacks. What happened must be stated somewhere, or it is lost the moment the new state holds. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2026/07/Screenshot-2026-07-28-at-23.05.44.png) Two happenings, one pair of states. An example: On July 28 at 14:03, a guest books the nights of August 10 to 14\. That is an event. At 14:02 the nights were free; at 14:04 they are taken. The same difference has a second possible cause: the host blocks the same nights for repairs. Free before, unavailable after, in both cases: the calendar's state is the same. Anyone who sees only the calendar cannot tell the booking from the blocked nights. The happening needs its own statement. ## Fact A fact is a true statement about state and/or causality, bound to the moment it is about. A fact states one of two things: what held at that moment, or what changed and why. Rich Hickey's definition covers both sides: *"an event or thing known to have happened or existed"*. The event or thing is what the fact speaks about; the knowing is the statement. And every fact carries its time. The word itself began there: factum, Latin, a thing done. In form, a fact is a claim: that a state *s* held, or that *(s, e, s′)* is true, the state before, the event, the state after. A claim can be true or false. A fact is a claim that is true. Because a fact speaks about one moment, it cannot change. Later change makes no statement about a past moment false. Hickey again: *"You cannot update a fact, because you can't change the past."* New knowledge arrives as a new fact that supersedes the old one, and the old one stays true about its own moment. An example: "On July 28 at noon, the nights of August 10 to 14 were free" states what held. The booking at 14:03 does not touch this sentence; it speaks about noon, and at noon the nights were free. "On July 28 at 14:03, the reservation for August 10 to 14 was confirmed" states what happened. A cancellation in September will not make it false. The price shows superseding. On August 1, the host raises the nightly price from 120 to 135 euros. "The nightly price is 135 euros, as of August 1" supersedes "the nightly price is 120 euros, as of July 28". Both stay true, each about its own time. ## Record A record is a fact written into a store: the value a system keeps. The record is not the fact. The fact is the statement; the record preserves it. The container can be anything that holds records: a ledger book, a current-state database, or an event store. The store decides what may happen to a record. One store appends records and never touches them again. Another overwrites records in place. Overwriting changes no fact, because no operation on a record can reach into a past moment. It destroys the record of a fact, and with it the system's only access to that fact. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2026/07/Screenshot-2026-07-28-at-23.07.03.png) Overwriting keeps state; appending keeps facts. An example: A confirmed reservation stored as a row in a reservations table is a record. An appended entry *ReservationConfirmed* with the same content is a record too. The price shows what overwriting costs. On August 1, the price column changes in place from 120 to 135\. The fact that 120 euros held through July is untouched; the record of it is gone. In October, a guest who booked in July disputes the charge, and the store cannot show the price that held when the booking happened. ## Three layers The four words sort into three layers. State and events are in the world: what holds, and what happens. Facts are statements about both, each bound to its moment. Records are what a system keeps. Everything a system knows about state and events, it knows through records. This is why storage deserves its own examination: the store is where facts survive, or where they end. In short form: a state *s* and a later state *s′*; a change *c = (s, s′)*; an event *e*, the happening between them; a fact, the claim that *s* held or that *(s, e, s′)* is true. Records fix the facts in a container. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2026/07/Screenshot-2026-07-28-at-23.07.49.png) State and events are in the world, facts state them, records keep them. ## What an event store keeps An event store appends records and never changes them. The discipline of the record matches the immutability of the fact: written once and true about its moment; later records supersede it. Each record preserves a fact about one event, and the record carries a type named in the domain's language: *ReservationConfirmed*, *ReservationCancelled*, *NightsBlocked*. The type holds what the state difference loses. A booking and a blocked calendar leave the same difference behind; *ReservationConfirmed* and *NightsBlocked* are different records, and the distinction survives. Martin Fowler's domain event *"captures the memory of something interesting which affects the domain"*, and it carries two times: the moment the happening occurred and the moment the system learned of it. The two can differ, and both belong to the fact. Current state is derived. Greg Young: *"Current State is a Left Fold of previous behaviours."* Start from nothing, apply every recorded event in order, and the result is the state as it stands. Event Sourcing names the decision to make event records the authoritative record and to treat every current value as derivable from them. It is a storage decision. Thinking in events does not depend on it. One neighbor clarifies the boundary. Datomic appends records of facts without domain types. Its documentation calls each record, the datom, an *"immutable atomic fact"*: an entity, an attribute, a value, a transaction. A store of this kind is a fact store. The domain-named type is what turns a fact store into an event store. The type adds the meaning: which happening changed the state. ## What a current-state database keeps Hugh Darwen, writing in the relational tradition with C.J. Date, describes a database as *"a set of propositions (assumed to be true ones)"*. A table heading is a predicate; each row fills in the blanks and makes one statement. Read this way, every row of a current-state database states what holds now. The price is 135 euros. The night of August 12 is unavailable. That is state, stored directly, and here the opening question resolves. An UPDATE changes a record and nothing else. The fact that held before still holds about its moment; its record is destroyed. The happening that caused the change is never written down at all: the calendar says unavailable and stays silent on whether a guest booked or the host blocked. Rich Hickey calls the habit behind this place-oriented programming, *"new information replaces old"*, and traces it to a time when memory was scarce and overwriting was how systems coped. A current-state database answers one question: what holds now. What held on July 28 left with its records. What happened at 14:03 was never written down. The dividing line runs through the discipline of the record, and nowhere else. A relational table written append-only keeps facts. Accountants have run their books this way for centuries: one entry per change, corrections as new entries, totals derived. An event log that someone edits in place keeps state under a misleading name. Overwritten records keep state. Appended records keep facts. ## Why hold the words apart The questions a business asks later are questions about facts and events. Why is this night unavailable? What price did the guest agree to at booking time? Who cancelled the reservation, and when? A store can answer them only while the records that carry the answers exist. Choosing storage is choosing what the system may forget. A current-state database forgets superseded facts and all happenings; for values whose history nobody will ask about, that is a fair trade. An event store forgets nothing it recorded, and the cost is derivation: current values must be computed from the records. Both choices are legitimate. With the four words held apart, the choice is made knowingly, and the debate about it gets shorter. *Cheers*! Sources: 1. [Rich Hickey, The Value of Values (talk, GOTO Copenhagen 2012). ](https://www.infoq.com/presentations/Value-Values/?ref=ricofritzsche.me) 2. [Hugh Darwen, What a Database Really Is: Predicates and Propositions, 1998; the repository sentence quoted in the opening is from C.J. Date's preface. ](https://www.dcs.warwick.ac.uk/~hugh/M359/What-a-Database-Really-Is.pdf?ref=ricofritzsche.me) 3. [Martin Fowler, Domain Event. ](https://martinfowler.com/eaaDev/DomainEvent.html?ref=ricofritzsche.me) 4. [Greg Young, Functional Domain Models and Event Sourcing, 2012\. ](https://gregfyoung.wordpress.com/2012/10/01/functional-domain-models-and-event-sourcing/?ref=ricofritzsche.me) 5. [Stanford Encyclopedia of Philosophy, Events; the state/event classification traces to Gilbert Ryle (The Concept of Mind, 1949) and Zeno Vendler (Verbs and Times, 1957). ](https://plato.stanford.edu/entries/events/?ref=ricofritzsche.me) 6. [Datomic Documentation, Data Model. ](https://docs.datomic.com/whatis/data-model.html?ref=ricofritzsche.me) 7. [Online Etymology Dictionary, fact. ](https://www.etymonline.com/word/fact?ref=ricofritzsche.me) 8. [Ralf Westphal, When to record an Event ](https://ralfwestphal.substack.com/p/when-to-record-an-event) ### The Command Context Consistency Principle URL: https://ricofritzsche.me/the-command-context-consistency-principle/ Last updated: 2026-07-30T19:59:07.000Z A few years ago I set out to build systems as self-contained feature slices: each slice owning one behavior, end to end. I built them the common way, as Vertical Slice Architecture, and what I got was handlers around a central data model. Every handler loaded mutable entities from that model and saved them back. The cause was the entity-centric thinking we were taught for many years. Domain-Driven Design made it precise: find the aggregate, load it, call a method, save it. The aggregate holds the invariants, and it holds the transaction: one aggregate per transaction, one consistency boundary, fixed at design time. The move that changed my systems was dropping the aggregate as the consistency boundary. A command reads facts to decide, and those facts have to still hold when the outcome commits. Nothing about that requires an aggregate. Dropping it led me to [Aggregateless Event Sourcing](https://ricofritzsche.me/aggregateless-event-sourcing/): no stream per aggregate, an append guarded by the facts the decision was based on instead of by a stream version. It worked, and it still works. It took me longer to see that the principle does not need the event store either. A relational database enforces it with locks and constraints; an event store enforces it with a conditional append. Event sourcing remains a great option. The principle does not require it. Its name is Command Context Consistency: record the outcome of a command processing only while the facts the decision is based on still hold. The aggregate is the wrong unit for consistency. The boundary belongs to the facts a command’s decision is based on, and that set changes from one command to the next. ### The Aggregate Is a Static Consistency Boundary Domain-Driven Design gave the aggregate a specific job. Eric Evans defined it as a cluster of objects with a boundary and a root, and made that boundary the unit of consistency for changes. Vaughn Vernon sharpened it into a transactional consistency boundary, with his rule of thumb that a system *“modifies only one aggregate instance per transaction”*, a strong default he allows breaking for good reason. When the transaction commits, the state inside the boundary is consistent, and anything outside it is left to eventual consistency. The boundary is fixed once, per aggregate, and the store enforces it as a single unit. In a relational model the aggregate root carries a version, and any change to any part of the aggregate turns on that one version. In event sourcing the aggregate is one stream, and an append is accepted only at the expected sequence number for the whole stream. Either way the check covers the whole object, one boundary for every command that touches the aggregate, whatever facts a given command actually reads. A command that reads three facts and a command that reads fifteen are validated against the same aggregate version. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2026/07/Screenshot-2026-07-26-at-21.20.09.png) One stream per aggregate: One version check covers every event on the stream, relevant to the command or not. One boundary cannot fit every command. Take two commands on one reservation. One changes the guest count, one changes the check-in date. Both load the Reservation aggregate, and the aggregate is one concurrency unit: its version covers every part of it, so the second write fails the version check. They changed different values, and neither change touched a fact the other read, and the boundary still counted them as a conflict. It is too large: it guards the whole reservation where the two commands worked on separate parts of it. Now take the rule that a listing night is booked once. Two guests request the same night. Each request creates its own Reservation, a separate aggregate instance; each transaction modifies one aggregate, exactly as the rule prescribes, and both commit. The invariant that should stop the second booking is not inside either reservation. It holds across every reservation for that listing and night. The boundary is too small: the fact that needs protecting sits outside the aggregate the command changes. The usual answers are known. Doctrine says to remodel until the invariant lives inside one aggregate, as Vernon’s aggregate-design series prescribes: introduce a ListingAvailability aggregate that owns the booked nights of one listing and route every booking through it. The rule holds again, every booking for that listing now serializes through one object, the object grows night by night, and it exists to hold a lock. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2026/07/Screenshot-2026-07-26-at-21.21.14.png) ListingAvailability aggregate owns the invariant: every booking for the listing serializes through this one object, and it grows night by night. The second answer is eventual consistency between aggregates: both reservations commit, and a process manager detects the collision afterwards and cancels the later booking, a confirmation followed by a cancellation. ![](https://cdn-images-1.medium.com/max/1600/1*-xK2l_2QLHXE1trBpB7GrA.png) Eventual consistency between aggregates: guest B gets a confirmation, then a cancellation. Practitioners know a third answer and file it as an exception: put a unique constraint on the nights and let the database refuse the second insert. The first two answers relocate the boundary and keep its nature, a single fixed boundary rather than the facts any one command needs. The third abandons the aggregate and guards the actual fact. It is worth taking seriously. The consistency a command needs is the set of facts its decision is based on. It rarely matches the outline of an aggregate, and one fixed boundary cannot fit a set that changes with the command. Know someone still designing aggregates? Send them this article. The argument works best on people who have lived the version-conflict pain. [Share the article ](#/share) ### The Command Shapes Its Context A command expresses an intention, for instance “book this stay” or “cancel this reservation”. To decide whether to accept it, the application reads the relevant facts from the Application State. Those facts form an immutable data structure: the command’s context. [A fact is a true statement](https://ricofritzsche.me/choosing-storage-is-choosing-what-your-system-forgets/) about state and causality, bound to the moment it is about: this guest is eligible, this night is occupied. A fact cannot change; a later commit supersedes it with a new fact about the new state. A context fact holds while no commit has superseded it. A fact belongs in the context when a rule evaluates it. Anything that does not affect the result is excluded, no matter how close it is in the data. The intent shapes the context. Which facts matter follows from what the command is trying to do. Booking a stay has to establish that the guest may book, that the listing permits the stay, and that the requested nights are free; its context holds those facts and nothing else. The capability defines which kinds of facts its rules read, and the command’s values (this guest, this listing, these nights) select the concrete facts for one execution. The same facts matter whatever store holds them. The context is read once. What the decide function receives is a snapshot of that moment, not a live view of the store. Other commands may commit while the decide function runs; the context does not change. Deciding uses only the command and the snapshot. If a rule needs the time, the time is one of those facts, read with the rest. The same command over the same facts gives the same outcome, every time. A decision is valid for exactly the facts it was made from. Between the read and the commit, another command may supersede one of them. Then the outcome was decided on facts that no longer hold, and it must not be recorded: in an event store the event records are not appended, and in any store the Application State does not change on a stale basis. So processing a command has a fixed shape: ![](https://cdn-images-1.medium.com/max/1600/1*gxh9hI4A3wy-vHSe3YFSHQ.png) The fixed shape: read, decide, record. The record step carries the condition: an outcome is recorded only while its context holds. That is the whole principle. ### Command Context Consistency Ralf Westphal coined the name [Command Context Consistency](https://ralfwestphal.substack.com/p/command-context-consistency), and he described it against an event store: the context is "all the events relevant for a command during consistency check", and the events a command produces are recorded only after the same query confirms that no new context events arrived. I had set out the same rule in [Aggregateless Event Sourcing](https://ricofritzsche.me/aggregateless-event-sourcing/?utm%5Fsource=ricofritzsche.me): a decision's context is the set of facts read for it, held unchanged from the read to the write. Dropping the aggregate has its own history in the event-sourcing community. Sara Pellegrini argued for killing the aggregate and named the alternative the Dynamic Consistency Boundary: read the events needed for the decision through a query, and append the result only if that selection is unchanged at the moment of the append. She calls it “a form of optimistic lock specific for event sourcing based systems”. All three texts speak event-store language. The principle does not. A command’s outcome is recorded only while the facts the decision is based on still hold. That sentence mentions no store. It fixes what has to be true at the moment of commit and says nothing about how to check it. An event store checks with a conditional append, a relational database with locks and constraints. Only the mechanism differs. The boundary of consistency is the context, one command at a time. A command needs to be re-validated when another commit supersedes a fact its decision is based on; commands do not conflict merely because they touch the same aggregate. Change the guest count and the check-in date of one reservation, and neither change touches a fact the other read, so both commit. Book the same night twice, and each claims the night the other read, so the second is re-validated against the new facts and refused. The boundary follows the facts each command reads. ![](https://cdn-images-1.medium.com/max/1600/1*3G27CcLHkF9CTsz1dGSCNA.png) The boundary follows the facts each command reads. A failed guard means one thing: the decision was based on stale context. The store reports it in whatever way its mechanism has: zero rows updated, a failed append condition, a uniqueness violation, a serialization failure. What happens next belongs to the capability. Its Request Processing Unit (RPU), the code that carries it, can roll back, reload the context, and run the same decide function again; that second decision may refuse the booking, and it may accept it when the context moved in a favorable direction. It can also map the conflict directly to a business rejection, the way a night claimed by another booking becomes ListingUnavailable. Either way the answer comes from current facts: the conflict is a business situation to resolve, not a technical error to raise. A rejection that changes no state need not be recorded; recording rejections for audit writes a separate record, on purpose. Command Context Consistency replaces the aggregate with a boundary the command draws for itself. What remains is to enforce it, in whatever store you have. ### Guarding the Whole Context The principle has stayed logical: record an outcome only while the facts the decision is based on still hold. A store enforces it with something physical, and the two are easy to run together. The logical context is the set of facts `decide` read that another commit can supersede. The physical guard is what the store actually holds still: a row, a version, a predicate, a unique constraint, a set of tags, a stream position. Correctness needs the guard to cover the context and never less. A guard narrower than the context lets a fact change unseen, and a wrong outcome commits. A guard wider than the context stays correct, but it blocks commands that changed a fact the decision never read, and contention and false rejection return under another name. Equality is the ideal, and no store hands it over for free. Only mutable facts whose continued validity matters belong in the guard; a captured request time, a generated identifier, or a provider's result is an input to the decision, not something the store can hold unchanged. The criticism of the aggregate returns here in its sharpest form: an aggregate version is a physical guard far wider than most command contexts. Take the two commands on one reservation to a relational store: PostgreSQL at its default READ COMMITTED, each command in one transaction. Changing the guest count reads the reservation’s current count and its status, and writes the new count. The guard is those two facts, and a conditional update names them. ```SQL UPDATE reservations SET guest_count = $1 WHERE id = $2 AND guest_count = $3 AND status = $4 RETURNING id; ``` The `WHERE` is the guard, and READ COMMITTED does not hold the row still on its own; the guard does. The update commits only while the count and the status still hold the values the decision was based on. A concurrent command changing the check-in date on the same row makes this statement wait, because a row lock in PostgreSQL holds the whole row, not a column. When that command commits, PostgreSQL re-evaluates the `WHERE` against the updated row before applying the update. The check-in date is not in the guard, so the row still matches, and the guest-count update proceeds. An empty `RETURNING` is the only rejection, and it comes only when the count or the status actually changed. A command-specific guard does not remove database serialization. Two writes to one row still take turns. What it removes is the needless rejection, the guest-count command refused because an unrelated column moved. The two commands are ordered, and both succeed. A decision that reads more than one fact guards each of them, in the same transaction. Whether a booking is allowed reads the guest’s eligibility, the listing’s rules, and the nights already taken. A free night is the absence of a row, which a row lock cannot protect, so a unique constraint guards it at commit: the command inserts one row per night, and the constraint rejects a night another booking already claimed. ```SQL INSERT INTO occupied_nights (listing_id, night) VALUES ($1, $2); -- UNIQUE (listing_id, night) ``` The guest’s eligibility and the listing’s rules are rows read for the decision but never changed. A shared lock holds them to the commit, acquired in a fixed order across commands so two bookings cannot deadlock. ```SQL SELECT booking_eligibility FROM guests WHERE id = $1 FOR SHARE; SELECT status, max_guests, min_nights FROM listings WHERE id = $1 FOR SHARE; ``` `FOR SHARE` is a wider guard than the columns read, because it holds the whole row: an update to any column of that listing waits, not only a change to a rule the decision is based on. It is the practical guard when a decision is based on most of a row, and a conditional predicate is the tighter one when it does not. Read under the guards, decide, and write in one transaction. If any guarded fact gave way, the transaction does not commit. On an event store, event records carry the facts and the guard is a query. The context query selects the events that describe the guest, the listing, and the nights. Reading it returns the context and its version: the highest sequence number among the records the query matches, absent while the context is empty: no matching event exists yet, so the first one to arrive is itself the change. Decide, then commit with a conditional append: the store recomputes the context version at the append, and the new events commit only while the actual version still equals the expected one. ![](https://cdn-images-1.medium.com/max/1600/1*9Cp5QkTFaNmr-Lie9-q1Zw.png) ****On an event store: query, decide, append\_if.** A new matching event raises the actual version above the expected one, and the append is rejected as a conflict; events that do not match the query change nothing about it. The query is the physical guard: it selects by event types and by predicates on the recorded payload, so it holds the events the decision is based on rather than the whole stream. Back to the two commands on one reservation, now as events. Each capability declares the facts its decision is based on as a context query: ```csharp // ChangeGuestCount: the confirmed reservation and later count changes public static EventQuery For(Request request) => new([ new EventFilter( ["ReservationConfirmed", "GuestCountChanged"], [SerializeToElement(new { reservation_id = request.ReservationId })]) ]); // ChangeCheckIn: the same shape, with "CheckInChanged" instead of "GuestCountChanged" // processing, the fixed shape from above: var contextQuery = Query.For(request); var read = await eventStore.Query(contextQuery); var result = Decide.Execute(request, Context.Fold(request, read.Records)); await eventStore.AppendIf(result.NewEvents, contextQuery, read.ContextVersion ?? 0); ``` A committed CheckInChanged does not match the guest-count query, so it does not move that context's version, and both appends commit side by side. Book the same night twice, and both context queries select the events occupying that night: the first append moves the version, the second conflicts and is decided again on the new facts. Run the claims yourself Every claim in this article is a test: the false conflict that vanishes, the double booking one constraint refuses, the conditional append that re-decides. Both stores, C#, ready to check out. [Checkout the code ](https://github.com/ricofritzsche/reservation-ccc-example?ref=ricofritzsche.me) The[ Dynamic Consistency Boundary](https://dcb.events/?utm%5Fsource=ricofritzsche.me) is the same guard in tag-based form. Same principle, two stores. The relational one guards with a conditional update, a shared lock, and a unique constraint; the event store with a conditional append. Neither reaches for an aggregate. The guard is the context the command read, held still to the moment the outcome is recorded. Get your FREE ACCOUNT If this article was useful, the next ones build on it. A free account gets them to you when they publish. [Sign up free ](#/portal/signup/free) ### Conclusion Go back to the two commands on one reservation. The guest count and the check-in date commit side by side now, because neither change lands in the other’s context. The two bookings for the same night still collide, because each changes the night the other read, and one is refused. The false conflict is gone, the real one stays, and no aggregate decided either. The consistency boundary belongs to the command: the facts it read, checked when the outcome is recorded. Command Context Consistency states it as a principle. An event store enforces it with a conditional append over a query, a relational database with locks and constraints. The aggregate can retire from this job. A [Domain Capability](https://architecture.ricofritzsche.me/concepts/domain-capability/?ref=ricofritzsche.me) is the ability to process a domain request, one that changes the Application State or projects it: in the first case the Application State is the sink, in the second it is the source. The capability defines the facts needed to decide, and the RPU carries that context into the commit condition. Consistency follows the command instead of a static object model. The event-store contract is written down in the [Command Context Consistency specification](https://architecture.ricofritzsche.me/specifications/command-context-consistency/?ref=ricofritzsche.me). *Cheers*! ### Sources 1. Ralf Westphal: Command Context Consistency — 2. Sara Pellegrini: The Dynamic Consistency Boundary — [https://sara.event-thinking.io/2023/05/dynamic-consistency-boundary.html](https://sara.event-thinking.io/2023/05/dynamic-consistency-boundary.html?ref=ricofritzsche.me) 3. Eric Evans: Domain-Driven Design (2003) — the Aggregate pattern. 4. Vaughn Vernon: Effective Aggregate Design (2011) — [https://www.dddcommunity.org/library/vernon\_2011/](https://www.dddcommunity.org/library/vernon%5F2011/?ref=ricofritzsche.me) 5. Rico Fritzsche: Aggregateless Event Sourcing — 6. Dynamic Consistency Boundary specification — [https://dcb.events/specification/](https://dcb.events/specification/?ref=ricofritzsche.me) 7. Rico Fritzsche: Command Context Consistency, concept page — [https://architecture.ricofritzsche.me/concepts/command-context-consistency/](https://architecture.ricofritzsche.me/concepts/command-context-consistency/?ref=ricofritzsche.me) 8. Rico Fritzsche: Command Context Consistency specification — [https://architecture.ricofritzsche.me/specifications/command-context-consistency/](https://architecture.ricofritzsche.me/specifications/command-context-consistency/?ref=ricofritzsche.me) ### Why Your Software Cannot Explain Its Business Decisions URL: https://ricofritzsche.me/why-your-software-cannot-explain-its-business-decisions/ Last updated: 2026-07-23T10:15:55.000Z A guest asks to cancel a reservation. The application accepts the command, releases the dates, and records the reservation as cancelled. Later, someone challenges the decision, claiming that the cancellation period had already ended. Explaining the outcome now requires more than reading the current state. We need to know which context was considered, which rule applied, and whether that context was still valid when the outcome was recorded. After more than 30 years of building and changing production systems, I have learned to distrust architectures that make the resulting state easier to find than the decision that produced it. The weakness becomes visible when a rule changes, concurrent requests interact, or someone must explain an outcome long after the code was written. The objective is practical. A developer should be able to follow one business decision without reconstructing the entire application. Application architecture must keep a business decision explicit from the command that expresses an intention to the outcome recorded in Application State. Application State is the data the application persists as its authoritative record. An explicit processing path makes a decision structurally traceable. Explaining one specific past decision additionally requires retaining the decision evidence that mattered at the time. This thesis applies whether the system stores current state, an event history, or a combination of both. Storage is an explicit implementation decision, made deliberately, not implied by the architecture. A database may protect the Application State, but it must not invent its business meaning. ### A Command Preserves the Intention The command tells us what someone asked the application to decide. In this case, the command is “*Cancel Reservation”*. A representation could look like this: ``` { "reservationId": "reservation_555", "requestedBy": "guest_456", "reason": "change_of_plans" } ``` The command expresses one concrete intention toward the capability *“Cancel a reservation”*. The guest wants the application to cancel a specific reservation. Whether that request can be accepted depends on the relevant context and the business rules. ``` COMMAND DOMAIN CAPABILITY OUTCOME CancelReservation(...) ───▶ Cancel a reservation ─┬──▶ ReservationCancelled └──▶ CancellationRejected ``` A Domain Capability is a capability the domain offers. It defines the business responsibility, the rules behind it, and the possible outcomes. The command is one concrete request toward that capability, tied to a specific reservation and requester. A Domain Capability is not the delivery mechanism. The same intention can reach the application over HTTP, a message queue, or a command-line tool. The delivery explains how the request arrives. *CancelReservation* explains what the caller wants to achieve. The capability stays the same no matter which channel carries the command. The application may accept the command: ``` { "outcome": "reservation_cancelled", "reservationId": "reservation_555" } ``` It may also reject it for a business reason: ``` { "outcome": "cancellation_rejected", "reason": "cancellation_period_expired" } ``` The rejection is a valid business outcome. The application understood the intention, evaluated the request, and determined that the current situation did not permit the cancellation. The command and the outcome preserve both ends of the decision: what was requested and what followed. Explaining why requires the relevant context and the rules applied to it. That is the missing information in systems that can show their current state but cannot explain the business decision behind it. ### The Outcome Alone Cannot Explain the Decision After the cancellation, a query may return this representation: ``` { "reservationId": "reservation_555", "status": "cancelled" } ``` The representation tells us the current situation. It cannot tell us why the application accepted the command. Was the requester allowed to cancel? Was the cancellation period still open? Had another operation already changed the reservation? A domain event gives the outcome a domain specific meaning: ``` { "type": "ReservationCancelled", "reservationId": "reservation_555", "cancelledBy": "guest_456", "occurredAt": "2026-08-08T10:30:00Z" } ``` *ReservationCancelled* clearly describes what happened in the domain. In contrast, a generic *ReservationUpdated* would retain the technical change while discarding its business meaning. [Thinking in events](https://levelup.gitconnected.com/thinking-in-events-2033b92ea220?sharedUserId=rico-fritzsche&ref=ricofritzsche.me) gives us this language before any storage decision is made. Modeling the domain through events is not the same as Event Sourcing. An event history can retain the sequence of facts, and Event Sourcing is one possible way to store Application State. It is not a requirement of this approach, and it does not automatically explain the decisions behind the facts it stores. The event shown tells us what happened. An event may retain additional decision evidence, such as the reason or the applicable policy version, but it does not do so by default. The explanation also depends on the command and the context used to decide it. For the cancellation, that context may look like this: ``` { "reservationStatus": "confirmed", "guestId": "guest_456", "cancellationAllowedUntil": "2026-08-10T15:00:00Z", "evaluatedAt": "2026-08-08T10:30:00Z" } ``` The context contains the guest who holds the reservation. The decide function compares it with the requester carried by the command: only that guest may cancel. *evaluatedAt* comes from a trusted server-side clock at the moment the request is evaluated. The capability defines which moment counts for the deadline; here it is the evaluation time, retained in the context. The cancellation context contains the information capable of changing the outcome. Property photos, the guest’s profile, and the full payment history have no place in this decision unless a cancellation rule depends on them. ``` CancelReservation + CancellationContext │ ▼ cancellation decision │ ├── ReservationCancelled │ └── CancellationRejected ``` The same command can produce a different result depending on the context at the time of the request: ``` requestedAt: 2026-08-08 ──▶ ReservationCancelled requestedAt: 2026-08-11 ──▶ CancellationRejected: cancellation_period_expired ``` The intention remained the same. The second request relied on a different business situation and was rejected because the cancellation period had expired. This gives us a requirement for an explainable decision: the capability must make its relevant context visible. A generic Reservation object containing everything associated with the reservation obscures that dependency. The *CancellationContext* states exactly which information the cancellation decision uses. It also exposes a consistency problem. The application may load a valid context, make the decision, and then commit the outcome after one of the relevant conditions has changed. A decision is valid only for the context on which it relied. Command Context Consistency, as described by [Ralf Westphal](https://ralfwestphal.substack.com/p/command-context-consistency) and [me](https://ricofritzsche.me/simplicity-wins-command-context-consistency/), expresses that requirement directly: > A command is accepted only when the context it relied on for its decision is still valid at the moment the result is committed. This requirement applies regardless of how the Application State is actually stored. The implementation may use constraints, versions, locks, or context checks. The architectural obligation remains the same: the outcome must not be recorded after the basis for the decision has become invalid. ### A Business Decision Needs a Visible Processing Path The application can execute a cancellation correctly and still leave its decision hard to explain. This happens when the command, the relevant context, the rule evaluation, the consistency protection, and the resulting outcome exist in code without forming one visible processing path. Consider an implementation where *CancelReservation* enters through a controller, a shared service loads the reservation object using a generic repository, a policy object contains part of the rules, and an ORM decides whether the update succeeds. None of these structures owns the cancellation decision. The service loads reservations for every use case, the policy object is reused across features, and the ORM applies a generic update strategy. The capability that belongs together from a domain perspective is distributed across code that was organized for reuse, not for the decision. Every required step may be present. Explaining why the cancellation was accepted still requires reconstructing the decision across those structures. A [Request Processing Unit (RPU)](https://levelup.gitconnected.com/request-processing-units-and-reactors-0fba33071d3e?sharedUserId=rico-fritzsche&ref=ricofritzsche.me) gives that processing a concrete boundary and clear ownership. It implements one request toward a Domain Capability, starting with the command and ending with an accepted or rejected outcome. For our example, the Domain Capability is **“Cancel a reservation”.** The RPU processes one concrete *CancelReservation* command: ``` CancelReservation │ ▼ process_cancel_reservation │ ├── obtain CancellationContext ├── invoke the decide function ├── protect the context during the commit ├── record the accepted outcome ├── coordinate required effects └── return CancellationOutcome ``` The capability defines the responsibility, the rules, and the possible outcomes. The decision is made per command, by applying those rules to the relevant context. The RPU makes the complete processing of that decision visible in the application. A simplified implementation could read like this: ``` let context = load_cancellation_context(&command).await?; let decision = decide_cancellation(&command, &context); let outcome = match decision { Accepted(result) => { commit_if_context_is_valid(result, &context).await? } Rejected(reason) => { CancellationRejected(reason) } }; Ok(outcome) ``` *decide\_cancellation* is a function and the decision is its result. The function evaluates the command against the supplied context and proposes acceptance or rejection. A proposed acceptance becomes final only when the context is confirmed to be still valid and the result is committed. When the context has changed in the meantime, the conflict is not a technical failure; the RPU maps it to the appropriate business outcome. The concurrent booking example in the database section shows one way this looks in practice. The RPU records accepted outcomes and returns rejected ones. Where audit or regulatory requirements demand it, rejected decisions and their relevant context can be retained as well. The RPU coordinates the effects its capability requires to fulfill the task, after the outcome is committed. Independent follow-up processes, such as initiating the refund, react to the recorded outcome outside the RPU. A failure in follow-up work must not make the committed outcome appear unsuccessful. This gives a developer somewhere concrete to begin when a decision must be explained. Starting with *process\_cancel\_reservation*, they can follow the same path the application followed. There is no need to infer the explanation from unrelated technical structures anymore. [Functional Core / Imperative Shell](https://levelup.gitconnected.com/functional-core-imperative-shell-for-agentic-coding-45f04beb55f5?sharedUserId=rico-fritzsche&ref=ricofritzsche.me) is one useful way to construct such an RPU. The Imperative Shell obtains the context and handles the commit and external effects. The Functional Core is the decide function evaluating the command against that context. This separation is an implementation choice. The RPU is defined by the complete processing path it implements. Your software cannot explain its business decisions when that path exists only implicitly. It may show the command, the current state, or the recorded outcome. The RPU connects them through the processing that produced the decision. ### The Database Protects Application State, Not Its Meaning A cancellation decision is valid for the context on which it was made. Another command may change that context before the outcome is recorded. In this relational implementation, the RPU uses one transaction together with locks and constraints. The transaction makes the change atomic; the locks and constraints protect the decision-relevant context until commit. Loading the context, making the decision, and recording the outcome operate on the same protected state. ``` begin transaction │ ├── load CancellationContext │ (reservation row locked) │ ├── cancellation decision │ ├── record ReservationCancelled │ ▼ commit ``` The lock is taken when the context is loaded: ``` SELECT status, guest_id, cancellation_allowed_until FROM reservations WHERE id = $1 FOR UPDATE; ``` FOR UPDATE holds the reservation row until the transaction ends. No concurrent transaction can update or delete that locked row before the current transaction commits. Rows the decision reads without modifying can be held with FOR SHARE instead. The protection applies to the rows the decision actually locks: every mutable fact the decision relies on must be locked, constrained, or revalidated during the commit. After the commit, the resulting row may look like this: ``` reservation_id status cancelled_at reservation_555 cancelled 2026-08-08 10:30:00 +00:00 ``` It tells us that the reservation is now cancelled and that a guarded state transition succeeded. It cannot tell us why the application accepted the command. The business meaning came from the capability: ``` CancelReservation + CancellationContext │ ▼ Cancellation decision │ ▼ ReservationCancelled ``` *status = ‘cancelled’* is one possible representation of that outcome in Application State. The full table retains more than the status. In the companion example, a reservation keeps *confirmed\_at*, *cancelled\_at*, and a constraint that permits *cancelled\_at* only on a cancelled reservation. What a table retains is part of the storage decision, and retaining such facts supports later explanation. The columns still do not define the rules behind cancellation, the authority of the requester, or the possible reasons for rejection. The lock protected this cancellation because the relevant condition lives in an existing row. Not every condition does. Row locks cannot protect the absence of a row; such conditions are protected by the schema itself. In the companion booking capability ([github.com/ricofritzsche/booking-a-stay-example](https://github.com/ricofritzsche/booking-a-stay-example?utm%5Fsource=ricofritzsche.me)), a confirmed reservation inserts one row per night into *listing\_unavailable\_nights*, whose primary key is (*listing\_id, night*). When two commands book an overlapping night, the second insert violates that key: ``` PRIMARY KEY (listing_id, night) concurrent BookStay, same night │ ▼ unique violation on insert │ ▼ BookingRejected: listing_unavailable ``` The violation is not treated as a technical error. The RPU rolls back and returns the business rejection: the listing is unavailable. The basis for the decision changed between loading the context and recording the outcome. Command Context Consistency requires that this stale acceptance is not committed; returning the business rejection is this capability’s response to the conflict. This is where the database has authority. Transactions, row locks, and constraints protect the integrity of Application State under concurrent access. They prevent an outcome from being committed against conditions that no longer hold. An event-sourced implementation has the same responsibility. It may append *ReservationCancelled* only while the event context used by the command remains valid. [Command Context Consistency](https://ricofritzsche.me/aggregateless-event-sourcing/) describes this requirement without making a predefined aggregate the universal consistency boundary. ``` Relational state: transaction, row locks, constraints Event history: conditional append Same requirement: protect the context used by the decision ``` Application State can show what the application recorded. The visible processing path connects that outcome to the command, the relevant context, and the decision that produced it. ### Make the Decision Path Explicit A business decision becomes explainable when the application makes its processing path explicit. The command expresses the intention. The Domain Capability defines the responsibility, the rules, and the possible outcomes. The RPU obtains the relevant context, invokes the decide function, protects that context during the commit, and records the accepted outcome or returns the rejection. Storage remains an explicit implementation decision. Relational transactions and constraints or a conditional append on an event store fulfill the same obligation: the outcome must not be recorded after the basis for the decision has become invalid. Event Sourcing is one way to meet that obligation, not a prerequisite for this architecture. Explaining one specific decision months later may require more than a visible path. The application may need to retain the outcome, the applicable policy version, and enough of the relevant context to show why the result was valid at that moment. How much to retain depends on business, legal, and operational requirements. The RPU gives that information a clear place. A system that retains only the latest values and no decision evidence discards the information its decisions relied on. An event history retains the outcomes in their recorded sequence and derives state from them; a relational model can retain decision-relevant facts in columns or dedicated decision records, while constraints protect the validity of the resulting state. Both are storage decisions. What makes a decision explainable is the explicit path from intention to outcome and what the application deliberately retains along it. A system built this way can answer the question every challenged decision raises: why. *Cheers*! This article was originally published on my [Medium Blog.](https://levelup.gitconnected.com/why-your-software-cannot-explain-its-business-decisions-23f960d3c942?ref=ricofritzsche.me) ### Rethinking REST API Design URL: https://ricofritzsche.me/rethinking-rest-api-design/ Last updated: 2026-07-09T18:58:00.000Z For many years I enforced a strict resource-oriented style for REST APIs. Endpoints had to be nouns. HTTP methods carried the verbs. The API exposed stable resources such as users, offers, reservations, invoices, or bookings, and clients interacted with them only through GET, POST, PUT, PATCH, and DELETE. The goal was consistency and predictability. Resource orientation supplied a shared set of rules that made the external contract easier to understand and to evolve across teams. Over time, however, this discipline produced an unintended effect. The stricter the adherence to resource-oriented conventions, the more domain behavior had to be expressed as mutations on resource representations. Registration, booking, cancellation, approval, withdrawal, or refund turned into updates against a single resource. The actual business meaning moved into request bodies, attributes, or status fields. The same compression happens inside applications when teams translate business situations directly into tables, entities, and status columns. Resource-oriented REST simply performs that translation one layer earlier. A database row becomes a resource. A database operation becomes an HTTP method. The business situation disappears behind the technical contract. REST itself does not require this outcome. The common resource-oriented interpretation simply makes generic CRUD operations the easiest default. REST does not force us to expose CRUD over HTTP, but the common resource-oriented interpretation makes it very easy to do exactly that. If the API boundary should continue to speak the language of the domain, it must surface intentions, decisions, and meaningful state transitions instead of reducing every change to a resource mutation. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2026/07/Screenshot-2026-07-09-at-11.46.17.png) ## REST Was Never Just CRUD Over HTTP REST was defined as an architectural style for network-based systems, not as a convention for naming controllers. Its constraints aim at properties such as scalability, visibility, modifiability, and independent evolution of components. The constraint that matters most for API design is the uniform interface: resources are identified, messages exchange representations, and state transitions can be made discoverable through those representations. A resource is whatever the server chooses to make addressable. HTTP does not restrict it to a database row. A resource can be a stored document, a computed view, a gateway to another system, or a business concept with its own processing rules. A representation is what gets exchanged about that resource, not the internal state itself. This leaves more room than the dominant CRUD reading suggests. A resource does not have to be limited to *User*, *Offer*, or *Reservation* as passive containers of fields. It can also stand for a registration, a booking request, a cancellation request, an approval, or a refund request. These are still nouns, but they come from the business process rather than from storage. The limitation appears when HTTP methods are treated as the domain vocabulary. GET, POST, PUT, PATCH, and DELETE carry protocol semantics. They do not express whether the business situation is a cancellation, a withdrawal, or an approval. Once those distinctions are collapsed into generic updates, the interface stays uniform while the behavior becomes harder to see. ## Command and Query at the API Boundary A clearer model at the boundary is to treat changing state and reading state as separate concerns. Commands express an intention to change something in the system. Queries ask for useful representations of current state. These two responsibilities can be designed and evolved independently. This view makes POST more useful than simple creation. A POST request can submit a booking request, start a cancellation, request a refund, approve a payout, or ask a target resource to process an intention according to its own rules. The application receives the intention, applies the relevant business decision, and produces an outcome. Queries can then be made separately whenever a client needs current information. ```text CRUD-shaped: Resource → Reservation Operation → PATCH Payload → { "status": "cancelled" } Mental model → Change a field ``` ```text Command / Query: Command → CancelReservation HTTP → POST /reservations/123/cancellation-requests Decision → Apply cancellation rules Outcome → ReservationCancelled or CancellationRejected Query → GET /reservations/123/summary ``` Seen this way, REST can remain the delivery style while Command and Query become the model underneath it. The client does not need to know how state is stored. It needs a clear way to express an intention and a clear way to retrieve the representations that matter afterwards. In a behavior-oriented API, the domain operations can be expressed using only two HTTP methods. POST is used to submit intentions (commands). GET is used to retrieve useful representations (queries). ## Address the Intention A behavior-oriented API still needs stable addresses. The difference lies in what those addresses represent. In resource-oriented APIs, the address usually points to the object that is to be modified. In a behavior-oriented design the address can point to the business intention itself. Consider a cancellation expressed as a field change: ```http PATCH /reservations/123 Content-Type: application/json { "status": "cancelled" } ``` This contract presents cancellation as a status mutation. The server may still enforce rules internally, but the public language already frames the behavior as data editing. Clients are encouraged to think in terms of changing fields. A clearer boundary gives the intention its own address: ```http POST /reservations/123/cancellation-requests Content-Type: application/json { "reason": "guest_requested", "requestedBy": "guest" } ``` The client submits a business intention. The application receives that intention, evaluates the relevant state and rules, and records the resulting fact only if the decision is valid. The outside world does not assert a new state. It requests an outcome under the rules of the business. The same pattern applies to other meaningful actions: ```http POST /user-registrations POST /stay-booking-requests POST /offers/123/withdrawal-requests POST /payouts/456/approvals POST /refund-requests ``` Although these endpoints still use nouns, they now represent records of business intent rather than passive data containers. The client expresses what it wants to achieve. The application determines which facts follow from that intent. ## The Client Submits an Intention There is an important boundary between asking the system to do something and telling the system what already happened. A command expresses intent. A fact records an outcome after the business rules have been applied. Mixing both at the API boundary gives the caller authority that usually belongs inside the application. This is why an API should be careful with endpoints that let callers assert outcomes directly. When the client declares what already happened instead of what it wants to happen, it bypasses the application’s responsibility to validate rules and make decisions. A better public boundary accepts the intention and lets the application own the decision: ```http POST /stay-booking-requests Content-Type: application/json { "offerId": "offer_123", "guestId": "guest_456", "from": "2026-09-01", "to": "2026-09-07", "guests": 2 } ``` The response can then describe the outcome: ```http 201 Created Location: /stay-booking-requests/request_789 { "requestId": "request_789", "outcome": "accepted", "reservationId": "reservation_555" } ``` or reject the intention with a domain reason: ```http 409 Conflict Content-Type: application/problem+json { "title": "Stay unavailable", "detail": "The selected offer is unavailable for the requested dates." } ``` This distinction matters beyond any particular storage model. The resulting fact may be stored as an event, written into a relational model, sent to another system, or represented in a projection. The architectural point stays the same: the client expresses intent, the application applies the decision, and only the application records the outcome as fact. ## Query the Business Situation Reading data through queries should receive the same level of attention as changing state through commands. A GET request may be safe from a protocol perspective, but the representation it returns still shapes how clients understand the system. When every query exposes users, offers, reservations, payments, or invoices as generic collections, clients have to assemble business meaning from technical fragments. This works for simple administration. It becomes weak when the client needs to understand a situation. A guest looking for a stay does not need a raw list of offers. A host preparing for arrivals does not need to reconstruct the day from reservation records. A support agent handling a cancellation does not need to fetch several resources and interpret status fields before seeing what happened. ```http GET /offers GET /reservations?guestId=guest_456 GET /payments?reservationId=reservation_555 ``` These endpoints expose stored things. A domain-oriented query exposes the representation needed for the task: ```http GET /available-stays?from=2026-09-01&to=2026-09-07&guests=2 GET /guests/guest_456/itinerary GET /hosts/host_123/upcoming-arrivals GET /support/reservations/reservation_555/context GET /reservations/reservation_555/cancellation-context ``` A query representation can combine information from several places. It can hide internal structure, remove irrelevant fields, and present the facts that matter for the current decision or screen. This keeps the client from rebuilding domain meaning out of storage-shaped resources. A useful representation can also show which intentions are available from the current situation: ```json { "reservationId": "reservation_555", "state": "confirmed", "guestId": "guest_456", "stay": { "from": "2026-09-01", "to": "2026-09-07" }, "availableIntentions": [ { "name": "Request cancellation", "method": "POST", "href": "/reservations/reservation_555/cancellation-requests" } ] } ``` This is where REST can support the domain instead of hiding it. The client retrieves a representation of the current situation, then follows the intentions made available by the server. Commands express what the client wants to achieve. Queries explain the situation in which those commands make sense. ## Domain Rejections Are Part of the Contract When an API accepts intentions, it must also return meaningful outcomes. Acceptance and rejection belong to the same business conversation. A rejected intention is still a decision, and the client needs enough information to continue in a useful way. Generic error responses hide that decision. A plain `400 Bad Request` can be correct for malformed input, and a `500 Internal Server Error` can be correct for an unexpected failure. They are weak responses when the application understood the intention and rejected it because of business rules. A domain-oriented boundary returns a stable reason that reflects the actual decision. The response should identify the kind of problem, explain the rejection at the right level of detail, and include structured fields when the client can act on them. ```http 409 Conflict Content-Type: application/problem+json { "type": "https://api.example.com/problems/stay-unavailable", "title": "Stay unavailable", "status": 409, "detail": "The selected offer has no availability for the requested dates.", "offerId": "offer_123", "from": "2026-09-01", "to": "2026-09-07" } ``` This response carries business meaning. The client can show the reason to the user, suggest different dates, or continue with another flow. It does not need to parse database errors, validation traces, or handler-specific messages. The same principle applies to other rejected intentions: cancellation period expired, payout already approved, refund not allowed, offer incomplete, account closure blocked by open invoices. These are part of the API contract because they are part of the business behavior. A command boundary should make successful and rejected outcomes equally explicit. ## Conclusion If an API only offers resource mutations, clients typically need to manage the business workflow themselves. They must know which steps to take, in which order, and how to handle failures and compensations across multiple calls. An behavior-oriented boundary reverses this. The client expresses what it wants to achieve. The server receives the intention, applies the relevant rules, manages the necessary steps, and produces the outcome. The workflow stays inside the application, where the business decisions belong. Consider a booking process. In many CRUD-shaped APIs, the client must create a reservation, block availability, handle payment, and confirm the booking, often while managing failures across calls. With an behavior-oriented approach, the client submits a single request. The application decides whether the offer is available, whether the guest meets the requirements, whether payment is needed, and what facts to record. This shift does not remove work. It moves the work to where the rules and context are available. The client becomes simpler. The server becomes responsible for correctness. Workflows that previously lived in client code or orchestration layers can now be expressed and enforced at the boundary where they are best understood. *Cheers*! ### Thinking in Events URL: https://ricofritzsche.me/thinking-in-events/ Last updated: 2026-07-03T16:12:55.000Z Most software projects do not struggle because a team cannot persist data, expose an endpoint, or build a form. They struggle when a business situation is translated into technical structure too early. A booking system is a good example. From the outside, it looks simple: someone finds a place, reserves it, pays for it, and arrives. But the simplicity disappears as soon as the real rules enter the conversation. Availability changes while people are searching. Prices depend on dates, policies, fees, and conditions. A reservation can be canceled, modified, rejected, confirmed, expired, or disputed. Each of these situations has a different meaning for the business. The problem is not complexity alone. The problem is losing the language that explains the complexity. A conversation about reservations, availability, guests, hosts, payments, stays, and cancellations slowly turns into records, fields, flags, status columns, handlers, and database operations. Step by step, it becomes increasingly difficult to understand the intent behind the data. This is where architecture discussions can drift from the actual problem. We talk about layers, ports, adapters, repositories, entities, services, messages, and storage patterns. Yet none of these fix a weak understanding of the domain. They only give shape to whatever understanding already exists. Event Modeling is interesting because it starts from a different place. It helps a team describe an information system through business events, decisions, user intentions, and views over time. It keeps the conversation closer to the people who understand the domain before the implementation starts to dominate the language. But Event Modeling is also easy to misunderstand. Thinking in events does not automatically mean that the system must be implemented with Event Sourcing. One is a modeling discipline. The other is an implementation choice. Confusing both turns a useful way of thinking into another technical prescription. This article is about that distinction. It is about Event Modeling as a way to move from technical data operations to business behavior, and about why that shift is valuable even when the final implementation is not Event Sourcing. ## What Event Modeling Is Event Modeling describes an information system by following how business facts unfold over time. It does not start with the internal shape of the software. It starts with examples of the business in motion and asks which facts become true as the system is used. Adam Dymitruk describes this with a hotel booking example: imagine the system already exists and ask which facts would have been captured as time moves forward. The model then adds what users see along the way, so the discussion stays connected to the actual use of the system instead of disappearing too early into internal structure. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2026/07/Business-Reservation-2026-07-03-131525.png) A reservation does not simply get updated; different business facts can happen after it is confirmed. The timeline is not an implementation flow. It is a way to keep order and meaning visible. A reservation is confirmed before it can be changed. A reservation can be canceled by a guest, host, or platform. A no-show is not the same situation as a cancellation. These distinctions matter because they carry different rules, authority, timing, and consequences. Events in Event Modeling are facts, not technical messages by default. `ReservationUpdated` is weak because it says that something changed, but not why it changed. It hides the difference between a guest changing dates, a host canceling a reservation, support correcting a mistake, or the platform marking a guest as a no-show. These situations may affect similar stored data, but they do not describe the same business fact. This is the deeper problem with generic update language. It preserves the mutation and loses the reason. The model no longer shows which situation occurred, which rule made it valid, and which later behavior depends on it. `ReservationCanceled`, `CheckInDateChanged`, or `ReservationMarkedAsNoShow` keep the domain distinction visible. The building blocks of Event Modeling are deliberately simple: events, views, commands, automations, and external systems. Their value is not in the notation itself. Their value is that they connect user-visible information, intent, fact, and consequence in one shared description. Adam’s process makes this order clear: first collect business events, arrange them on a timeline, then add what users see, what they do, and which information supports those steps. Event Modeling is related to Event Storming, but it serves a different purpose. Event Storming is mainly used to explore the problem space. Event Modeling takes the discovered behavior and describes how the system should work over time. The distinction matters because discovery and system description are related, but they are not the same activity. ## Business Language Comes Before Technical Structure A software model is already a translation. The question is where that translation starts. If it starts with tables, endpoints, DTOs, entities, or generic operations, the business language is already being compressed into technical structure. That structure may be necessary later, but it is a poor starting point for understanding the domain. Domain experts do not only provide requirements. They provide the vocabulary that makes the requirements precise. In a booking domain, words such as reservation, guest, host, property, listing, availability, check-in, check-out, cancellation, no-show, refund, and stay are not labels added for nicer screens. They separate situations that have different rules and consequences. Booking.com distinguishes reservation changes, cancellations, no-shows, guests, properties, check-in and check-out. Airbnb’s help material uses reservation, host, guest, listing, stay, cancellation, refund, and check-in in the same business area. This is close to the idea behind Ubiquitous Language in Domain-Driven Design. s Martin Fowler has summarized it, the goal is building a "common, rigorous language" between developers and users. The word rigorous is important. Shared language is not a soft communication exercise. Software does not handle ambiguity well, and business ambiguity does not disappear because the code compiles. A generic word like ***update*** damages that rigor. It says that stored data changed, but it does not say which business situation occurred. A guest changing the check-in date, a host canceling a reservation, a property marking a guest as a no-show, and support correcting a wrong detail can all touch reservation data. Treating these cases as one update makes the model easier to implement too early and harder to understand afterwards. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2026/07/Generic-update-language-preserves-the-da-2026-07-03-153511.png) Generic update language preserves the data change but loses the business reason. Business language also protects the boundary of a decision. A cancellation is not just a different value in a status column. It may depend on who cancels, when the cancellation happens, which policy applies, whether payment has been captured, and whether a refund is due. A no-show has another meaning. A date change has another meaning again. These differences are not implementation details. They are the reason the system exists. Technical structure should support that language instead of replacing it. Once the team understands the business situations clearly, it can still choose tables, documents, messages, projections, workflows, or an event store. But the choice then follows from the behavior that needs to be supported. It is no longer the vocabulary that defines the domain. ## How Event Modeling Changes the Conversation The practical value of Event Modeling is not that a team writes events on a wall. Its value is that the conversation changes before the technical design hardens. Instead of starting with the shape of the data, the team follows a concrete scenario through time. Which information does the guest see before making a reservation? Which rule decides whether the reservation can be accepted? Which fact becomes true after the decision? Which view changes afterwards? Which person or external system has to react? This exposes gaps that a technical model can hide for a long time. A table can contain a status column without explaining who is allowed to change the status, when the change is valid, or which consequence follows. A generic handler can accept a request without making the business difference visible. An Event Model is less forgiving because every fact has to stand in the timeline and explain its place. A cancellation is a good example. Once the model names the fact, the next questions become concrete. Who canceled the reservation? Was it the guest, the host, or the platform? Did it happen before or after the free cancellation period? Does the property become available again? Is a refund due? Does anyone need to be notified? These questions are not technical details. They are the behavior of the system. The same applies to a no-show, a date change, a payment failure, or a reservation that cannot be honored by the property. Each situation creates a different path through the business. Event Modeling helps the team see these paths before they are compressed into fields, flags, and handlers. This also makes the conversation easier for domain experts. They do not need to understand the internal structure of the software to challenge the model. They can look at the timeline and say whether the sequence is correct, whether a rule is missing, or whether a situation has been described with the wrong word. Technical design is still needed. Event Modeling does not remove database schemas, APIs, transactions, workflows, or storage choices. It gives those decisions better input. Once the behavior is visible, the team can decide how to implement it without pretending that the implementation structure is the domain. ## Thinking in Events Does Not Require Event Sourcing Event Modeling and Event Sourcing fit well together, but they are not the same thing. Event Modeling describes behavior. Event Sourcing decides how state is persisted. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2026/07/Thinking-in-Events-Does-Not-Require-Even-2026-07-03-153244.png) Event Modeling names the facts; Event Sourcing makes the fact history authoritative. This distinction is easy to lose because both use events. In an Event Model, an event is a business fact used to describe the system. In Event Sourcing, events become the stored history from which application state is derived. Fowler’s definition is strict: Event Sourcing means that all changes to application state are stored as a sequence of events, and that this sequence can be used to reconstruct past states. That is a persistence decision with consequences. The event store becomes the authoritative record of state. Event Sourcing means storing the full series of actions in an append-only store instead of storing only current state in a relational database. The store acts as the system of record. Event Modeling does not require that decision. A model may contain facts such as `ReservationConfirmed`, `CheckInDateChanged`, or `ReservationCanceled`, while the implementation still writes current reservation state to relational tables. The model can guide the naming of operations, the boundaries of decisions, the views users need, and the consequences that follow. None of that forces events to become the source of truth. Adam Dymitruk makes this separation directly in his work on traditional systems: > “Events happen - whether we store them or not is our choice.” That sentence is the clean boundary. The business may contain meaningful facts even when the system does not persist those facts as an event log. Adam’s article shows Event Modeling applied with table storage and explicitly separates the modeling approach from the event-sourced implementation style. A booking example makes the difference concrete. If a guest cancels a reservation, the model can name the fact as `ReservationCanceled`. One implementation may append that event to an event store and derive the current reservation view from it. Another implementation may update a reservation row, store cancellation details, release availability, and record an audit entry. The model is still useful in both cases because it keeps the business fact visible. The decisive question is where authority lives. If the event history is the authoritative application state, the system is using Event Sourcing. If the current tables are authoritative and events only appear in the model, logs, messages, audit trails, or integration notifications, the system may be event-aware or event-driven, but it is not Event Sourcing. This distinction keeps Event Modeling useful outside the Event Sourcing community. A team can use events to understand the domain without accepting the operational and design trade-offs of an event-sourced system. Event Sourcing may still be the best implementation in some cases, especially when history, auditability, temporal reasoning, and derived views are central. But it should be chosen because the system benefits from events as the source of truth, not because the team used events to understand the business. Event Sourcing becomes especially compelling when the team needs strong auditability, the ability to reconstruct past states, or independent read models that can evolve separately from the write path. ## Conclusion Event Modeling should improve the storage decision, not replace it. Once the team has described the business facts, decisions, views, and consequences, it can choose how state should be persisted. That choice may be Event Sourcing. It may also be relational tables, document storage, projections, workflow state, or a combination. A booking system can expose explicit behavior such as canceling a reservation, changing the check-in date, confirming payment, or marking a guest as a no-show, while still storing current reservation state in relational tables. The model remains valuable because it keeps the behavior visible. The implementation only becomes a problem when these behaviors collapse back into one generic update operation. The reverse is also true. An event store does not guarantee a good model. If the stored events are named `ReservationUpdated`, `GuestUpdated`, or `PropertyChanged`, the system may technically use Event Sourcing while still preserving a CRUD-shaped understanding of the domain. Storing vague events only preserves the vagueness permanently. Event Sourcing becomes a serious option when the factual history is valuable as the authoritative application state: for auditability, temporal reasoning, reconstruction of past state, independent read models, or a clear record of why state changed. These benefits come with costs. Events are long-lived facts. Projections, queries, concurrency, and versioning need explicit design. The storage decision should follow from those needs. Event Modeling helps reveal them, but it should not smuggle in the answer. Its first job is to make the behavior clear. The implementation should preserve that clarity instead of turning the model back into anonymous data mutations. *Cheers*! ### Sources 1. Adam Dymitruk: [Event Modeling: What is it?](https://eventmodeling.org/posts/what-is-event-modeling/?utm%5Fsource=ricofritzsche.me) 2. Adam Dymitruk: [Event Modeling Traditional Systems](https://eventmodeling.org/posts/event-modeling-traditional-systems/?utm%5Fsource=ricofritzsche.me) 3. Event Modeling: [About Event Modeling](https://eventmodeling.org/about/?utm%5Fsource=ricofritzsche.me) 4. Martin Fowler: [Ubiquitous Language](https://martinfowler.com/bliki/UbiquitousLanguage.html?utm%5Fsource=ricofritzsche.me) 5. Martin Fowler: [Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html?utm%5Fsource=ricofritzsche.me) 6. Microsoft Azure Architecture Center: [Event Sourcing pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/event-sourcing?utm%5Fsource=ricofritzsche.me) 7. Software Engineering Radio: [Martin Dilger on Understanding Event Sourcing](https://se-radio.net/2026/05/se-radio-720-martin-dilger-on-understanding-eventsourcing/?utm%5Fsource=ricofritzsche.me) 8. Booking.com Partner Help: [Handling cancellations and guest requests](https://partner.booking.com/en-gb/help/reservations/changes-cancellations/handling-reservation-cancellations?utm%5Fsource=ricofritzsche.me) 9. Booking.com Partner Help: [Marking guest no-shows at your property](https://partner.booking.com/en-gb/help/reservations/overbookings-no-shows/marking-guest-no-shows-your-property?utm%5Fsource=ricofritzsche.me) 10. Airbnb Help Center: [Cancel your home reservation as a guest](https://www.airbnb.com/help/article/169?utm%5Fsource=ricofritzsche.me) 11. Airbnb Help Center: [Rebooking and refund policy for homes](https://www.airbnb.com/help/article/2868?utm%5Fsource=ricofritzsche.me) 12. Airbnb Help Center: [If your host cancels your home reservation](https://www.airbnb.com/help/article/170?utm%5Fsource=ricofritzsche.me) ### Simply Event Sourcing URL: https://ricofritzsche.me/simply-event-sourcing/ Last updated: 2026-07-26T22:32:07.000Z Why do I write this article? Because I see a lot of confusion around Event Sourcing. In my practical work over the last few years, I have repeatedly seen teams perceive Event Sourcing as something complex that is not always necessary. I think the reason for this is easy to explain and understand. The enterprise software development I encountered over the last decades was shaped by object-oriented programming and the tactical design patterns of Domain-Driven Design. For more than a decade, this was also how I thought Event Sourcing worked. In my mind, Event Sourcing was linked to aggregates. *This article was originally published here:* [Simply Event SourcingAggregates Were Never Required — Command Context Consistency and DCB’s Tag-Based Contract![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/icon/10fd5c419ac61637245384e7099e131627900034828f4f386bdaa47a74eae156-59706cb7-1a85-46b9-9942-69e9234d2cb2)Level Up CodingRico Fritzsche![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/thumbnail/0-4N7b-9Ba256YG_mh-3e58d191-4cb4-4127-8c03-3e35e0b218be)](https://levelup.gitconnected.com/simply-event-sourcing-7ae694e71ce2?sk=b4b0f93d6c866891d24216e3ca318e0c&ref=ricofritzsche.me) The problem with this entire approach is that we always have to start by designing aggregates and building centralized models of objects like *Customer* or *Order* in order to establish consistency boundaries. This means making too many assumptions early on about how things might be. All subsequent behavior must inevitably fit into this structure. Experience has shown me that this makes the development process inflexible and cumbersome. I really came to understand this when I was working on the idea of self-contained feature slices but was still stuck thinking within the confines of Domain-Driven Design (DDD). It is impossible to construct a slice independently as long as it depends on central object structures that own the state, define the consistency boundaries, and determine how events are stored. Almost exactly a year ago, in June 2025, I published an article titled *Aggregateless Event Sourcing*¹, which explored this idea. In the article, I explained that aggregates are not necessary to achieve consistency. The context arises from the command, which is why Ralf Westphal² and I now use the term *Command Context Consistency* for this approach. Event Sourcing itself is simple and does not depend on the tactical design patterns of DDD. Two years before my article, in April 2023, Sara Pellegrini³ published the first chapter of her *Kill the Aggregate* series. She described the limitations imposed by aggregates and later showed that a command handler can build the exact data structure needed for a particular decision from the relevant event history. There is no need to rebuild a permanent object structure containing everything that happens to share the same noun. Looking back, I realize that the title of my own article shows how strongly Event Sourcing was still linked to aggregates in my mind. **Calling something *aggregateless* gives the aggregate a position in the original definition and then describes how to remove it.** The definition I use is close to the one Mathias Verraes⁴ formulated in 2019: > A system is eventsourced when the single source of truth is a persisted history of the system’s events; and that history is taken into account for enforcing constraints on new events. Martin Fowler’s⁵ description is similarly broad. > Event Sourcing ensures that all changes to application state are stored as a sequence of events. It says that changes to application state are captured and stored as a sequence of events from which state can be reconstructed. Neither definition introduces aggregates, aggregate roots, entity lifecycles, or per-aggregate event streams. ### Implementation Became the Definition A common aggregate-based Event Sourcing implementation follows a familiar sequence. A command identifies an aggregate instance, the event stream belonging to that instance is loaded, an object is rebuilt from those events, a method is called on the object, and the resulting events are appended if the expected stream version still matches. This is a coherent way to implement Event Sourcing. The aggregate acts as the decision structure and the concurrency boundary at the same time. No one carried Event Sourcing further into practice than Greg Young. He taught the pattern to the DDD community for years, paired it with CQRS, described current state as "a left fold of previous behaviours", and this implementation is the one his work made standard. The problem begins when this specific implementation is presented as the definition of Event Sourcing. The aggregate pattern groups related entities and value objects into a unit for data changes and uses that boundary to protect consistency. When it is combined with Event Sourcing, the aggregate boundary is usually materialized as an event stream. Every event is assigned to one aggregate instance, and optimistic concurrency protects the version of that entire stream. The consequence is that once the aggregate boundary is embedded in the event stream structure, it becomes much harder to change later. It changes the order in which we think. Before recording a fact, we ask which aggregate owns it. Before implementing a command, we ask which aggregate must handle it. When a rule needs information associated with two aggregates, we immediately have a cross-aggregate consistency problem, although the business rule itself never mentioned aggregates. The structure came first and the behavior was forced into it afterwards. ![](https://cdn-images-1.medium.com/max/800/1*cvV6ATvVvDo2PF7QEp7fSw.png) Figure 1: Classic aggregate stream versioning flow This also explains why aggregate-based systems tend to accumulate behavior around nouns. Commands that mention the same *Customer*, *Order*, or *User* are placed into the same aggregate even when they need completely different facts and protect unrelated rules. The aggregate provides one predefined boundary for all of them because they happen to share the same conceptual object. But Event Sourcing itself does not require this boundary. ### Start with the Decision Consider a command that registers a user with a particular username. The decision requires an answer to a specific question: has that username already been claimed? The relevant history might consist of events recording the registration, change, release, or reservation of that username. The command does not require every event ever recorded about every User object. It does not care whether another user changed a profile picture, accepted new terms, or updated a postal address. The command context is the set of facts required to decide whether the username may be registered. From those facts, the Domain Capability⁶ handling the command derives temporary decision data representing whether the username is available. The capability applies the rule and, when the username is available, produces a *UserRegistered* event. Another command involving the same user can require an entirely different context. Updating a display name and suspending an account both mention a user, but that shared noun does not prove that the same facts or the same concurrency boundary are required. Each command therefore requires its own view of the history. This is the important inversion. The context follows the decision instead of the decision following a predefined object boundary. ![](https://cdn-images-1.medium.com/max/800/1*pQ1hMmCCHielY7vwpJ4uWg.png) Figure 2: Building a decision from the command context The event history remains shared because it represents what happened in the application. Its interpretation remains local because the Domain Capability handling a command selects and projects the facts required for that decision. The capability can therefore own its rules, its temporary decision structure, and the events it produces without depending on a central object that attempts to represent the entire application. An event such as *UsernameChanged* is then a fact that can be interpreted wherever it is relevant. It does not have to be treated as an internal state transition owned by a User aggregate. A user identifier can still be part of the event data, but that identifier is data describing what happened rather than proof that the event belongs to a predefined aggregate stream. ### Event History Is More Than an Audit Trail The distinction between Event Sourcing and an audit log is important here. A state-based system can update a row and then write an event describing the update. It can publish that event to a message broker and retain it indefinitely. This still does not make the event history the source of truth when the current-state database remains authoritative and future decisions are based on that database. Under the definition used in this article, the direction is reversed. New state is expressed by recording new events. Derived state can be stored for efficient access, but it can be reconstructed from the event history. More importantly, the relevant history is considered when deciding whether the next events may be accepted. That second part separates Event Sourcing from merely projecting or analyzing an existing event log. This means that projections are derived data. They can be rebuilt, replaced, or designed for a specific query. They may be updated synchronously or asynchronously, depending on the requirements of the application. None of these choices changes which data is authoritative. ### Consistency Is a Separate Question Once a command has read its context and made a decision, another process may append a conflicting event before the command records its own result. The command would then be based on history that is no longer current. Every practical implementation therefore needs a way to protect the relationship between the facts used for a decision and the new facts produced by that decision. Aggregate-based Event Sourcing handles this with the expected version of an aggregate stream. The append succeeds only when no event has been added to that stream since it was read. This protects consistency, but it protects the entire stream. An unrelated event in the same aggregate stream also invalidates the decision and causes a conflict or retry. Command Context Consistency (CCC)² takes the command’s actual context as the boundary. The Domain Capability handling the command selects the relevant events, derives the data required for the rules, and produces new events. During the append, the event store verifies that no relevant events have appeared since that context was read. Other events may have been recorded in the meantime, but they do not invalidate the decision when they do not match the context query. Consistency is checked against a context, and the stability of that context is ensured while recording the resulting events. Dynamic Consistency Boundary (DCB)⁷ follows the same basic idea. A command queries the events required for its decision, and the append must fail when relevant events have been recorded since that context was read. At this level, DCB applies the same principle of Command Context Consistency. It protects the decision against concurrent changes in the relevant event history instead of protecting a predefined aggregate stream. The difference is not what constitutes application state. In both CCC and DCB, the persisted event history represents the application state. The difference lies in how the relevant part of that history is made queryable for a command. In CCC, an event conceptually contains an event type, a sequence number, a timestamp, and a payload. CCC defines the conceptual requirements for protecting a command context, but it does not prescribe a concrete event store API or query representation. A context query may, for example, select events by type and apply a predicate such as `accountId = "abc1234"` to the payload. An implementation may add indexes, derived tags, or other access structures to execute such queries efficiently, but CCC does not require them. The producer therefore does not have to anticipate how other commands may later query the event. DCB makes tags part of the required event store contract. A DCB event contains an event type, event data, and tags, while the event data remains opaque to the event store. Events are selected through the DCB query contract by event type and tags rather than by predicates against the payload. Any payload value needed to select events through this query contract must therefore be represented by a tag when the event is stored. For example, an account identifier contained in the event data may also be exposed as a tag such as `account:abc1234`. These tags therefore define a queryable view of the event at write time. This is the actual distinction between CCC and DCB. Both retrieve the event context required by a command and protect the append with a query-based condition. In the usual read-decide-append flow, the condition detects whether relevant events have appeared since the command observed its context. CCC leaves the representation and execution of that query to the implementation. Tags, indexes, or other access structures may be introduced as optimizations. DCB makes event types and tags part of the required query contract while the event data remains opaque to the event store. Any value that a command must use for a selective consistency query therefore has to be exposed as a tag when the event is written. DCB is consequently a specific tag-based event store contract for applying the same consistency principle rather than a separate consistency model. Both CCC and DCB address consistency in an event-sourced system. Neither is a synonym for Event Sourcing. ![](https://cdn-images-1.medium.com/max/800/1*e5HhPGuzgMwIPmwvlYlMfg.png) Figure 3: Aggregate stream versioning and the relationship between CCC and DCB Aggregate stream versioning is another way to protect consistency within an event-sourced system. The meaningful comparison is therefore between aggregate stream versioning and consistency based on the command context: how each selects the relevant facts, detects conflicting changes, supports efficient queries, and allows the consistency boundary to evolve. CCC defines the principle without prescribing a particular query representation, while DCB specifies a tag-based event store contract for applying it. Comparing DCB directly with Event Sourcing therefore compares an event store contract for consistency with the persistence concept it supports. ### CQRS and Event-Driven Messaging Are Separate Concepts Command Query Responsibility Segregation (CQRS) is also regularly included in descriptions of Event Sourcing, although it addresses another question. CQRS separates the handling of commands from the handling of queries. Event Sourcing combines naturally with this separation because query-specific projections can be derived from the event history, but CQRS does not require Event Sourcing, and Event Sourcing is not defined by CQRS. Microsoft’s own architecture guidance⁸ describes Event Sourcing as a pattern that some CQRS implementations incorporate, rather than as the same pattern. The same is true for event-driven messaging. An event-sourced application may publish recorded events to other components, but the event store is not a message broker, and Event Sourcing does not require a distributed system. A single application using one relational database and an append-only event table can already be event-sourced when the event history is authoritative and participates in decisions. Microservices, asynchronous projections, message brokers, distributed sagas, and aggregate frameworks are architectural choices that may appear in an event-sourced system. Combining all of them and presenting the result as Event Sourcing explains why teams perceive the concept as much more complex than it is. ### A Simple Concept Still Requires Engineering Calling Event Sourcing simple does not mean that every event-sourced system is easy to build. Production systems still have to deal with evolving event structures, atomic persistence, efficient access to history, derived views, external side effects, and failures. These are real engineering questions. The point is that these questions should be discussed as individual design problems instead of being bundled into the definition of Event Sourcing. A system that serves millions of requests across several regions has different operational requirements from a single application using PostgreSQL. The persistence concept remains the same in both cases. The same is true for consistency. A command that checks a unique username may require a narrow context. Another command may require facts about thousands of products or a globally ordered sequence. The boundary follows the rule that must be protected. Event Sourcing does not promise that every rule will be cheap or local, but it also does not require every rule to be forced through an object boundary chosen before the rule was understood. This gives us a much simpler starting point. A command arrives with an intention. The Domain Capability handling it determines which recorded facts are relevant to that intention, derives the data needed for the decision, applies its rules, and produces new facts. Those facts are recorded only when the context on which the decision was based is still valid. That is enough to describe the event-producing part of an event-sourced system. Everything beyond it is an implementation decision. ### Conclusion Looking back, *Aggregateless Event Sourcing*¹ was a necessary title for me at that point because it named what I was trying to remove from my own thinking. Today I see the limitation of the term more clearly. It still starts with the aggregate and then describes Event Sourcing as a variation that works without it. But the truth is that Event Sourcing never required aggregates. An event-sourced system persists its accepted facts as the authoritative history. The Domain Capability handling a command interprets the facts relevant to the decision, applies its rules, and produces new facts. Those facts are recorded only if the relevant context has remained stable. Aggregate stream versioning can protect a predefined stream. CCC defines consistency against the event context required by the command, while DCB specifies a tag-based event store contract for applying the same principle. Event Sourcing remains the underlying persistence concept. Keeping these terms separate allows a useful discussion about each of them. Teams can evaluate whether an aggregate stream is an appropriate consistency boundary or whether the command’s actual event context should define it. They can then decide whether a DCB-compatible tag contract fits their access requirements or whether another implementation of CCC is preferable. None of these choices is a prerequisite for preserving facts as the source of truth. That is why I dropped the qualification “aggregateless”: the approach I am describing is simply Event Sourcing. *Cheers*! ***Sources:*** 1. [Rico Fritzsche: Aggregateless Event Sourcing](https://ricofritzsche.me/aggregateless-event-sourcing/) 2. [Ralf Westphal: Command Context Consistency](https://ralfwestphal.substack.com/p/command-context-consistency) 3. [Sara Pellegrini: Chapter 1 — I am here to kill the aggregate](http://Chapter%201 - I%20am%20here%20to%20kill%20the%20aggregate?utm%5Fsource=blog.ricofritzsce.de) 4. [Mathias Verraes: Eventsourcing: State from Events Events as State](https://verraes.net/2019/08/eventsourcing-state-from-events-vs-events-as-state/?ref=ricofritzsche.me)? 5. [Martin Fowler: Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html?utm%5Fsource=blog.ricofritzsche.de) 6. [Rico Fritzsche: Autonomous Domain Capabilities](https://medium.com/gitconnected/autonomous-domain-capabilities-why-layered-architecture-is-breaking-down-9b5bf5d81ba6?ref=ricofritzsche.me) 7. [Dynamic Consistency Boundary: Specification](https://dcb.events/specification?utm%5Fsource=blog.ricofritzsche.de) 8. [Microsoft Learn: Command Query Responsibility Segregation (CQRS) pattern](https://learn.microsoft.com/en-us/azure/architecture/patterns/cqrs?utm%5Fsource=blog.ricofritzsche.de) 9. [Greg Young: Functional Domain Models and Event Sourcing](https://gregfyoung.wordpress.com/2012/10/01/functional-domain-models-and-event-sourcing/?ref=ricofritzsche.me) ### Why the Domain Is Defined by Domain Capabilities, Not Object Models URL: https://ricofritzsche.me/why-the-domain-is-defined-by-domain-capabilities-not-object-models/ Last updated: 2026-06-17T14:07:02.000Z After publishing [my last article](https://medium.com/gitconnected/request-processing-units-and-reactors-0fba33071d3e?sk=0ea397131da626244500036be16547e6&ref=ricofritzsche.me) about Request Processing Units / Reactors, I noticed that some parts were probably harder to understand than I expected. Especially the project structure led some developers to mentally map the approach back to horizontal layered architectures and centralized technical ownership, even though the architectural cut is fundamentally different. It is important to recognize that layering and Separation of Concerns (SoC) are not the same thing. Layered architectures separate software through centralized technical ownership. One domain capability becomes physically fragmented across controllers, services, repositories, entities, mappers, and persistence models. Understanding what a single capability actually does means traversing multiple technical abstractions spread across the entire system. The approach I described applies a completely different cut. The domain capability itself becomes the primary ownership boundary. The full processing of one capability lives in one place, including the localized imperative shell around loading and persisting facts as well as the functional core responsible for context interpretation, business decisions, and consequence generation. The technical separation exists locally inside the Request Processing Unit (RPU) itself instead of globally across centralized technical layers. This distinction sounds subtle at first, but it changes the coupling structure of the entire system. And I increasingly believe this is the actual architectural shift behind the approach, far more than the terminology around RPUs or Reactors themselves. *This article was originally published here:* [*https://levelup.gitconnected.com/why-the-domain-is-defined-by-domain-capabilities-not-object-models-8921b454c2cd?sk=a62fcc2e7321f5873cd30fe820525c0e*](https://levelup.gitconnected.com/why-the-domain-is-defined-by-domain-capabilities-not-object-models-8921b454c2cd?sk=a62fcc2e7321f5873cd30fe820525c0e&ref=ricofritzsche.me) ### Distributed Technical Ownership One thing became very obvious while discussing the architecture with others: many developers instinctively equate Separation of Concerns with layering. This is understandable because most software architectures of the last decades taught us exactly that. Controllers handle transport concerns, services contain or coordinate logic, repositories access persistence, entities represent the domain model, and mappers translate between structures. The domain capability itself emerges only as the combined result of all those technical parts. ![](https://cdn-images-1.medium.com/max/800/1*s8xNS7t3540oOAapfD63BA.png) In typical layered, object-oriented architectures, one domain capability is processed across multiple technical ownership boundaries. The capability emerges through the cooperation of controllers, services, domain models, repositories, mappings, and persistence structures distributed throughout the system. This creates a very specific ownership structure inside the system. The technical layers own different aspects of the capability, which means the capability itself becomes physically fragmented across the architecture. To understand what ‘register a tool’ actually does, it is necessary to traverse multiple technical ownership boundaries spread across the system. In other words, layered architectures are systems of distributed technical ownership. The system is organized around technical ownership boundaries instead of autonomous domain capabilities. A single capability therefore does not exist as one coherent ownership boundary but is reconstructed through cooperation between multiple technical structures. The consequence is not only additional indirection or functional dependencies. The much bigger consequence is coordination pressure. Every new domain capability partially integrates into already existing technical structures, shared abstractions, dependency inversions, entity models, and repositories. Over time these shared ownership centers become bottlenecks because many otherwise independent capabilities continuously depend on the same structures evolving together. Vertical Slice Architecture already moved software structure significantly toward domain capabilities instead of purely technical layers. But the architectural center frequently remains shared internal ownership structures underneath the slice itself. The feature or request becomes the organizational boundary of the project structure while the actual processing of the domain capability is still reconstructed through multiple internal coordination boundaries. The capability therefore remains structurally distributed even though the packaging appears vertical. ### Localized Capability Ownership Autonomous domain capabilities in the form of RPUs apply a fundamentally different ownership model. I also described this shift from another perspective in this article [Why I Replaced Enterprise OOP Thinking with Feature-Local Logic](https://medium.com/gitconnected/why-i-replaced-enterprise-oop-thinking-with-feature-local-logic-9a70c461d889?ref=ricofritzsche.me). The primary boundary is no longer the technical layer but the domain capability itself. An RPU owns the complete processing of one domain request, including loading relevant facts, building the [command context](https://medium.com/gitconnected/simplicity-wins-command-context-consistency-600b913f74eb?ref=ricofritzsche.me), evaluating business rules, generating consequences, and persisting the resulting facts. The architecture applies a localized Functional Core / Imperative Shell structure inside each capability boundary where pure business decisions and infrastructure concerns remain clearly separated without fragmenting the capability itself across the system. ![](https://cdn-images-1.medium.com/max/800/1*W90XnAysgquempUu26jKJQ.png) The RPU becomes the primary ownership boundary of the domain capability. The Imperative Shell encapsulates infrastructure and IO concerns, while the Functional Core focuses on context interpretation, business rules, and consequence generation. ***Note:** *Solid arrows represent the internal data flow of the capability. Dashed arrows represent interaction with external infrastructure such as the Event Store or optional Providers.* The practical consequence is that understanding one domain capability no longer requires traversing large parts of the system. The complete processing remains localized in one place. To understand how a tool is registered for instance, the developer opens the *register\_tool* RPU folder. The structure remains simple because the capability itself is the ownership boundary instead of being reconstructed through multiple technical structures distributed across the system. ![](https://cdn-images-1.medium.com/max/800/1*io3Kpfcxxa_kD_pYBeTaQQ.png) RPU folder structure example The important distinction is that Delivery Mechanisms, Reactors, and Providers do not become ownership centers of the domain capability itself. They exist to connect, coordinate, or translate, but they do not absorb the domain decision boundary. External interactions such as HTTP requests, CLI commands, or incoming messages are translated into internal domain requests and transformed back into client-compatible responses. A Reactor coordinates data flow when one interaction requires several domain capabilities or external systems to participate together. Providers encapsulate access to infrastructure and external resources. None of these structures define the domain capability itself. The domain remains centered around autonomous capabilities interpreting facts and producing consequences independently within their own decision boundaries. The domain itself behaves as a functional core. Application state is not stored as mutable object graphs continuously coordinated across the system. Instead, the state is derived from an immutable sequence of recorded facts inside the Event Store. This changes how the domain evolves over time. New capabilities emerge as independent processing units without continuously restructuring a centralized mutable domain model. ### User Interactions and Domain Capabilities Are Different Things One of the most important distinctions in the RPU approach is the separation between user interactions and domain capabilities. Both are frequently treated as the same thing in software architecture even though they describe completely different concerns. A user interaction describes how the outside world engages with the system. It starts with an external trigger such as an HTTP request, a UI action, a CLI command, or an incoming message and ends when the system returns a response. User interactions are shaped by presentation concerns, workflows, client requirements, transport protocols, and user experience. ![](https://cdn-images-1.medium.com/max/800/1*9BXf-cEXOe2EnfP1b9Yf-A.png) A domain capability describes something fundamentally different. It defines one autonomous business decision boundary inside the system. A capability owns the complete processing of one specific business responsibility including interpreting relevant facts, evaluating business rules, and producing consequences. In other words, all business rules are encapsulated within the domain as a whole. Not in aggregates, but in the domain. That is precisely where the problem with Domain-Driven Design lies. Treating user interactions and domain capabilities as the same architectural unit creates large coordination structures almost automatically. One user interaction frequently combines multiple domain capabilities, external systems, validation steps, data loading operations, and response transformations. When all of that becomes one architectural ownership boundary, the result is usually a large handler, application service, workflow object, or aggregate orchestration layer where business meaning slowly accumulates again. The RPU approach separates those concerns intentionally. - User interactions remain external interaction boundaries. - RPUs remain internal business capability boundaries. - A Reactor may coordinate several capabilities when one interaction requires multiple processing steps, but the participating RPUs still own their business decisions independently. This distinction is important because user interactions and domain capabilities evolve for fundamentally different reasons. A frontend workflow or external API may change completely while the underlying business capability and decision boundary remain stable. Separating both prevents external interaction structures from becoming the ownership model of the domain itself. ### Why Delivery Mechanisms, Reactors, and Providers Are Not Layers Delivery Mechanisms, Reactors, and Providers can superficially resemble presentation layers, service layers, and infrastructure layers from traditional architectures. But the ownership model underneath is fundamentally different. In layered architectures, horizontal structures participate directly in the ownership of every domain capability. Controllers, services, repositories, aggregates, and orchestration structures collectively reconstruct the capability through cooperation across the system. The capability itself therefore does not exist as one autonomous ownership boundary. Delivery Mechanisms, Reactors, and Providers do not participate in the ownership of the domain capability itself. The capability already exists fully inside the RPU. Fact loading, command-context construction, business rules, consequence generation, consistency evaluation, and persistence flow remain localized inside the capability boundary. ![](https://cdn-images-1.medium.com/max/800/1*Y9eGCEzQpTEoIHGhm_w-Og.png) A Delivery Mechanism translates external requests into internal domain requests and transforms the resulting domain responses back into client-compatible responses. The domain capability itself remains fully owned by the RPU. A Delivery Mechanism translates an external interaction into an internal domain request and transforms the result back into a client-compatible response. The interaction boundary itself remains capability-local instead of becoming a shared horizontal structure such as *UserController* or *ToolController*. A *RegisterToolHttpHandler* belongs only to the *register\_tool* capability and does not become a shared ownership structure for unrelated capabilities. Reactors also do not become centralized orchestration layers where displaced domain logic accumulates. A Reactor coordinates already autonomous domain capabilities when one user interaction spans multiple decision boundaries. The participating capabilities continue to own their business decisions independently. Providers encapsulate infrastructure access without containing knowledge about the domain capability being processed. Layers participate in the ownership of every capability horizontally across the system, and that is where the difference lies. Delivery Mechanisms, Reactors, and Providers remain peripheral to already complete domain capabilities. ### Conclusion The important shift behind the RPU approach is not new terminology or another variation of feature-oriented packaging. The deeper shift is the representation of the domain itself. The domain no longer acts as a shared mutable object model continuously coordinated across repositories, services, aggregates, and orchestration structures. Autonomous domain capabilities interpret immutable facts, evaluate business rules, and produce consequences within localized decision boundaries. The domain therefore behaves as a deterministic functional core instead of a centralized mutable structure. This changes how systems evolve over time. New capabilities emerge as independent processing units instead of continuously extending shared behavioral centers and coordination structures. The system grows additively through autonomous domain capabilities that remain locally understandable, independently evolvable, and directly aligned with real domain responsibilities. *Cheers*! ### Request Processing Units and Reactors URL: https://ricofritzsche.me/request-processing-units-and-reactors/ Last updated: 2026-06-17T14:03:32.000Z Today is the day I finally get rid of several terms I used in recent months and in previous articles. I wrote a lot about Feature Slices and about how to apply that concept in real systems. But don’t worry: the underlying concepts did not change. This article is about terminology and linguistic precision. It is time to clarify the language and bring the concepts into a more coherent form. The term Feature Slice, or simply Slice, is ultimately imprecise. It emerged from a movement that tried to break away from dominant software structures built around horizontal technical layers, shared services, repositories, and heavily centralized models. For that purpose the term was useful, but it never described the actual architectural unit particularly well. A “feature” can mean almost anything. A “slice” describes shape, not responsibility. The more I refined the underlying structure, the more obvious it became that the terminology no longer matched what I was actually describing. The architecture I describe is not built around slices. It is built around Request Processing Units, Reactors, Interactions, Delivery Mechanisms, Providers, and an Event Store as application state. These terms are more precise because they name responsibilities instead of shapes. RPUs describe internal domain capabilities. Reactors describe the technical response to an Interaction when several building blocks have to be coordinated. Delivery Mechanisms describe the translation between the outside world and the internal processing structure. *This article was originally published here:* ### Core Philosophy The architecture I am talking about is built around self-contained Request Processing Units (RPUs) as its primary building blocks. Earlier, I called these building blocks Feature Slices. And there was a time when I believed very strongly that these units had to contain absolutely everything, including transport protocols such as HTTP. I no longer think that is correct because an RPU is not an external interface. It is an internal processing unit of the domain. ![](https://cdn-images-1.medium.com/max/800/1*A5QVT5BnV4T08jmpmbJfMw.png) An RPU implements exactly one internal domain capability: one domain request, either a Command or a Query. It contains the complete responsibility for processing that request: loading relevant facts, building context, evaluating business rules, generating a result or consequences, and executing the imperative shell around the pure decision logic. ![](https://cdn-images-1.medium.com/max/800/1*vDUK6SxsZaA00vq4Aa6oog.png) The important distinction is that RPUs are intentionally decoupled from external interactions. One external interaction may be fulfilled by exactly one RPU. But it may also require several RPUs coordinated together through what we call a Reactor. A Reactor describes the technical response to an Interaction. It coordinates RPUs and Providers when the system needs to produce one coherent response from several building blocks. It does not own the domain decisions of the participating RPUs. ![](https://cdn-images-1.medium.com/max/800/1*nX4JpPBTT8xBNXEB7i4-7g.png) This distinction matters because external interactions and internal processing boundaries are not the same thing. Treating the external interaction itself as the architectural unit usually leads back toward oversized transactional services, broad handlers, or artificial aggregate boundaries. The RPU building block instead allows the domain to choose its own internal granularity. The goal is maximum locality of behavior, minimal technical layering, and extreme changeability. Duplication between RPUs is accepted intentionally. Similar logic may exist in several RPUs because independence of processing units is more valuable than aggressive DRY abstractions. RPUs do not depend on each other, and they do not reuse internal functions from other RPUs. RPUs are therefore self-sufficient and intentionally substantial. They are the place where real domain decisions happen, so they form the foundation of the domain. Reactors are different: they stay lightweight and flexible. Shared abstractions frequently centralize behavior again and slowly reintroduce coupling between otherwise independent capabilities. Code generation can mitigate some duplication later if necessary. But locality and independence come first. ### Terminology Before going further, I want to make the core terms explicit. These are the terms I use from now on, and they describe different perspectives of the same system. Some of them are technical. Some of them describe the business view. Mixing them is exactly what created part of the confusion around the older term Feature Slice. - **RPU (Request Processing Unit):** Atomic, self-contained processing unit that implements exactly one internal domain capability, which means one domain request, either a Command or a Query. - **Reactor:** Optional technical integration unit that is introduced when one Interaction requires several RPUs or Providers to be coordinated. A Reactor is the technical counterpart to an Interaction: the Interaction brings the trigger from the outside, and the Reactor coordinates the immediate response of the system. It does not contain the domain decision logic of the participating RPUs. - **Interaction:** The user-facing process that starts when an external trigger occurs and runs from collecting the required input to returning the output. It can be fulfilled by one RPU or by several RPUs coordinated by a Reactor. - **Use Case:** Higher-level business description from the user or stakeholder perspective. A Use Case contains one or more Interactions. - **Delivery Mechanism:** Thin translation layer that converts external requests, for example HTTP or CLI, into commands for an RPU or a Reactor and converts the result back into the desired output format. - **Providers:** Infrastructure components that RPUs and Reactors use when they need to interact with the outside world, for example databases, ID generators, blob storage, or external APIs. Providers are always passed in from the outside and never live inside an RPU. - **Application State (Event Store):** The central, single source of truth of the system. It consists of the immutable sequence of events, or facts, that all RPUs load context from and Command RPUs append new consequences to. The entire application state is derived from this event store. ![](https://cdn-images-1.medium.com/max/1200/1*J5PRq6ZDrDd3CQnpmceofw.png) A Use Case and an Interaction describe the user-facing perspective. The Delivery Mechanism translates the external request. Internally, a Reactor may coordinate multiple RPUs. The Event Store remains the durable place for recorded facts. Note: *In this terminology, a Reactor is not an event handler, subscriber, projection, saga, or process manager. It does not react to recorded facts after the fact. It coordinates the system response to an Interaction.* ### Recommended Project Structure Here is the project structure I currently use and recommend for this approach. The important change is that the internal processing units are named explicitly. They are not hidden behind generic folders such as services, use cases, application, or domain. The code structure should make the processing approach visible. ```bash src/ ├── rpunits/ ← All atomic RPUs │ ├── register_tool/ │ │ ├── load_context.rs │ │ ├── build_context.rs │ │ ├── generate_consequences.rs ← pure Functional Core │ │ ├── append_consequences.rs │ │ ├── process_request.rs ← Imperative Shell (store + providers) │ │ └── mod.rs │ ├── check_out_tool/ │ ├── return_tool/ │ └── get_inventory/ │ ├── reactors/ ← Optional, only created when needed │ └── prepare_tool_rental/ │ ├── command.rs │ └── process.rs ← technical response coordination │ ├── http/ ← Delivery Mechanism (protocol translation) │ ├── routes.rs │ └── handlers/ │ ├── register_tool_handler.rs │ └── ... │ ├── events/ ← Pure data only ├── store.rs ← Application store └── providers/ ← Infrastructure (database, HTTP adapter, etc.) ``` This structure makes the distinction visible in code. Atomic processing units are placed under *rpunits*. Reactors are separated into *reactors* only when they are needed. HTTP is only one delivery mechanism and stays outside the RPUs. The individual files inside an RPU describe the processing steps directly. The structure is intentionally boring. It avoids generic technical buckets and keeps the important question visible: which internal request is processed here? ### User Interface, Delivery Mechanism, and Presentation Concerns RPUs and Reactors are protocol-agnostic. They are not tied to HTTP, a web UI, a CLI, or any other external delivery form. The Delivery Mechanism translates external requests into RPU or Reactor commands and converts the result back into the required output format. Different delivery forms can translate into the same domain request without changing the RPU itself. ![](https://cdn-images-1.medium.com/max/800/1*bP68LhlJVFAAvkXTbMKZrw.png) Some requirements have both a domain part and a presentation part. A simple example is user registration with repeated password input. The domain rule is that both passwords must match. That rule belongs inside the responsible RPU, for example in *rpunits/register\_user/generate\_consequences.rs*. The presentation concern is that the frontend shows two password fields and may provide immediate client-side feedback. That belongs in the user interface, for example in JavaScript. This distinction keeps the RPU responsible for the domain request while the user interface remains responsible for how the interaction is presented to the user. ### Concrete Example: prepare\_tool\_rental To make the distinction more concrete, imagine a user wants to prepare a tool rental in one user-facing Interaction. From the user’s perspective, this may look like one coherent action: a tool should be available in the system and then checked out to a team for a specific job site. Internally, this is not necessarily one RPU. The current tool rental example already separates the main capabilities into Register Tool, Check Out Tool, Return Tool, and Get Inventory. The command RPUs record facts such as *tool-registered*, *tool-checked-out*, and *tool-returned*, while Get Inventory builds the current inventory view from the recorded facts. A Reactor such as *prepare\_tool\_rental* could coordinate two RPUs: ```bash reactors/ └── prepare_tool_rental/ ├── command.rs └── process.rs ``` The Reactor receives one command for the larger interaction. It first calls the *register\_tool* RPU when the tool is not yet known in the system. It then calls the *check\_out\_tool* RPU to check out that tool to the requesting team. The important part is that the Reactor does not absorb the logic of those RPUs. Register Tool still owns the decision whether a tool can be registered. Check Out Tool still owns the decision whether a tool can be checked out. The Reactor only coordinates the sequence when the business interaction needs both steps. In code, the structure could look like this: ```text src/ ├── rpunits/ │ ├── register_tool/ │ ├── check_out_tool/ │ ├── return_tool/ │ └── get_inventory/ │ ├── reactors/ │ └── prepare_tool_rental/ │ ├── command.rs │ └── process.rs │ └── http/ └── handlers/ └── prepare_tool_rental_handler.rs ``` The HTTP handler is only the delivery entry point. The Reactor is the coordination unit. The RPUs remain the atomic processing units. ### Conclusion By replacing the imprecise Feature Slice terminology with Request Processing Units, the architecture finally has clearer, more consistent, and more honest language. RPUs give us precise, self-contained units for internal domain capabilities, intentionally decoupled from external user interactions. Reactors remain lightweight and optional. They only appear when one Interaction requires several RPUs or Providers to be coordinated as one system response. Presentation and protocol concerns stay in the Delivery Mechanism. Domain requests are processed by RPUs. Coordination, when needed, becomes explicit through a Reactor. The result is an architecture that maximizes locality, independence, and long-term maintainability while staying simple and focused on real business needs. *Cheers …*and goodbye, Feature Slices! ### Functional Core / Imperative Shell for Agentic Coding URL: https://ricofritzsche.me/functional-core-imperative-shell-for-agentic-coding/ Last updated: 2026-04-14T09:10:19.000Z In my [previous article](https://levelup.gitconnected.com/agentic-coding-reveals-what-self-contained-feature-slices-actually-are-930417f21a0c?utm%5Fsource=ricofritzsche.me), I described feature slices as boundaries of ownership. I will take that as given here and move directly to the practical question: how does a code repository have to look if an agent is supposed to generate that shape reliably? People talk about prompting, model quality, or tool choice, while the code repository keeps teaching the agent the wrong structure. If the codebase still exposes services, controllers, repositories, managers, helpers, common folders, generic file names, and shared extension points, then those are exactly the shapes the agent will keep producing. The problem starts long before the prompt. I want to make that concrete: which names keep inviting the wrong architecture, which names make ownership clearer, how I cut features so the decision logic stays local, how [Functional Core / Imperative Shell](https://ricofritzsche.me/simplify-succeed-replacing-layered-architectures-with-an-imperative-shell-and-functional-core/) gives the slice a usable internal shape, and how those choices have to be encoded again in skills, Codex instructions, and review rules. A ready-to-use SKILL.md template is linked at the end of the article for anyone who wants to apply this structure directly in a project. ### The Project Structure Instructs the Agent Before the Prompt Does In one codebase, the generated change extends an existing service, adds another data-access abstraction, and pushes more logic into a shared area. In another, the same task turns into a self-contained feature with local naming and an explicit execution flow. The difference is what the code repository already presents as normal. ![](https://cdn-images-1.medium.com/max/800/1*0Wjd3qk3K4l_N5ugzL8pGw.png) The same prompt produces different results depending on what the repository already presents as normal. A weak instruction leaves too much of the internal structure open. A stronger one closes the most damaging escape routes early: no services, no controllers, no repository pattern, no helpers, no managers, no shared fallback, no generic file names when behavior-specific names are possible. At that point the instruction stops being a feature request and starts becoming a structural constraint. A folder called *register\_client* does not solve much if the files inside still use generic technical names such as service, logic, manager, or repository. Those names already pull the result back toward technical categories. If the files are called *load\_registration\_context*, *decide\_registration*, *append\_registration*, and *registration\_state*, the feature starts to describe its own execution directly. A feature should not contain files with generic technical names. Names such as *service.rs*, *logic.rs*, *manager.rs*, *repository.rs*, *util.rs*, or *helper.rs* already pull the structure back toward technical categories instead of behavior. Inside a slice, file names should describe the actual work being done, especially at the boundary and in the decision flow. Names such as *load\_registration\_context.rs,* *decide\_registration.rs*, *append\_registration.rs*, *query\_parameters.rs*, or *error\_responses.rs* make the execution easier to understand directly from the feature itself. The code repository, the skill file, and the [Codex](https://openai.com/codex?utm%5Fsource=ricofritzsche.me) or [Claude Code](https://claude.com/product/claude-code?utm%5Fsource=ricofritzsche.me) instruction all need to point in the same direction. ### Functional Core / Imperative Shell Gives the Slice an Internal Form A feature-specific directory and meaningful file names still leave one practical question open: how should the feature be structured internally? I do not mean it here as a general architecture topic, but as a concrete way to shape a self-contained feature. ![](https://cdn-images-1.medium.com/max/800/1*GBIW8QLFgOcc25zZK1jfrQ.png) Functional Core / Imperative Shell inside a feature slice: the shell handles entry, loading, and external effects, while the core carries the pure decision. The shell handles the entry, the loading, and the external effect. The core carries the actual decision. That gives the slice a readable internal execution flow without falling back to services, managers, or other generic technical roles. For a coding agent, the same structural intent can be made explicit in instructions like these. ```markdown ### 3. Functional Core, Imperative Shell * Core = pure, deterministic decision logic * Shell = boundaries and effects only (HTTP, DB, external systems) * The shell may load data and persist results * The core must not perform IO Never mix both. ### 4. Explicit IO boundaries * IO is visible and isolated * No hidden IO inside core ``` In a feature like *register\_client*, names such as *load\_registration\_context*, *decide\_registration*, and *append\_registration* already reflect that shape directly. The loading step belongs to the shell. The decision belongs to the core. The write step belongs back to the shell. The structure inside the slice should describe how the feature runs, not which architectural category a file belongs to. ### Share Nothing That Carries Domain Meaning A self-contained feature is easier to generate, easier to change, and easier to replace because it does not depend on shared domain-specific structures outside of itself. The shell and the core stay inside the feature. The feature can use bootstrapped infrastructure such as a database pool, an HTTP client, a logger, or configuration, but the behavior that gives the feature its meaning does not live in a shared service, shared model, or shared helper somewhere else. A feature can be implemented with much less surrounding context when the code repository does not force the agent to understand a separate domain layer, a generic service layer, and a reusable data-access layer before it can make a local change. The feature owns the behavior it needs. The structure around it stays thin. Once data is treated as explicit input and explicit output, the pressure toward shared mutable structures drops with it. The feature does not need a central object that carries the current meaning of the system. It needs the data required for its own decision, and it needs a clear way to write its result back. That keeps the internal contract of the feature smaller and easier to generate correctly. A structure like this keeps the dependency surface narrow: ``` /register_client/ load_registration_context.rs decide_registration.rs append_registration.rs vs. /register_client/ handler.rs /domain/ client_service.rs /data/ client_repository.rs ``` The first shape keeps the feature implementable from its own slice. The second shape makes the feature depend on domain-specific structures that are shared across other behavior as well. A self-contained feature should be replaceable. That is one of the strongest practical tests. If a feature depends on shared domain-specific structures, replacing or regenerating it is immediately more expensive. The agent has to preserve conventions outside the feature, understand behavior that is only partially visible from the slice itself, and avoid changing abstractions that other parts of the system also depend on. The feature stops being a local unit and turns into a participant in shared ownership again. ### Why This Shape Works So Well with Agents It reduces how much surrounding context is needed before a change can be made safely. A human can jump across the codebase, keep partial knowledge in mind, compensate for weak boundaries, and gradually reconstruct how shared models, services, and abstractions interact. An agent has a much narrower working surface. The more behavior depends on centralized structures outside of the feature, the more context has to be pulled in before the change is even understandable. The shell makes the entry and the side effects visible. The core makes the actual decision visible. The feature can be implemented, reviewed, and changed from its own boundary without first understanding a domain model, a service layer, and a reusable abstraction stack somewhere else in the codebase. It keeps the cognitive load where the feature already is. The agent does not have to reconstruct the system around the feature before it can work on the feature itself. ### Project-Level Skills Teach the Agent How Features Are Built Here A skill does not belong inside a feature. Instead, it sits one level above and teaches the agent how features in this project should be built. The feature is the generated result. The skill defines the generation environment. ``` your-project/ .codex/ skills/ feature-slice-rust/ SKILL.md src/ features/ register_client/ evaluate_route_access/ ``` A prompt may ask for a self-contained feature, but the code repository may still contain shared abstractions, generic technical names, and existing extension points that keep pulling the result back into the old shape. A project-level skill counters that directly. It gives the agent a reusable instruction that applies across all features in the codebase, not just the one task in front of it. “*Implement client registration…*” is not enough. The skill has to say how a feature is shaped here. It has to forbid the generic fallback structures, require meaningful file names, keep the core pure, keep IO explicit, and reject cross-feature dependencies as a default move. This is what that can look like in practice for a coding agent. ``` --- name: feature-slice description: Create or modify a self-contained feature slice using domain-driven naming, strict isolation, and explicit IO boundaries. No shared logic, no cross-feature dependencies. --- ## Non-negotiable rules ### 1. Feature isolation * Everything lives in `features//` * No imports from other features * No shared folders (`common`, `shared`, `utils`, etc.) * No global abstractions ### 2. No OOP structures * No classes * No services, managers, repositories * No inheritance ``` It tells the agent that features in this codebase are not assembled from services and shared abstractions. They are built as self-contained slices with explicit boundaries and names that describe behavior directly. It stops being another place to restate the task and becomes part of the structure that keeps generated code from drifting back into the same old defaults. ### Cross-Feature Dependencies Must Stay Exceptional A feature stops being self-contained as soon as its behavior depends on the internal behavior of another feature. That is one of the easiest ways for ownership to leak back out of the slice. ![](https://cdn-images-1.medium.com/max/800/1*Lv4ID4OUYiM2qCd1TaRBfg.png) A feature may depend on shared infrastructure, but it should not depend on another feature’s internal behavior by default. This is where a strict default matters. The normal case should be no cross-feature dependency at all. A feature can use bootstrapped infrastructure such as a database pool, a logger, configuration, or an external client. It can also depend on stable external contracts where that is necessary. But it should not reach into another feature to reuse decision logic, internal loaders, write steps, or domain-specific helpers. The moment one feature starts depending on another feature’s internal behavior, a local change stops being local. Regenerating the slice, replacing it, or even reviewing it now requires understanding behavior that lives somewhere else. The folder still looks separate but the ownership already is not. Two features may both validate an address, classify a client, or compute a price-related decision. That does not automatically justify one shared abstraction between them. The question is stricter than that. Do they actually need to change together for the same reason? If not, the extraction creates coupling where the slice should have remained independent. Cross-feature dependencies are not a normal optimization. They are a structural exception and should be treated that way. For a coding agent, the structural intent can be made explicit in instructions like these. ``` ## Dependency rules - A feature must not import another feature's internal decision logic - A feature must not depend on another feature's internal loaders or write steps - Shared domain-specific helpers are not allowed as a default destination - Cross-feature dependencies require explicit justification - Shared infrastructure is allowed: - database pool - logger - configuration - external clients - Stable external contracts are allowed where needed ``` Without it, a project can have feature folders, meaningful file names, an imperative shell, and a pure core, and still slide back into shared ownership through imports that cross the slice boundary. The structure stays visible. The independence is gone. A useful practical test is this: can the feature still be changed, regenerated, or replaced from its own slice without pulling domain-specific behavior in from somewhere else? If the answer is no, the dependency has already gone too far. ### Conclusion Agentic coding does not reward codebases that leave structure implicit and hope the agent will infer the right shape. It rewards codebases that make feature boundaries, naming, dependencies, and execution form explicit from the start. That is why this is not only a prompting topic. It is an engineering topic. The more clearly the project encodes how features are supposed to be built, the less room remains for the old object-oriented defaults to reappear under a new name. The full project-level skills behind these examples can be found here: [https://github.com/ricofritzsche/agentic-feature-slice-templates](https://github.com/ricofritzsche/agentic-feature-slice-templates?utm%5Fsource=ricofritzsche.me) *Cheers*! This article was originally published on my Medium account here: [https://levelup.gitconnected.com/functional-core-imperative-shell-for-agentic-coding-45f04beb55f5](https://levelup.gitconnected.com/functional-core-imperative-shell-for-agentic-coding-45f04beb55f5?ref=ricofritzsche.me) ### Building a Road Network from OSM: Roads, Segments, and Directed Edges URL: https://ricofritzsche.me/building-a-road-network-from-osm-roads-segments-and-directed-edges/ Last updated: 2026-03-08T13:51:37.000Z The first shift happens earlier than most people think. Before [restrictions](https://ricofritzsche.me/building-a-routing-graph-from-osm-transition-constraints-and-path-constraints/), before costs, before routing algorithms, there is already a modeling step that changes the character of the data. In OSM, a road arrives as a way: an ordered list of node references with tags attached to it. That is a perfectly good source representation. It describes the road geometrically, and it can carry useful semantics such as road class, name, or one-way information. But a routing graph still cannot use that way as-is. The graph does not traverse *a road* in the loose map sense. It traverses directed edges. That difference matters because a way in OSM is still too broad. It can run through many intermediate points, bend several times, pass multiple junctions, and continue for quite a long distance as one object. The routing graph needs something smaller and sharper. It needs traversable units between consecutive graph nodes, and it needs them in a direction that can actually be followed. That is why building the graph means segmenting a way into edge-sized pieces and making direction explicit. In graph terms, that is standard road-network modeling: nodes represent junctions or relevant points, and edges represent the road segments between them. Once movement rules matter, those edges are directed, because the legality of travel can differ by direction. You can already see the raw material for that in a normal OSM way: ```xml ``` That way is meaningful as source data, but the graph still has to do the harder part. It has to turn the ordered node chain into traversable pieces. If the way runs from node A to node B to node C, the graph does not stop at *this is one street*. It derives one segment from A to B and another from B to C. And then it asks the more serious question: in which direction is each of those segments actually traversable? That is the point where the road network stops being a visual description and starts becoming a movement model. ## Why a Way Is Not Yet an Edge One of the easiest mistakes in this space is to look at an OSM way and assume it already corresponds to one edge in the routing graph. It does not. A way is a source object: an ordered chain of node references with tags. That is enough to describe a road in the map data, but not enough to define the traversable units the graph needs. The important difference is simple. A way can contain several consecutive node pairs, and those node pairs are exactly where the graph begins to resolve the source object into smaller movement units. In our model, the graph does not traverse the whole way as one indivisible thing. It derives edges from the consecutive node pairs inside that way, and only after that does direction become explicit. In a primal street network, intersections are modeled as nodes and street segments as edges. The source geometry is not yet the graph. The graph begins where the road is expressed in explicit edge-level units. ## From Ways to Edges An OSM way is already a street segment in source form. It is an ordered chain of nodes with tags. The important point is that it is still one source object even when it contains several intermediate nodes. The next step in the road network is therefore not to jump straight to directed movement. The next step is to resolve that way into edges between consecutive node pairs. A small diagram shows that step directly: ```text way: A - B - C - D edges: (A,B) (B,C) (C,D) ``` That is the first structural transformation. The way remains the source street segment. The graph turns it into smaller edge-level units. Only after that does direction become explicit and the graph derive the traversable movements from those edges. A second diagram makes the distinction clearer: ```text source way A ---- B ---- C ---- D graph edges E1 = (A,B) E2 = (B,C) E3 = (C,D) ``` This matters because the graph should not attach movement semantics to the whole way when those semantics apply at a finer level. Access, one-way rules, restrictions, and costs need graph objects that are local enough to carry them precisely. The way is too broad for that. The edges are the first level where the road becomes decomposed into explicit local connections the graph can actually work with. Street-network literature makes the broader point very clearly: in a primal graph representation, intersections are modeled as nodes and street segments as edges, and a usable topology requires separating true network nodes from intermediate geometry points that only shape the line of the street. That distinction is exactly what matters here. The source geometry is not yet the graph. The graph begins where the road is resolved into explicit edges between graph-relevant points. That is the chain the rest of the model depends on. The way is the source street segment. The edges are the local graph units derived from it. The directed edges come one step later and represent the actual traversable movements the network exposes. Once that distinction is clear, the rest of the model falls into place much more cleanly. ## From Edges to Directed Edges Edges are still not the final thing the graph routes over. They tell us that two graph points are connected. That is already an important step, but it still does not say whether movement is possible from left to right, from right to left, or in both directions. For routing, that distinction is not secondary. It is the structure of movement itself. A small diagram is enough to show the next step: ```text edge: (A,B) directed edges: A -> B B -> A ``` If the source semantics say that movement is only allowed in one direction, the result changes immediately: ```text edge: (A,B) directed edge: A -> B ``` That is the whole point. The edge expresses local connectivity. The directed edge expresses actual traversable movement. This matters because the graph is not supposed to answer whether two points are somehow adjacent in the source geometry. It has to answer whether a vehicle may move from one point to the next in a specific direction. A one-way street is the obvious case, but it is not the only one. The same local connection can later carry different movement semantics depending on direction. That is why direction cannot remain an interpretation outside the graph. It has to become part of the graph itself. A slightly larger example shows how this grows out of the segmented source structure: ```text way: A - B - C way: C - D edges: (A,B) (B,C) (C,D) directed edges: A -> B B -> A B -> C C -> B C -> D ``` The source road is not one undifferentiated object. OSM already separates it into ways, and those boundaries usually matter because semantics change there. The graph keeps that as part of the model. It derives edges from consecutive node pairs inside those ways, and then derives directed edges from those edges according to the movement semantics of that source segment. If the second way from C to D is one-way, then only C to D exists. The reverse movement does not. Once the graph is in that form, a lot of later design decisions stop looking arbitrary. [Transition constraints](https://ricofritzsche.me/building-a-routing-graph-from-osm-transition-constraints-and-path-constraints/) are defined over directed edges. Path constraints are ordered sequences of directed edges. A route itself is nothing else than a valid chain of directed movement through the graph. That is why I do not treat directed edges as a small technical refinement of edges. They are the level at which the road network becomes precise enough to talk about legal movement at all. ## Why a Bidirectional Road Still Becomes Two Directed Edges Once edges are in place, the next question comes naturally. If a road can be used in both directions, why not keep one edge and stop there. The answer is simple, but it matters. A bidirectional road still contains two different movements. One movement goes from A to B. The other goes from B to A. They share the same physical road and often the same geometry, but in the graph they are still different traversals. Here a simple example: ```text edge: (A,B) directed edges: A -> B B -> A ``` This is not duplication for the sake of form. It is the graph stating movements explicitly. The road between A and B is usable in both directions, so the graph contains both traversals. If the source semantics say it is one-way instead, then only one directed edge exists. That difference belongs in the graph itself, not in some later interpretation layer. This is also the point where the model becomes much easier to work with. An undirected edge can only say that two graph points are connected. A directed edge says something stronger: movement from this node to that node is possible. That is exactly the level later rules need. Access does not apply to an abstract connection in the middle. It applies to a directed movement. A turn restriction is not a property of a road in general. It is a restriction on moving from one directed edge into another. Even costs become cleaner at this level, because they belong to a movement through the graph, not to an undifferentiated line on the map. That is why a bidirectional road still becomes two directed edges. The graph is not trying to mirror the visual simplicity of the map. It is trying to represent movement precisely enough that later semantics can attach without ambiguity. Once both directions are explicit, the rest of the network has a stable structure to work on. ## What Directed Edges Make Possible The real value of directed edges appears one step later, when the graph has to carry actual road semantics instead of just connectivity. Without them, the network can still show that two points are linked. With them, it can express the exact movements the road system permits. That is the difference the rest of the model depends on. This is where the graph becomes usable for more than route calculation alone. A cost can belong to one concrete traversal. An access rule can deny one direction without affecting the other. A restriction can attach to the movement it actually governs instead of to a broader road object nearby. That makes the network much easier to inspect, explain, and expose through an API, because the structure already contains the same units a routing decision is built from. That is also why the step is worth making explicitly. Directed edges do not add conceptual overhead to the graph. They remove ambiguity from everything built on top of it. *Cheers*! **References** - [OpenStreetMap Wiki — Relation:restriction](https://wiki.openstreetmap.org/wiki/Relation:restriction?ref=ricofritzsche.me) - [Geoff Boeing — OSMnx: New Methods for Acquiring, Constructing, Analyzing, and Visualizing Complex Street Networks](https://arxiv.org/pdf/1611.01890?ref=ricofritzsche.me) - [Hannah Bast et al. — Route Planning in Transportation Networks](https://arxiv.org/pdf/1504.05140?ref=ricofritzsche.me) - [Robert Geisberger and Christian Vetter — Efficient Routing in Road Networks with Turn Costs](https://ae.iti.kit.edu/english/1536.php?ref=ricofritzsche.me) - [Philippe Bellitto and Cyril Gavoille — On Minimum Connecting Transition Sets in Graphs](https://arxiv.org/pdf/1807.08463?ref=ricofritzsche.me) ### Building a Routing Graph from OSM: Transition Constraints and Path Constraints URL: https://ricofritzsche.me/building-a-routing-graph-from-osm-transition-constraints-and-path-constraints/ Last updated: 2026-03-07T15:52:13.000Z Over the last years, I have built road networks based on [OpenStreetMap](https://www.openstreetmap.org/?ref=ricofritzsche.me) in different contexts and for different clients. That work taught me a lot. It also made one thing very clear to me: there is a big difference between consuming map data and actually defining the road network a machine should work with. What I am building now is my own Road Network API. Not a map viewer, not a routing frontend, and not another wrapper around an existing routing engine, but the road network itself as a product and as an API. That sounds simpler than it is. OpenStreetMap gives us nodes, ways, relations, tags, and geometry. It gives us a rich and detailed description of the world. But it does not hand us a finished road network in the form needed when the real question is not what is visible on a map, but what movement is actually possible. That difference becomes very concrete around restrictions. A road can exist visually and still not be reachable from a certain direction. A turn can be illegal at one junction. A whole maneuver can be forbidden even though each individual road segment looks valid on its own. This is exactly where the model has to become explicit. In this article, I want to focus on that point and explain why a serious road network needs two different kinds of movement rules: transition constraints and path constraints. ## From OSM Data to a Routing Graph Over the years I learned to treat OpenStreetMap with a bit more precision than I did at the beginning. It is tempting to look at a road, a junction, a restriction relation, and assume the routing graph is already there. It is not. OSM gives us great source data: nodes with coordinates, ways with ordered node references, relations with members and tags. That is a strong and flexible model for describing the world. But it is still a source model. It is not yet the structure a routing system can execute directly when the real question is no longer what is visible on the map, but what movement is actually allowed. You can see that immediately in the raw data itself. A street arrives as a way with node references and tags. A restriction arrives as a relation with members and roles. Even the common restriction shape already tells the story: from, via, to. And OSM does not stop at a via node. The via member can also be a way. That one detail is enough to show why the source representation cannot be the final routing representation. A simple turn at one node and a maneuver that runs through an intermediate connector are different things. OSM can describe both, but a routing graph has to resolve them into something explicit and operational. ```xml ``` That is why I do not treat OSM relations as the golden form for routing. They are the right source form for editing, exchange, and community-maintained map data. But a routing graph still has to do the harder part. It has to turn ways into directed edges. It has to make access and direction explicit. And it has to translate restriction relations into rules that are local to the graph and efficient to evaluate. [Geisberger and Vetter ](https://ae.iti.kit.edu/1862.php?ref=ricofritzsche.me)make the same point from the routing side when they move to an edge-based model to handle turn costs properly. Once turns and movement rules matter, the plain road graph is no longer enough. You need a representation that makes those transitions explicit. I think the important thing is the transformation from descriptive map data into a movement model that can actually be queried and used. That is the value of a road network. It makes the hidden structure of movement explicit. Roads become directed edges and restrictions become real rules in the graph. That changes everything for the systems built on top of it, because they no longer have to reinterpret raw OSM relations again and again. They can work against a road network that already answers the harder question: not what is mapped here, but what movement is possible here. This is also the point where the distinction in this article begins to matter. Some restrictions affect one immediate turn at one node. Others describe a maneuver that runs through an intermediate path. Treating both as if they were the same would flatten an important difference that the road network has to preserve. ## Restrictions Force a Different Graph Before considering restrictions, it's natural to think in terms of roads, intersections, and geometry. A street connects to another street, a vehicle moves from one segment to the next, and the whole thing still feels close to the visual map. Restrictions break that illusion. They force the model to care about movement itself, not just about connected lines. OSM makes that visible in a very direct way. A restriction relation is not attached to a road as a passive property. It defines a permitted or forbidden movement from one way, through a via element, into another way. In the common case, that via element is a node. But OSM also supports a via way, which already tells you that not every forbidden maneuver is just a turn at one junction. Some restrictions span an intermediate connector or slip road and only make sense as a sequence. That is exactly where a plain road graph becomes too flat. If the graph only knows that roads are connected, it still does not know whether a specific movement through that connection is legal. And once the via member can be a way, the problem gets even sharper. Now the legality of the movement depends on a short path through the network, not only on one local turn. This is why routing literature moves beyond the simple node-edge picture as soon as turn costs and turn restrictions matter. Geisberger and Vetter do not treat this as an edge case. They treat it as a structural requirement of realistic routing. The graph has to represent turns explicitly because the cost or legality of movement depends on more than the current road segment alone. That is also the point where I stopped looking at restriction relations as something the routing layer could just evaluate later. They are still the right source [representation in OSM](https://wiki.openstreetmap.org/wiki/Relation%3Arestriction?ref=ricofritzsche.me), but they are not yet the operational rule set of the road network. A routing system needs something sharper. It needs rules that are already resolved against the graph it works on. That means one kind of rule for immediate edge-to-edge movement at a node, and another kind of rule for a maneuver that runs through an ordered sequence of intermediate edges. Treating both as if they were the same would erase a distinction that exists in the source data itself and matters even more once the graph is built. For me, the consequence is clear: the road network cannot end at roads and intersections. It has to model admissible movement. And the moment it does that seriously, two different kinds of constraints appear naturally. One describes an immediate transition at a node. The other describes a maneuver across a path. That is the point where **transition constraints** and **path constraints** stop sounding like internal terminology and start looking like the only honest way to represent what the road network actually contains. ## Transition Constraints The simpler of the two cases is the one OSM users know best. A vehicle comes in on one road, reaches a junction, and is not allowed to continue into one specific outgoing road. That is the familiar shape behind restrictions like no left turn, no right turn, no U-turn, or only straight on. In OSM, this is usually represented as a relation with a from way, a via node, and a to way. The important part is not the XML shape itself. The important part is what it means once the road network has already been built: this is no longer just a tagged relation. It becomes a rule about one immediate movement from one directed edge to another through one node. That's why it is called a transition constraint. The word transition is doing real work here. The restriction does not live on the incoming edge alone. It does not live on the outgoing edge alone either. And it does not live on the node in isolation. It lives in the transition between two edges at that node. That is the real object the routing graph has to reason about. A road can be perfectly valid on its own, the next road can also be perfectly valid on its own, and still the movement from one into the other can be forbidden. Once you see that clearly, it becomes hard to accept looser models that treat restrictions as decorative metadata hanging somewhere near the junction. A transition constraint is represented in our road network as a rule over three concrete graph elements: an incoming directed edge, a via node, and an outgoing directed edge. In other words, the restriction is no longer attached vaguely to a junction or carried around as a relation that still needs interpretation later. It is resolved into the actual movement the graph must allow or deny. If edge E1 enters node N from the south and edge E2 leaves N to the west, then a no left turn becomes a rule that says: the transition from E1 through N into E2 is denied. If the source restriction is an only straight on, the shape is the same, but the meaning changes slightly. Then the graph stores that only one specific outgoing transition is allowed from that incoming edge at that node, and every other outgoing transition from the same incoming edge is excluded. A tiny example makes that more tangible: ``` E2 ← | | E3 ←-----N-----→ E4 | | ↑ E1 ``` If a vehicle arrives on E1 and the source data contains a no left turn restriction, then the graph stores a transition constraint like this: ``` from_edge = E1 via_node = N to_edge = E2 kind = deny restriction = no_left_turn ``` That is the design choice that matters here. We do not keep the restriction as a loose source object and hope the routing layer will reconstruct the meaning later. We resolve it once into the graph itself. The result is a road network that can answer the real question directly: may a vehicle move from this edge through this node into that edge, yes or no? There is also a nice conceptual fit here with the graph literature around forbidden transitions. That work describes exactly this kind of situation: whether a walk through a graph is valid depends on whether a consecutive pair of edges is permitted at a shared vertex. That is almost a direct formal description of a node-based turn restriction in a routing graph. It is a useful reminder because it shows that this is not some special OSM quirk. It is a more general graph problem that just becomes very concrete in [road networks](https://arxiv.org/pdf/1807.08463?ref=ricofritzsche.me). ## Path Constraints The second case is the one that makes the distinction unavoidable. A restriction is no longer about one immediate turn at one node. It is about a maneuver that runs through an intermediate connector, slip road, or short link before reaching its final continuation. OSM allows exactly that by letting the via member of a restriction relation be a way, not only a node. That means the source data itself already distinguishes between a local turn at a junction and a movement that spans a whole intermediate path. If the road network flattened both into the same kind of rule, it would throw away information that is explicitly present in the source. This is the reason why the road network needs a second kind of constraint. A path constraint is not attached to one node and it is not defined by one adjacent edge pair. It is defined by a sequence: an incoming edge, an ordered set of intermediate edges, and the outgoing edge that completes the maneuver. The crucial point is the order. A connector road is not just “somewhere in between.” It is part of the movement that is being restricted. The graph therefore has to preserve that sequence and evaluate it as a sequence, not as a loose collection of roads that happen to touch. This is also exactly the kind of problem that pushes routing beyond the simple node-edge picture. Once turn costs and turn restrictions matter, the legality of a route depends on more than the current edge alone. That is why turn-aware and edge-based representations appear so naturally in routing literature. In the road network we built, a path constraint is resolved into three parts: a from edge, an ordered via-edge sequence, and a to edge. That is the design decision that keeps the source semantics intact. We do not reduce a via-way restriction to a vague note saying that something is forbidden around here. We resolve it into the exact maneuver the graph must deny or allow. If a vehicle comes from E1, then follows E2 and E3 through a connector path, and finally reaches E4, the graph can state precisely that this whole maneuver is forbidden. We resolve it into the exact maneuver the graph must deny or allow, including the entry into the connector, the ordered path through it, and the final continuation. A small example makes that easier to see: ```text A ----E1---- B ----E2---- C ----E3---- D ----E4---- E ``` If the restriction is about the maneuver from E1 through the connector path E2 and E3 into E4, then the graph stores a path constraint like this: ```text from_edge = E1 via_edges = [E2, E3] to_edge = E4 kind = deny restriction = no_right_turn ``` The important thing here is that none of the intermediate edges is invalid on its own. E2 may be perfectly traversable. E3 may also be perfectly traversable. E4 may be perfectly fine when approached from somewhere else. What is restricted is the maneuver formed by that ordered sequence. That is exactly why a transition constraint is not enough in this case. A transition constraint can describe one immediate move from one edge through one node into another edge. It cannot carry the fact that the rule only becomes meaningful once the vehicle has already committed to a particular intermediate path. This is the point where the road network becomes much more honest than the visual map. On the map, all those segments just look connected. In the graph, the model can say something much sharper: yes, these roads exist; yes, they are traversable in general; but no, this specific maneuver through them is not allowed. That is the value of turning source relations into explicit path constraints. The graph stops guessing later and starts carrying the actual movement rule now. ## Why Both Are Needed At that point the obvious question comes up. If both cases exist, why not collapse them into one single kind of rule? Why not treat every restriction as a path, or force every maneuver back into local edge-to-edge transitions? The idea sounds tidy at first, but it stops fitting the moment you look closely at what the restriction is actually describing. A transition constraint works because it stays exactly where the rule lives. One incoming edge, one node, one outgoing edge. Nothing more. That is the natural shape of a local turn restriction. It keeps the graph precise and it keeps the rule understandable. The graph can say exactly what is forbidden or allowed at that point, without wrapping a simple turn in a heavier structure than it needs. A path constraint solves a different problem. It keeps the shape of a maneuver that only makes sense across an ordered intermediate path. That is not the same thing as one turn at one node. If I tried to break that kind of restriction into a series of local transitions, I would spread one movement rule across several smaller rules and lose the fact that they belong together as one maneuver. The graph would still contain fragments of the logic, but not the rule in the form it actually exists. The other direction is not better. A no-left-turn at a junction is not a path just because it can be written as a very short sequence. Modeling it that way would make the graph more uniform on the surface, but less exact in meaning. It would blur the difference between a local transition and a maneuver across a connector, even though OSM itself already distinguishes between those two cases. That is why both are needed. The road network has to preserve the actual shape of the movement rule it represents. Some restrictions describe one immediate transition at one node. Others describe a maneuver through an intermediate path. If the graph keeps that difference intact, it becomes much easier to understand, much easier to inspect, and much closer to the real movement rules hidden inside the source data. *Cheers*! ### Road Networks Are Execution Models, Not Stored Geometry URL: https://ricofritzsche.me/road-networks-are-execution-models-not-stored-geometry/ Last updated: 2026-02-16T11:53:15.000Z When working with geographic systems over time, a particular habit becomes visible. Road networks are imported, normalized, indexed, and eventually discussed in terms of storage and retrieval. The conversation centers around schemas, spatial indices, memory layout, and graph representations. The network becomes something that is stored and accessed. This way of thinking is natural. Many traditional software systems revolve around data structures. We persist them, transform them, query them, and optimize them. Once a road network has been transformed into nodes and edges, it appears to fit comfortably into that familiar category. The difficulty begins when the network is no longer observed, but executed. ## From Geometry to Movement Geometry alone does not define a road network in practice. Coordinates and connectivity describe space, but they do not yet describe movement. The moment a system must determine whether a vehicle may traverse an edge, whether a restriction applies at a specific transition, or whether a segment behaves differently depending on context, geometry becomes only one part of the equation. A road network encodes constraints, permissions, hierarchies, and contextual rules. These rules are not decorative annotations attached to edges. They shape how movement is allowed to occur. They define which paths are legal, which are optimal, and which are prohibited. This is where the concept of a dataset becomes insufficient. A dataset suggests passivity. It suggests something that can be inspected and queried. A road network used for execution behaves differently. It participates in decision-making. To make this distinction tangible, consider a small fragment of OpenStreetMap data in its editorial form: ``` # Way id: 12345 tags: highway: residential motor_vehicle: delivery nodes: - 100 - 101 - 102 # Node id: 101 tags: barrier: bollard ``` In this representation, geometry and semantics coexist, but they are not yet resolved into executable meaning. The tags describe intent. They do not yet define a decision. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2026/02/From-Editorial-Structure-to-Deterministic-Execution-2.png) The projection layer transforms editorial OSM into an execution artifact The editorial structure expresses meaning in a flexible way. The projection layer makes that meaning explicit and operational. Only in the execution artifact does direction, access state, and constraint become structurally defined. ``` # Directed Edge 1 from: 100 to: 101 access_state: delivery_only barrier: false # Directed Edge 2 from: 101 to: 102 access_state: prohibited barrier: bollard ``` The representation of `delivery_only` in a production system is rarely a string comparison at runtime. In a deterministic projection layer, it might instead be encoded as a compact bitmask or access profile identifier resolved during import: ```Rust #[derive(Clone, Copy)] pub struct Edge { pub from: u64, pub to: u64, pub access_mask: u8, pub barrier: bool, } const PUBLIC: u8 = 0b0001; const DELIVERY: u8 = 0b0010; const EMERGENCY: u8 = 0b0100; let edge_1 = Edge { from: 100, to: 101, access_mask: DELIVERY, barrier: false, }; let edge_2 = Edge { from: 101, to: 102, access_mask: 0, barrier: true, }; ``` Conditional rules are flattened during projection into time-bounded state transitions or indexed constraint tables: ```Rust // time_window pulled from OSM conditional tags/opening_hours during projection fn effective_access(edge: &Edge, current_hour: u8) -> AccessMask { match edge.time_window { Some(window) if window.contains(¤t_hour) => edge.access_mask, Some(_) => AccessMask::NONE, None => edge.access_mask, } } ``` The crucial difference is not the syntax, but the timing. The decision structure is computed once during projection, not reconstructed repeatedly during traversal. What was previously descriptive has now become executable. The system no longer interprets raw tags at runtime. It traverses a model whose semantics have already been resolved. ## The Emergence of Runtime Interpretation If the network is treated purely as stored information, decision logic tends to migrate into runtime evaluation. Access restrictions are interpreted directly from tags and conditional rules are reconstructed when needed. Restrictions encoded in editorial form are translated repeatedly during execution. These systems often work well at first because they compute routes and estimate travel times. Under moderate load, they appear stable. But over time, interpretation costs add up. Reasoning about edge behavior becomes tangled with reconstructing semantics. What was once a clean separation between structure and logic begins to blur. The network is no longer an explicit execution model. It is an interpreted artifact. This shift does not usually produce dramatic failures. Instead, it produces gradual complexity. Edge cases become harder to explain. Tail latency becomes sensitive to contextual branching. Architectural clarity erodes quietly. Snap behavior illustrates the same structural distinction. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2026/02/Spatial-Search-Candidate-2026-02-16-103647.png) Classic unbounded snap — the source of tail latency in dense areas In dense areas, the number of candidate edges can vary widely. Evaluating access semantics only after collecting candidates introduces variability into the hot path. A projected model changes the structure: ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2026/02/Input-Coordinate-Processing-2026-02-16-103742.png) Deterministic snap pipeline (projection layer pre-filters and precomputes access) Here, constraints are structural. Candidate expansion is bounded. Access decisions are no longer interpretive but encoded. In one production system where this shift from interpretation to projection was applied, the change did not alter the graph size or the database engine. It altered only where semantics were resolved. The 99.9th percentile routing latency dropped from 180 ms to 11 ms under identical traffic, and tail variance flattened almost completely. The topology did not change. The projection layer did. ## Read-Only Artifacts and the Question of Static Behavior It is important to distinguish between treating a network as a dataset and deliberately creating a read-only execution artifact. High-performance systems often materialize projected graphs, precompute access states, and transform editorial structures into deterministic edge representations. These artifacts are static in the operational sense: they do not change during query execution. They are optimized for predictability. The static artifact is a response to the dynamic nature of movement constraints. It stabilizes semantics into something executable. What is static is the representation, not the conceptual model it encodes. Confusion arises when these two levels are conflated. A read-only projection is an architectural decision that clarifies semantics. Treating the original network as a passive dataset often postpones that clarification. ## Designing the Network as an Execution Model The decisive step is not the import of geometry, but the projection of semantics. A road network becomes operationally robust when its constraints are modeled explicitly. Access rules are resolved into decision-ready states. Turn restrictions are translated into directed transitions and snap logic operates within defined structural bounds rather than open-ended interpretation. A complex OSM turn restriction, such as a 'no left turn' relation involving multiple ways, is not stored as a relation lookup during execution. In a projected execution model, it becomes an explicit transition constraint: ``` transition(from_edge_id, to_edge_id) = DISALLOWED ``` The result: traversal becomes a table lookup, not a relation interpretation. Execution becomes traversal of a well-defined model rather than repeated reconstruction of meaning. The difference may not be visible initially. It becomes visible over time in how easily behavior can be reasoned about, in performance stability, and in architectural clarity. A road network for execution is deliberately shaped into a structure that expresses and stabilizes movement semantics. Recognizing this distinction changes how the system is designed from the beginning. ## Conclusion The question is not whether road networks can be stored efficiently. They can. The question is whether they are treated as passive data or as executable structures. When semantics remain implicit, systems compensate at runtime. When semantics are projected deliberately, execution becomes predictable. The difference does not lie in database technology or hardware capacity. It lies in whether the network is understood as geometry to be queried or as a decision space to be made operational. That distinction, once recognized, forces a decision. Either the network remains a descriptive artifact and interpretation becomes a permanent runtime cost, or semantics are projected deliberately and execution becomes structurally stable. The architecture will reflect that choice, whether you make it consciously or it makes itself at 3 a.m. when the 99.9th percentile starts screaming. *Cheers*! ### OpenStreetMap Is Not a Routing Model And Treating It Like One Is Expensive URL: https://ricofritzsche.me/openstreetmap-is-not-a-routing-model-and-treating-it-like-one-is-expensive/ Last updated: 2026-02-14T14:08:19.000Z When routing systems based on [OpenStreetMap](https://www.openstreetmap.org/?ref=ricofritzsche.me) start to show cracks, the first reflex is usually to look at scaling, indexing, or infrastructure. Latency creeps up, tail behavior becomes unpredictable, cloud cost rises, and the instinctive reaction is to add capacity or tune queries. But over the years I have found that the deeper issue usually sits somewhere else. It sits in an assumption that is rarely questioned: that importing OSM correctly means you now have a routing model. That assumption is not a small technical shortcut. It defines the architecture that follows. And once the architecture is shaped around raw OSM semantics, correcting it later becomes disproportionately expensive. OpenStreetMap is an extraordinary system. It allows a distributed community to describe the world with remarkable flexibility. Nodes, ways, relations, and tags form a living, evolving representation of geography. That flexibility is its strength because it allows nuance, correction and growth. Routing engines, however, operate under entirely different constraints. A routing core does not describe the world. It decides within it. It needs deterministic edge semantics. It needs unambiguous directionality. It needs a stable execution graph that can answer the same question twice and produce the same result, even under concurrency and load. It needs decision logic that does not have to reinterpret metadata on every request. OSM was never designed for that role. And when the two models are treated as interchangeable, subtle complexity leaks into the execution layer. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2026/02/From-Editorial-Structure-to-Deterministic-Execution-1.png) From Editorial Structure to Deterministic Execution ## Access Legality Is Not a Property Access legality often begins its life as tag evaluation. A tag says `motor_vehicle = no`, and so the edge is marked prohibited. That works until contextual reality enters the system. None of the following are exotic: conditional access, vehicle classes, delivery exceptions, barrier overrides and time-dependent rules. They are standard. If the routing engine evaluates these directly from raw OSM tags at request time, it is effectively reconstructing a decision model for every query. The more context matters, the more branching logic expands. What looks simple at import time becomes interpretive at execution time. Interpretation is expensive. Not only computationally, but conceptually. When access legality lives in dynamic tag evaluation instead of in a projected decision structure, reasoning about behavior becomes harder. Edge cases accumulate. Engineers start compensating with patches rather than clarifying the model. Access legality is not a static attribute attached to a way. It is the result of a structured decision context. If that structure is not materialized ahead of time, it reappears as runtime complexity. ## Turn Restrictions and Semantic Drift Turn restrictions illustrate the same pattern. In OSM, they are relations referencing ways and nodes. This is perfectly natural in an editorial system. But a routing engine operates on directed edges. It splits ways, normalizes directions, and builds a traversal graph. If the transformation from OSM relations to execution edges is not explicit and deterministic, semantic drift begins. Restrictions are applied inconsistently. Rare routing anomalies surface. Debugging requires digging back into relation structures that were never meant to represent execution semantics directly. The system still “works" and most paths are correct. But correctness becomes probabilistic at the margins, and those margins are exactly where production incidents tend to live. The problem is not incorrect data. It is the absence of a clear projection from editorial structure to execution model. ## Where Performance Actually Breaks Discussions about performance often focus on database tuning and computing capacity. These are important, but they rarely address the underlying cause of latency growth. I've seen this happen countless times. ### Snap-to-network behavior In many implementations, snapping begins with a spatial radius search, followed by exact distance calculations for all candidates within that radius. The candidate set is then filtered based on access logic that is evaluated dynamically. Under moderate traffic, this is acceptable. Under sustained load, it becomes unstable. The instability does not originate in the spatial index. It originates in the absence of structural bounds. When the candidate search is unconstrained, the system allows pathological expansion. When access legality is resolved dynamically, every candidate carries interpretive overhead. Tail latency stretches not because the graph is large, but because the execution path is ambiguous. ### Contrast to a projected model If the snap strategy enforces a strict bounding-box prefilter that aligns with index selectivity, and if the number of candidates is capped deterministically before expensive distance computation, the behavior changes entirely. If access legality is precomputed into a decision-ready representation rather than reconstructed from tags, candidate filtering becomes structural rather than interpretive. Suddenly, p95 stabilizes. Tail latency shrinks. Not because hardware improved, but because ambiguity was removed from the hot path. The difference between triple-digit milliseconds and single-digit behavior is often not a matter of optimization. It is a matter of model clarity. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2026/02/Unbounded-Candidate-Expansion-vs-Structural-Constraint-3.png) Unbounded vs Deterministic Snap Strategy ## Flexibility Versus Determinism OSM thrives on flexibility. Its model embraces evolving tags, community conventions, and contextual nuance. That is precisely why it is valuable. Routing engines, particularly in scaling SaaS environments, cannot afford that flexibility at execution time. They require determinism. They require projection. They require separation between geographic input and decision logic. When OSM is treated as the routing model rather than as the source of geographic truth, architectural responsibility is effectively delegated to editorial metadata. At small scale this remains invisible but at scale, it becomes measurable in latency variance, in cloud cost, and in increasingly fragile execution paths. The misalignment is not dramatic. It is gradual. And that is what makes it dangerous. ## Designing the Execution Model The alternative is not rejecting OSM. It is treating it with discipline. OSM remains the input layer. A projection layer transforms it into a deterministic graph tailored for execution. Access legality becomes a modeled decision system. Turn restrictions are translated explicitly into edge semantics. Snap strategies operate within bounded, predictable constraints. Once that separation exists, routing behavior becomes testable and explainable. Latency correlates with traffic instead of complexity. Cost reflects usage instead of structural ambiguity. That is the difference between importing data and designing a routing core. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2026/02/Separating-Geographic-Input-from-Executi-2026-02-14-125416-1.png) Separation of Geographic Input and Routing Execution ## Conclusion The real question is not whether OSM is sufficient for routing. It is! The question is whether the architectural responsibility for execution semantics has been taken seriously. When OSM is treated as the routing model, the system implicitly delegates decision-making to editorial metadata. That delegation is convenient at first. It allows rapid progress. It avoids an explicit projection layer. It keeps the graph close to its source. But convenience at the modeling stage becomes cost at the execution stage. Latency variance increases not because traffic grows, but because interpretation lives on the hot path. Edge-case inconsistencies emerge not because data is wrong, but because semantics were never made explicit. Infrastructure budgets expand not because the workload demands it, but because ambiguity demands compensation. These effects are gradual. They rarely trigger a single catastrophic failure. Instead, they accumulate. Systems become harder to reason about. Performance becomes less predictable. Complexity moves from design time into runtime. A routing core is not a dataset. It is an execution model. That execution model must be designed deliberately: projected from OSM, not derived implicitly from it. Access legality must be modeled, not interpreted. Turn restrictions must be translated, not mirrored. Snap strategies must be bounded structurally, not tuned reactively. Once that discipline is in place, the system behaves differently. Performance stabilizes because decision paths are deterministic. Correctness improves because semantics are explicit. Cost aligns with traffic rather than with structural ambiguity. OpenStreetMap remains an extraordinary geographic source but importing it is not the same as designing a routing engine. And the difference is architectural, not incremental. If your platform depends on OSM-based routing and latency or cost keeps drifting in the wrong direction, the root cause is rarely infrastructure alone. In many cases, it is structural. That is exactly what I analyze in a structured Geo-API Performance & Model Audit. *Cheers*! ## ### When Reuse Breaks Decoupling URL: https://ricofritzsche.me/when-reuse-breaks-decoupling/ Last updated: 2026-01-19T08:58:04.000Z There is a very specific moment in almost every system where things start to feel *slightly* off. Nothing is broken. Tests are green. The architecture still looks clean. But you notice that implementing a new feature suddenly requires touching two or three existing ones. Not because the behavior truly changed, but because the structure demands it. That moment is subtle. It rarely comes with a dramatic failure. It usually comes with a small improvement, a tiny refactoring, or a bit of reuse. This article is about seemingly harmless features, such as *rotating an API key*. And how the instinct to reuse existing features quietly undermined everything that self-contained feature slices, Functional Core/ Imperative Shell, and event sourcing are supposed to give us. More importantly, it’s about how easy it is to fall into the reuse trap, even when you *know better*. ## The innocent idea: rotate means create plus revoke Rotating an API key sounds trivial. You create a new key. You revoke the old one. End of story. If you already have a *create\_key* feature and a *revoke\_key* feature, the natural thought follows immediately: *Why not just orchestrate them?* Call *create\_key*, then call *revoke\_key*, wire them together, and you’re done. No duplication. Clean reuse. DRY. Responsible engineering. That instinct is deeply ingrained in most of us. We’ve been trained for years to see reuse as a virtue and duplication as a failure. So the move feels almost automatic. And this is exactly where things start to go wrong. ## The first smell: orchestration that “just coordinates” At first, the orchestration looks innocent. A *rotate\_key* feature that does little more than glue two existing operations together. It doesn’t make decisions, it doesn’t add rules, it just coordinates. Or so it seems. But then you need to check whether the old key exists. So you reach into the revoke feature to load some context. Then you need to pass scopes, kinds, roles, client IDs. So you start sharing command types or helper functions. Then you want consistent error handling. So you import rejection types. Before you notice it, *rotate\_key* imports half of *create\_key* and half of *revoke\_key*. And suddenly changes in those features ripple into rotate\_key. Tests that should be independent start failing together. Refactoring one feature requires understanding another. At that moment, something fundamental shifted. ## The real mistake: treating actions as reusable units The core mistake here is subtle, but decisive. We treated **actions** as reusable units. Create a key. Revoke a key. Those look like generic actions, so we assume they can be composed freely. But in a system built with a self-contained feature slice mindset., features don’t exist to perform actions. They exist to **own invariants**. That distinction matters more than any abstraction or pattern. ## Invariants define features, not steps Let’s look at what the features actually *mean*. *create\_key* owns an invariant. Something like: *a key can be created if plan limits allow it, scopes are valid, and the tenant state permits it*. It decides whether creation is allowed and records the fact if it is. *revoke\_key* owns a different invariant: *a key can be revoked idempotently, safely, and permanently*. Now look at *rotate\_key*. Its invariant is not “create and revoke”. Its invariant is: *after rotation, there is exactly one active key replacing the previous one*. That’s a different rule. A different guarantee. A different meaning. Once you see that, it becomes obvious that *rotate\_key* is not an orchestration of two features. It is a **feature in its own right**. Trying to express it as reuse was the mistake. ## Orchestration is where meaning starts to leak The moment a feature starts orchestrating other features, it has to understand their meaning. It has to know what “exists” means for revoke. What “created” means for create. What failures matter and which ones don’t. That means orchestration is not neutral. It is *semantic glue*, and semantic glue is just another domain layer, whether you call it that or not. ![](https://miro.medium.com/v2/resize:fit:764/1*kzrhBtNZNOIPHJ0E2iROAA.png) This is exactly how shared domain layer or service sneaks back into systems that were supposed to avoid it. Not through big frameworks or repositories, but through “helpful” orchestration layers that know a little too much. ## Feature APIs and why they still didn’t feel right One possible reaction is to introduce a feature registry or API. Let features expose executable commands, and let orchestrators call those without importing internals. This might sound like a clean solution: no direct imports, clear boundaries, and explicit wiring. But if you’ve ever gone down that path, you know the feeling: something still isn’t right. ![](https://miro.medium.com/v2/resize:fit:764/1*YbUUFChSPZPu0kQOu8JWPQ.png) You end up with centralized command definitions. Shared result types. A growing registry that slowly turns into a service locator. And most importantly, you’re still orchestrating semantics across features. You avoided technical coupling, but you didn’t avoid **conceptualcoupling**. That’s why the discomfort remained. ## The moment of clarity: stop orchestrating, start owning The real fix wasn’t to improve the orchestration. It was to remove it. *rotate\_key* should not coordinate *create\_key* and *revoke\_key*. It should own its own decision. That means it loads its own context. It checks plan constraints itself. It decides how a new key replaces an old one. It appends its own events. ![](https://miro.medium.com/v2/resize:fit:764/1*1IFmODG_HthkFGqryTt30g.png) Yes, some mechanics are duplicated. And yes, some checks look similar. And that’s fine. Because duplication of *mechanics* is cheap. Coupling of *meaning* is expensive. ## Why duplication is not your enemy We need to be honest about what duplication actually costs. Duplicating a few lines of validation logic costs almost nothing. Duplicating context loading costs a bit of code. Duplicating decision rules that are conceptually different costs clarity, not correctness. But coupling features through orchestration costs you: - local reasoning - independent refactoring - safe evolution - test isolation In long-lived systems, that cost compounds fast. ## Event sourcing without shared entities makes this even clearer In an [aggregateless event-sourced](https://ricofritzsche.me/aggregateless-event-sourcing/) system, features append facts. They don’t [mutate shared objects](https://ricofritzsche.me/beyond-aggregates-correlation-shared-state/). They don’t coordinate through in-memory state. That means the natural unit of decomposition is the **decision that emits facts**, not the sequence of steps that happen to look similar. When *rotate\_key* appends events that represent “old key replaced by new key”, it doesn’t matter that *create\_key* and *revoke\_key* append similar facts. The meaning is different. The causality is different. Trying to reuse those features was reversing causality: structure first, behavior second. ## Audit logging reveals the same trap from the other side The same thinking applies inreverse with something like audit logging. If an audit feature imports *create\_key*, *suspend\_key*, or *rate\_limit*, it’s already wrong. Audit doesn’t *do* anything. It *explains* what happened. Audit should project facts, not recompute logic. The moment it imports feature logic, it stops being a projection. Again, the fix is not better reuse. It’s **stricter** **ownership** of meaning. ## Why this matters beyond one feature After stepping into the reuse trap and stepping back out, one rule became painfully clear: **If a workflow has its own invariant, it deserves its own feature slice.** It’s not an orchestrator, coordinator, or glue layer but it’s a feature in its own right. Once you adopt that rule, a lot of architectural decisions suddenly become easy. ![](https://miro.medium.com/v2/resize:fit:764/1*3wJ5Kb83IicN6mMA-gPjTw.png) This isn’t really about rotating API keys. It’s about resisting the constant pressure to centralize meaning. Every time you feel the urge to “just reuse” another feature, ask yourself: - Am Ireusing mechanics, or am I sharing meaning? - Does this workflow have an invariant of its own? - Wouldremoving this feature later require touching others? If the answer to the last question is yes, you’re already coupled. Feature slicing don’t fail because people don’t know the patterns. They fail because people stop trusting duplication and start trusting structure. 1. They fail when we optimize for elegance instead of independence. 2. They fail when we treat reuse as a goal instead of a consequence. ## Closing thought I briefly fell into the reuse trap myself. That’s an important part of this story. Not because it’s embarrassing, but because it’s honest. If you’re building systems that are meant to evolve for years, you will constantly be tempted to orchestrate instead of owning. To abstract instead of deciding. To reuse instead of duplicating. The discipline is not in knowing the right patterns. The discipline is in knowing when **not** to apply them. And sometimes, the most decoupled architecture is the one that dares to repeat itself. *Cheers*! *This article was published originally* [*here*](https://levelup.gitconnected.com/when-reuse-breaks-decoupling-73b2b0c1b5e6?ref=ricofritzsche.me)*.* ### Why Simple Changes Rarely Stay Simple URL: https://ricofritzsche.me/why-simple-changes-rarely-stay-simple/ Last updated: 2025-12-21T08:51:36.000Z One of the most common experiences in software development is also one of the least questioned: a seemingly small, well-understood change that ends up taking weeks or even months to deliver. The requirements are clear and the domain is familiar. Yet, despite the fact that no new technology is involved, the effort required is growing disproportionately. What initially looks like a minor adjustment suddenly touches multiple services, multiple teams, multiple approval steps, and multiple release cycles. Over time, this stops being surprising and becomes accepted as “*how things are*”. What makes this phenomenon interesting is not that it happens, but that it happens so consistently, even in organizations with competent engineers, modern technology stacks, and well-established processes. If the problem were merely a lack of skill or outdated tooling, the pattern would be less universal. Instead, it appears across companies, domains, and architectures, regardless of whether they follow monoliths, microservices, or something in between. That consistency suggests that the root cause lies elsewhere. ## The Explanations We Reach for First The explanations typically offered are familiar. - The domain is complex. - The system has grown organically. - There is legacy code that cannot be touched easily. - Regulatory constraints require extra care. - External dependencies slow things down. None of these explanations are wrong, but they rarely explain why even small changes behave the same way as large ones. They describe the environment, not the mechanics that turn simplicity into friction. This distinction becomes important when you start viewing systems not as static structures, but as the result of decisions made over time. What stands out in many of these situations is that the people involved are rarely the problem. Teams understand the domain, developers are experienced, and architects are capable of articulating the system’s design. CI pipelines run reliably, observability is in place, and documentation exists. From the outside, the setup looks mature. Yet delivery slows down, estimates become defensive, and predictability erodes. When competent people consistently produce disappointing outcomes, it is usually a sign that the system they operate in behaves in ways that no individual can easily override. This is a theme that shows up whenever responsibility and execution drift apart. Software systems do not resist change by accident. Over time, they develop a form of inertia that is the result of countless decisions made under local constraints. Each decision may have been reasonable in isolation, but together they create a structure that increasingly resists modification. The important part is that this resistance is not abstract or cultural; it is mechanical. It is embedded in how code is structured, how responsibilities are divided, and how decisions flow through the organization, often long after their original context has disappeared. ## When One Decision Exists in Many Places A recurring pattern in such systems is the fragmentation of domain logic. A single business rule, conceptually simple and well understood, ends up being implemented in multiple places. Sometimes this is done intentionally in the name of separation of concerns or reuse. Sometimes it happens gradually as systems evolve. The effect is always the same: a single conceptual change now requires coordinated updates across several technical boundaries. Each boundary introduces its own constraints, owners, and release rhythms. What was once a straightforward decision becomes a negotiation across contexts, and the system starts to behave less like a cohesive whole and more like a collection of loosely aligned interpretations of the same idea. Closely related to this is the issue of responsibility. Ownership is often clearly defined on paper. Services have owners, repositories have maintainers, and teams have scopes. In practice, decision authority is frequently distributed differently. One team may own the code, another the domain logic, and yet another the release process. This separation is often introduced to reduce risk or increase control, but it has the side effect of making changes harder to execute. Shared responsibility and ambiguous authority are two sides of the same coin. This pattern is reminiscent of what happens when systems prioritize safety over change. ## Abstraction as a Way to Avoid Commitment Abstraction plays a central role in how complexity stabilizes itself. Abstractions are usually introduced with the promise of flexibility and safety. They aim to decouple concerns, enable reuse, or prepare for future variation. Over time, many abstractions stop serving change and start serving reassurance. They become a way to postpone commitment, to keep options open that will never be exercised, or to protect teams from making explicit decisions. As behavior accumulates, these abstractions become harder to remove, and every change must pass through them. What was intended to simplify ends up preserving complexity. This is a phenomenon that often looks like architectural rigor while functionally acting as a brake. Distribution is another area where intention and outcome often diverge. Many modern systems are split into services with clearly defined interfaces, suggesting a high degree of independence. In practice, these services frequently move together. Shared data models, synchronous call chains, and coordinated releases create implicit coupling that is not visible in architecture diagrams. The system appears modular, but behaves as a single, tightly bound unit when changes are introduced. Distribution without true autonomy does not reduce complexity; it merely relocates it, often making causal relationships harder to see. As structural friction increases, organizations often respond by adding tooling. More pipelines, more checks, more dashboards, and more coordination mechanisms are introduced to regain control. These tools are not inherently problematic; many of them are necessary in complex environments. However, they often act as compensatory mechanisms rather than solutions. They help manage the symptoms of structural issues without addressing their causes. The system becomes more observable and more controlled, but not necessarily easier to change. This is a distinction that matters if effectiveness, not compliance, is the goal. At a certain point, complexity becomes self-reinforcing. Every change feels risky, which justifies additional safeguards. These safeguards add structure, which increases friction, making future changes even riskier. Over time, the organization adapts its behavior to this reality. Estimates grow, timelines stretch, and innovation slows. This adaptation is often seen as a sign of maturity, but it is equally often a response to structural resistance and a state of equilibrium that favors stability over change. Architecture discussions tend to intensify in such environments. New diagrams are created, target states are defined, and principles are refined. While these activities can be valuable, they often remain descriptive rather than diagnostic. They explain how the system is structured, but not why it behaves the way it does. Without examining the mechanics of change, architecture remains a reflection of existing decisions rather than a challenge to them, and structure becomes something to be defended rather than questioned. ## Looking at Change Instead of Structure What is frequently missing is a simple but uncomfortable perspective: observing what actually happens when a change is made. Which files change together? Which services are involved? Which teams need to align? Which approvals are triggered? These questions are concrete and observable. They do not require theory or frameworks. They also tend to reveal patterns that are difficult to ignore once seen, because they expose how decisions are enforced in practice rather than how they are described. Tracing real changes through a system makes priorities visible. It shows what is protected, what is duplicated, and what is centralized. These patterns are rarely the result of a single design decision. They emerge incrementally as the system evolves. Each step makes sense locally, but together they create a structure that resists modification. Over time, explanations replace decisions. The organization becomes adept at explaining why things are the way they are, but less willing to change them. This represents a shift from action to justification. Large initiatives can hide these issues. Small changes cannot. They move through existing structures and expose every friction point along the way. This is why small changes are such powerful diagnostics. They reveal the true cost of the system’s structure, not in theory, but in practice, and they do so without the noise that often surrounds large transformation efforts. None of this is about blaming individuals or teams. These dynamics arise in well-intentioned organizations with capable people. They are the result of accumulated decisions that were never revisited once their original context disappeared. Once embedded, they operate independently of intent, shaping outcomes even when everyone involved wants improvement. If you strip away the narratives and just look at what remains, the picture becomes clearer. How many places need to change? How many parties need to agree? How much coordination is required? These factors define the real cost of change, and they are measurable. Over time, organizations that cannot change easily do not stop delivering altogether. They slow down, become selective, and defer decisions. They lose flexibility, not because they lack ideas, but because implementing them becomes costly. ## What Simple Changes Reveal At some point, a different question becomes unavoidable. Not how to make changes easier, but what structure makes them so hard in the first place. That question rarely has a comfortable answer, because it points back to decisions that once felt necessary and now feel immutable. If simple changes rarely stay simple, it is not because software is inherently complex. It is because structure accumulates faster than intent, and because decisions, once distributed and protected, are rarely revisited. Seeing complexity not as a technical challenge but as a structural consequence changes how problems are framed. It shifts the focus from finding more solutions to understanding the constraints that quietly shape every outcome, and this shift is often where real change begins. *Cheers*! This article was originally published [here](https://levelup.gitconnected.com/why-simple-changes-rarely-stay-simple-c412d47b45ba?ref=ricofritzsche.me). ### Building a Durable Telemetry Ingestion Pipeline with Rust and NATS JetStream URL: https://ricofritzsche.me/building-a-durable-telemetry-ingestion-pipeline-with-rust-and-nats-jetstream/ Last updated: 2025-10-26T09:04:27.000Z Ingestion pipelines are often simple to begin with: a device sends a location and an API writes to a database. This approach works until it doesn’t. When you’re ingesting thousands of GPS updates per second from trackers across multiple tenants, that direct-to-database approach becomes your bottleneck and your single point of failure. The symptoms are always the same: timeouts spike during traffic bursts, the database chokes on concurrent writes, and you start losing data when things get busy. Then someone suggests adding caching, or connection pooling, or sharding. You’re treating symptoms while the fundamental design is broken. The real problem is coupling. When your ingestion API writes directly to Postgres, every device update has to wait for a transaction to complete. Your API response time is now tied to database performance. If the DB hiccups, your ingestion fails. If you need to run a heavy query, it impacts ingest throughput. You’ve made your fast data intake depend on your slow data storage. ## Why We Decoupled Ingestion from Storage We designed a separate ingestion pipeline specifically to break that coupling. Devices send telemetry to a stateless Rust service that does one thing well: validate the data and get it into a durable queue. That’s it. No database writes, no complex processing, just capture and queue. The service authenticates requests, validates the payload, and publishes events to [NATS JetStream](https://docs.nats.io/nats-concepts/jetstream?ref=ricofritzsche.me). Once the event is safely in the queue, we return HTTP 202 Accepted. The device is free to go about its business. Everything else – writing to Postgres, evaluating geofences, triggering alerts – happens asynchronously by downstream consumers. This gives us a shock absorber. When devices send a burst of updates, JetStream buffers them. When the database is under maintenance, data keeps flowing into the queue. When we need to add new processing logic, we add another consumer without touching the ingestion path. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/10/Diagram-1_-Direct-to-DB-vs-Decoupled-Arc-2025-10-26-082813.png) Diagram 1: Direct-to-DB vs Decoupled Architecture The alternative would be handling backpressure at the API layer, implementing complex retry logic in every device, and watching your database struggle under write load. I’ve seen production systems grind to a halt because someone decided ingestion should be “simple” and wrote straight to the database. ## How the Pipeline Actually Works A device posts telemetry to **/api/v1/telemetry**. The Rust service validates each record – checking required fields, normalizing timestamps to UTC, rounding coordinates to seven decimal places. Invalid records get rejected immediately with specific errors. We don’t try to save garbage. For each valid record, we generate a deterministic event ID using UUIDv5\. We hash the tenant ID, asset ID, timestamp, and normalized coordinates together. This means the same telemetry data always produces the same event ID, which becomes crucial for idempotency later. The service then publishes each event to JetStream with a subject like: ``` telemetry.v1.{tenant}.{assetHash} ``` We sanitize tenant identifiers and hash asset IDs to keep subject names consistent. JetStream writes the event to disk and acknowledges receipt. Only then do we consider the ingestion successful. JetStream stores these events in an append-only log with configurable retention – we keep three days of data by default. This log is durable and replayable. If a downstream consumer crashes, it can resume from where it left off. If we need to reprocess historical data, we can replay events from the log. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/10/Diagram-2_-Event-Flow-Through-the-Pipeli-2025-10-26-083935.png) Diagram 2: Event Flow Through the Pipeline Multiple consumers can subscribe to the same stream for different purposes. One writes to Postgres, another evaluates geofence rules in real-time, another might update an in-memory cache. The ingestion service doesn’t care what happens downstream. It captured the data durably, and that’s its only job. ## Why Durability Matters More Than You Think When JetStream acknowledges an event, it has written that event to disk. If the broker crashes and restarts, the event is still there. If our Postgres database goes offline for maintenance, devices can keep sending data. Events queue up in JetStream until the database comes back. Every telemetry update is a first-class event in an immutable log. We can replay past events to recompute outcomes, audit exactly what data we received and when, or trace any alert back to the original location update that triggered it. Old telemetry ages out naturally within three days what is enough to recover from most operational issues and to replay recent data. Critical events like geofence entries and exits get persisted separately in a long-term events database. The log-based approach also enables debugging that would be impossible with direct database writes. If a customer questions a reported location or missed alert, we can pull the exact event from the log and see what the system actually saw. No guessing, no reconstructing from database state. ## Scaling Horizontally Without Breaking a Sweat The ingestion service is completely stateless. We run multiple instances behind a load balancer. Each instance validates requests and publishes to the same JetStream cluster. Adding capacity means deploying more instances. No coordination needed, no shared state to worry about. JetStream itself can be clustered for both scalability and resilience. If consumers can’t keep up with the ingest rate, messages queue in JetStream rather than backing up into the HTTP layer. The service keeps accepting data at line speed while downstream processes work through the backlog at their own pace. We also support batching at the API level. Devices can send multiple telemetry records in a single request, either as a JSON array or newline-delimited JSON. This reduces HTTP overhead and allows downstream consumers to batch database inserts. Writing 500 records in one transaction is vastly more efficient than 500 individual inserts. The subject naming scheme provides logical partitioning by tenant and asset. If one tenant generates extreme load, we can dedicate processing resources specifically to their feed without impacting others. Heavy tenants don’t cascade failure to everyone else because their events flow through isolated subject channels. ## Making Operations Deterministic and Idempotent We use UUIDv5 for event IDs because it’s content-addressed. The same input data always produces the same UUID. This makes the entire pipeline idempotent by design. When publishing to JetStream, we set the *Nats-Msg-Id* header to the event ID. JetStream deduplicates messages with identical IDs within a configured window. If our service tries to publish the same event twice – maybe due to a network retry – the broker recognizes the duplicate and discards it. Downstream, the event ID serves as a primary key in Postgres. If a duplicate event somehow makes it through, the database insert fails on unique constraint violation rather than creating duplicate records. The geofence evaluator uses event IDs to skip events it has already processed. This determinism extends to testing. Our validation and normalization logic is purely functional – no I/O, no system time calls, no random UUIDs. Given the same input, you get the same output. This makes the code trivially testable without mocking frameworks or complex fixtures. If we need to evolve the telemetry format, we’ll version the schema explicitly rather than changing what constitutes a unique event. The event ID is derived only from core identifying attributes – tenant, asset, timestamp, coordinates. Optional metadata fields don’t affect the event’s identity. This stability is crucial for maintaining idempotency over time. ## Handling Failures Without Losing Data Distributed systems fail. Networks partition, brokers get overloaded, downstream services crash. We built failure handling into every layer. When the service publishes to JetStream, it waits for acknowledgment with a timeout. No ACK means potential failure. We retry up to three times with exponential backoff. Same event ID every time, so if the broker actually received it, deduplication prevents double insertion. If retries fail, the event goes to a Dead Letter Queue. We maintain a separate JetStream stream specifically for events that couldn’t be published to the main stream. These DLQ events include the original payload and metadata about the failure. Operations can inspect the DLQ manually and replay events if needed. The ingestion API still returns 202 Accepted even when events land in the DLQ, but we note the failure in the response and increment rejection counters. This alerts the client that something unusual happened without requiring them to retry and potentially create duplicates. We also implement rate limiting per tenant using a token bucket algorithm. If a client exceeds their rate limit, we return HTTP 429 immediately. This protects the pipeline from runaway clients and provides early backpressure. Large payloads get rejected with 413 to prevent memory exhaustion. The service’s readiness check requires JetStream connectivity. If the broker is down, we report not ready and the load balancer stops routing traffic. Better to fail fast than accept data we can’t handle. ## Keeping Tenants Isolated Every telemetry record is tagged with a tenant identifier. The NATS subject includes both tenant and asset: *telemetry.v1.{tenant}.{assetHash}*. This means tenant data never mixes at the message level. Consumers can subscribe to specific tenant feeds using subject filters. We can run dedicated processing instances for high-volume tenants or assign them to separate database partitions. One tenant’s load spike doesn’t cascade to others because their event streams are logically isolated. Events for each tenant+asset combination arrive in order. JetStream preserves publish order within a subject. This predictability matters for geofence evaluation – GPS points get processed in the sequence they were reported. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/10/Diagram-3_-Tenant-Isolation-via-Subject-2025-10-26-084717.png) Diagram 3: Tenant Isolation via Subject Routing At scale, we could create separate JetStream streams per tenant for complete isolation. Currently, one stream with subject-based partitioning has been sufficient. The key is maintaining tenant boundaries from ingestion through processing so no tenant affects another’s reliability. ## What We Avoided by Not Writing Directly to Postgres Direct-to-database ingestion looks simpler but that simplicity is deceptive. First, throughput becomes limited by database write performance. Postgres can handle many concurrent inserts, but thousands per second from distributed trackers will eventually overwhelm it. You’ll see lock contention, I/O saturation, and climbing response times. Second, you have no backpressure handling. If the database lags or goes offline, incoming data either gets dropped or clients time out. There’s no buffer to smooth out traffic spikes or provide resilience during database maintenance. Third, you’ve coupled ingestion latency to database operations. Clients must wait for transactions to complete and indexes to update before getting a response. With our pipeline, clients get sub-10ms responses regardless of downstream load because we’re just validating and queuing. Fourth, error handling becomes messy. If a single record in a batch violates a constraint, do you roll back the entire batch? Do you partially commit? How do clients know which records succeeded? The pipeline handles each event individually with clear success or rejection states. Fifth, you can’t easily add new consumers. If you want to start computing analytics or pushing updates to a cache, you’re either polling the database or adding database triggers. Both approaches are inefficient and tightly couple new features to the data model. The event log approach trades some operational complexity for structural soundness. Yes, we run an additional service and maintain a message broker. But we gained horizontal scalability, reliable buffering, replayable history, and clean separation between ingestion and processing. Those benefits are worth the operational overhead. ### Conclusion Could you build this differently? Sure. Kafka instead of JetStream, C# instead of Rust, different subject schemes or retention policies. The specifics matter less than the architectural pattern: decouple high-speed data intake from heavy processing, buffer through a durable log, and make everything deterministic and replayable. This isn’t over-engineering. It’s acknowledging that high-frequency telemetry systems have specific requirements that a simple REST-to-database approach can’t meet. The added complexity pays for itself in reliability, scalability, and operational flexibility. If you’re building anything that ingests substantial event streams – IoT telemetry, application logs, financial transactions – consider this pattern. The investment in proper event handling up front saves you from painful rewrites later when your direct-to-database approach can’t keep up. *Cheers*! ### Newsletter #2: When Layers Get in the Way URL: https://ricofritzsche.me/newsletter-2-when-layers-get-in-the-way/ Last updated: 2025-10-08T16:19:20.000Z More layers don't make architecture cleaner. They make it harder to see what's actually happening. _This post is for subscribers only._ ### Beyond Enterprise OOP: Building Clear, Composable Systems with PostgreSQL and Rust URL: https://ricofritzsche.me/beyond-enterprise-oop-building-clear-composable-systems-with-postgresql-and-rust/ Last updated: 2025-10-08T12:37:50.000Z A recent discussion about treating database routines as Microservices resonated with something that had been forming in my work for years. If a routine is cohesive, versioned, and close to the data, it already behaves like a service: no extra runtime, no layers forwarding queries through a web framework, no duplicated rules. It’s a simple idea that cuts against decades of enterprise reflexes. [PostgreSQL user-defined functions and stored procedures, also known as PostgreSQL routines, are essentially Microservices. PostgreSQL-hosted Microservices! Here are some core Microservice features… | Vedran B. | 18 commentsPostgreSQL user-defined functions and stored procedures, also known as PostgreSQL routines, are essentially Microservices. PostgreSQL-hosted Microservices! Here are some core Microservice features and properties that are present with PostgreSQL routines: 1\. Independently Deployable Yes, you can deploy and redeploy them without needing to redeploy the entire system easily. You don’t even have to restart anything, the deployment is atomic with zero downtime. 2\. Loosely Coupled PostgreSQL routines are independent — changes in one service minimally affect others. This is the same as Microservices, you need to design them that way. Also, routines use a lightweight protocol, the internal PostgreSQL protocol, which is even lighter than REST or gRPC. 3\. Highly Cohesive Each PostgreSQL routine has a single, well-defined business responsibility, and it promotes simplicity and maintainability. Same as Microservices, you need to define a clear business responsibility. They encapsulate business logic close to data for reuse and performance. 4\. Own Data and State (Decentralized Data Management) Each PostgreSQL routine usually manages its own set of tables and thus avoids tight coupling. But you can deploy them on different servers or even use foreign data wrappers inside to manage tables on different servers or a different kind of server, even. 5\. Autonomous (Self-Contained) Each PostgreSQL routine can operate and make decisions independently of others. It encapsulates its dependencies (own set of tables). 6\. Scalable Independently Each service can be scaled based on its own resource needs. Just deploy them on the replica servers and/or cache the output. Microservice scaling capability is tied to the underlying database, anyhow. But if your routines don’t even access data, opportunities to scale and optimize are even greater, same as with Microservices. 7\. Resilient and Fault-Isolated Failures in one routine don’t bring down others, they are independent. But you can also use connection retry and command retry mechanisms. 8\. Discoverable and Observable You don’t even need a service registry, they are discoverable in a standardized information schema metadata. It’s automatic. Just look at the “information\_chema.routines” table. 9\. Technology Heterogeneity (Polyglotism) Yes, you can write them in C, Rust, Python, JavaScript, Java, PHP, that I know of, and lately C#. But it has its own procedural language that works naturally and seamlessly with SQL. 10\. Organized Around Business Capabilities PostgreSQL routines map to business domains, not technical layers. PostgreSQL will abstract the technical aspects of the storage engine and expose your physical data model, which represents your business domain. --- So yeah, in effect and practically speaking, and for all intents and purposes - PostgreSQL routines (user-defined functions and stored procedures) are just PostgreSQL-hosted Microservices. | 18 comments on LinkedIn![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/icon/al2o9zrvru7aqj8e1x2rzsrca-3)LinkedInVedran B.![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/thumbnail/c45fy346jw096z9pbphyyhdz7-1)](https://www.linkedin.com/feed/update/urn:li:activity:7381199465942126592/?ref=ricofritzsche.me) I’ve spent much of my career inside those reflexes. In enterprise environments built with Java or C# the architecture diagram usually came first and the data last. Every change passed through a procession of layers: repositories, entities, services, DTOs, mappers. Each tried to abstract something that was already concrete. Over time, those layers became barriers between what a system knows and what it does. The same frustration appears everywhere: databases are treated as storage engines while the real logic hides in application layers. Yet most of what systems do is manage state transitions in data. When an order moves from pending to shipped, or a vehicle crosses a boundary, the meaningful part of the process happens at the data level, not in memory. Logic written close to data isn’t an anti-pattern. As Uncle Bob argues in *Clean Architecture*, the database is treated as a detail, hidden behind layers of indirection. In many data-driven systems that distance doesn’t add safety, it only obscures where decisions actually happen. My work takes the opposite direction. Instead of isolating the database as a technical concern, I treat it as the foundation for rules that belong near the data. The application defines control flow and intent; the database defines truth and constraints. This clear division keeps logic visible while allowing the system to rely on the guarantees already built into the database. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/10/Architecture-flow-_-Mermaid-Chart-2025-10-08-110909.png) This flow summarizes the architecture in one view: the application defines intent; the database guarantees truth. This way of thinking grew naturally after years of watching mainstream object-oriented patterns outlive their usefulness. Once you stop assuming that every system needs multiple layers to stay clean, other shapes emerge.The database can continue to excel in areas such as integrity, consistency and atomicity, while the application can focus on validation, coordination and decision flow. The distance between data and logic shrinks without turning the database into a black box. The important insight is that simplicity doesn’t mean the absence of structure; it means having just enough of it. A system can have boundaries and clarity without the scaffolding of enterprise OOP. The database doesn’t need to be passive storage, and the application doesn’t need to hide logic behind abstractions. Each part should do what it’s best at and remain transparent about it. ## How I Solve It The guiding principle is proximity and keeping logic where it naturally belongs. The database shouldn’t execute control flow; its job is to define and protect truth. The rules that guarantee consistency live in the schema through constraints, keys, and declarative logic. The application handles decisions about what to check, what to allow, and what to reject. Those decisions stay explicit, traceable, and easy to test. A simple example makes this clear. Take a rule like *“an item name must be unique within a tenant".* In many enterprise stacks this would be scattered across several layers: a repository checking for duplicates, a service applying the rule, a mapper translating objects, and a controller wrapping it all in an API response. That’s a lot of structure for one invariant. In this approach, the rule lives in the database itself. ```sql create table if not exists geofence_name_guard ( tenant_id text not null, name_norm text not null, geofence_id uuid not null, unique (tenant_id, name_norm) ); ``` A unique constraint expresses it directly and atomically. The application doesn’t need to repeat that rule in its own logic; it only needs to interpret the outcome and respond accordingly. When a write fails because of a constraint violation, the system isn’t broken but it’s behaving correctly. The logic around it stays minimal. Instead of an object graph, there’s a small, immutable data structure holding what the operation needs to know such as which record is being updated, whether it exists, and who currently owns the name. A simple decision function consumes that data and returns an outcome. ```rust pub fn decide_rename(f: &RenameFacts) -> RenameDecision { if !f.geofence_exists { return RenameDecision::GeofenceNotFound; } match f.existing_guard_holder { None => RenameDecision::Allow, Some(holder) if holder == f.geofence_id => RenameDecision::Allow, Some(_) => RenameDecision::NameTaken, } } ``` The surrounding code performs the orchestration: it loads the necessary data from the database, calls the decision function, and applies the result within one transaction. The decision remains pure and easy to test; the database enforces integrity; and the application layer makes the sequence explicit from start to finish. ```rust pub async fn handle_rename_geofence( pool: &PgPool, tenant_id: &str, geofence_id: Uuid, new_name: &str, new_name_norm: &str, ) -> Result<(), AppError> { let geofence_exists: bool = sqlx::query_scalar( "SELECT EXISTS( SELECT 1 FROM geofence WHERE id = $1 AND tenant_id = $2 )" ) .bind(geofence_id) .bind(tenant_id) .fetch_one(pool) .await?; let existing_guard_holder: Option = sqlx::query_scalar( "SELECT geofence_id FROM geofence_name_guard WHERE tenant_id = $1 AND name_norm = $2" ) .bind(tenant_id) .bind(new_name_norm) .fetch_optional(pool) .await?; let facts = RenameFacts { tenant_id: tenant_id.to_string(), geofence_id, new_name: new_name.to_string(), new_name_norm: new_name_norm.to_string(), geofence_exists, existing_guard_holder, }; match decide_rename(&facts) { RenameDecision::GeofenceNotFound => Err(AppError::NotFound), RenameDecision::NameTaken => Err(AppError::Conflict("Name already taken".into())), RenameDecision::Allow => { let mut tx = pool.begin().await?; sqlx::query( "UPDATE geofence SET name = $1 WHERE id = $2 AND tenant_id = $3" ) .bind(&facts.new_name) .bind(facts.geofence_id) .bind(&facts.tenant_id) .execute(&mut *tx) .await?; sqlx::query( "INSERT INTO geofence_name_guard (tenant_id, name_norm, geofence_id) VALUES ($1, $2, $3) ON CONFLICT (tenant_id, name_norm) DO UPDATE SET geofence_id = EXCLUDED.geofence_id" ) .bind(&facts.tenant_id) .bind(&facts.new_name_norm) .bind(facts.geofence_id) .execute(&mut *tx) .await?; tx.commit().await?; Ok(()) } } } ``` This approach follows the same idea of keeping logic close to data without hiding behavior inside the database. Everything stays visible and deterministic. The database remains the authority on consistency, while the application coordinates the flow of decisions and writes. Together they form a conversation that’s easy to follow: the request expresses intent, the database enforces the rules, and the result becomes part of the system’s record. The goal isn’t to reduce code for its own sake, but to remove what doesn’t add meaning. When a constraint already defines a rule, there’s no reason to simulate it again in code. When a decision can be expressed with simple data and a clear outcome, there’s no need to wrap it in layers of abstraction. The database guarantees integrity; the application keeps intent explicit. This way of building software invites a closer look at where complexity really comes from and how much of it is self-inflicted. Many systems grow heavy not because of the problem they solve but because of the frameworks and patterns wrapped around them. When each layer stops trying to hide the next, the design starts to feel lighter and more direct. ## A Simpler System, Still Composable This way of working changes how a system takes shape. Each feature becomes a small, self-contained slice, a single path from input to outcome. It defines its data structures, decision logic, and transaction, and doesn’t depend on shared models or global services. This structure aligns naturally with event modeling, where every action is seen as a complete scenario: what information arrives, what decision is made, and what state change follows. By treating each slice as its own closed loop, the system remains easy to reason about and extend.# ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/10/Untitled-diagram-_-Mermaid-Chart-2025-10-08-113535.png) closed-loop slice Each slice operates on its own data and logic but shares a common foundation. The database ensures integrity and atomicity, while the application layer keeps control flow visible. Because slices interact only through persisted state or well-defined events, they remain independent. One slice can evolve, be replaced, or scale separately without disturbing the rest. The system grows by addition instead of revision; new behavior joins alongside existing flows rather than reshaping them. This separation makes scaling straightforward. When a slice grows or needs a different lifecycle, it can move into its own service without redesign. The interface is already simple: SQL in, decision out, commit. The system doesn’t start as Microservices; it grows toward that naturally as boundaries solidify. It also simplifies what many traditional systems tend to over-complicate. There’s no [ORM](https://en.wikipedia.org/wiki/Object%E2%80%93relational%5Fmapping?ref=ricofritzsche.me) state to coordinate, no long chain of dependencies to follow before reaching the data. The logic sits next to the facts, and each request describes a complete piece of work from start to finish. At runtime the flow stays predictable. A request comes in, the Rust shell validates it, the slice executes its sequence: load the relevant data, run the decision, apply the result in a single transaction. The path is visible in logs and traces, and when something breaks, it fails in one place that’s easy to see and fix. This simplicity isn’t fragile. It’s controlled. PostgreSQL handles transactions and concurrency; Rust enforces ownership and error handling. Together they create boundaries that keep features independent without coordination frameworks. Even under load, the behavior stays the same. Each slice reads only what it needs, makes its decision, and applies the result in a single transaction. There are no shared caches to refresh, no domain objects drifting out of sync with persisted state. The surface area remains small, and the logic stays transparent regardless of scale. The aim is to keep the distance between data and decision short. Logic stays visible, and every effect is deliberate. The database maintains consistency; the application defines intent. Each side reinforces the other, creating a system that is simple to follow and easy to trust. Over time, the system grows by addition rather than accumulation. New behavior arrives as new slices, each with its own data structures, decision logic, and transaction. When a slice is complete, it stands on its own. Nothing else needs refactoring or layering on top. The architecture remains steady because every part keeps the same clear boundaries it started with. It’s a modest structure but one that holds up well over time. It doesn’t try to impress with complexity or patterns; it works because each part knows its role. Logic and data stay close, decisions stay visible, and every change leaves the system as understandable as it was before. ## What This Avoids Enterprise systems often grow around abstractions meant to protect developers from their own tools. ORMs, repositories, service locators, dependency injection frameworks each starts as a convenience and ends as a layer that hides what’s really happening. These structures create distance between the data and the behavior that depends on it. Over time, the system becomes a maze of intermediaries translating the same values back and forth. The approach I describe keeps that distance short. The database already understands how to guarantee integrity; there’s no need to reimplement it in code. A unique index, a constraint, or a transaction does the job directly and consistently. Rather than competing with these mechanisms, the application cooperates with them. By treating the database as the authority on state and using Rust to define how that state may change, the system loses none of its structure but gains a great deal of clarity. Queries are explicit. Decisions are explicit. Every line of code exists for a reason that’s visible in the logs and reproducible in tests. This clarity also removes much of the accidental complexity around data flow. There’s no object graph to manage, no lazy loading to debug, no hidden state waiting to surprise you under load. The service does one thing at a time, and you can read its path from request to commit without guessing what happens in between. The same principle applies to evolution. When a rule changes, you update a decision function or a single SQL statement. There’s no cascade of side effects in generated code or configuration. The shape of the system remains stable while the rules inside it evolve freely. It’s a simpler kind of discipline. Instead of controlling complexity with more architecture, you limit it by keeping each part honest about its role. PostgreSQL guards the facts. Rust moves the data safely through the system. The result is a codebase that grows without losing transparency, and a system that doesn't fight you as it matures. ## Conclusion Many large systems grow heavy because they try to protect developers from their own tools. Frameworks and layers (ORMs, repositories, service locators, dependency injection) begin as conveniences and end as barriers. Each one adds distance between the data and the logic that depends on it. Over time, the system spends more effort maintaining its structure than expressing its rules. Over the years, I’ve grown skeptical of architectures that try to protect developers from the tools they use. Layers of abstraction promise freedom from change but often achieve the opposite. They make small changes expensive and simple ideas hard to express. The systems that survive are not the most abstract; they are the ones that remain understandable. The model I describe here is not new or revolutionary. It’s closer to rediscovery. Many of the ideas we now label “modern architecture” existed long before frameworks. What’s different today is the maturity of the tools. PostgreSQL offers transactional consistency, indexing, and performance that used to require specialized systems. Rust gives safety and precision without runtime overhead. Combined, they allow a style of development that feels closer to reasoning about a problem than managing an architecture. I see this very much as a step away from the object-oriented style that shaped most enterprise systems I worked on. After years of practicing it, I no longer find the trade-offs worthwhile. The constant modeling of behavior into classes and hierarchies creates distance from the data, not clarity. Objects try to represent change through mutable state and inheritance, yet most real-world systems revolve around immutable facts and clear transitions. Microservices, on the other hand, are a different matter. I do not reject them, but I approach them differently. In my view, they should emerge from well-defined slices of behavior, not from diagrams drawn in advance. A feature that already has a clean boundary and minimal dependencies can become a service naturally, without a framework or ceremony around it. When logic and data stay close, the feedback loop tightens. You can see immediately how rules behave, test them as small decisions, and deploy them without fear of side effects. It’s not a call to push everything into the database or to rewrite systems in Rust. It’s an argument for clarity, for designing systems where the flow of information is short, explicit, and easy to follow. That’s the goal in all of this: less complexity, more clarity. A system that explains itself when you read it and where every part does exactly what it’s meant to do, and nothing more. *Cheers*! ### The Era of GIS is Over: Why Decision-Makers Must Move Beyond GIS URL: https://ricofritzsche.me/the-era-of-gis-is-over-why-decision-makers-must-move-beyond-gis/ Last updated: 2025-09-27T12:43:17.000Z Let’s be honest: Geographic Information Systems *(GIS)* is a name that belongs in the museum of outdated tech acronyms. Born out of CAD ([Computer-aided design](https://en.wikipedia.org/wiki/Computer-aided%5Fdesign?ref=ricofritzsche.me)) in the 1960s, slapped onto clunky client-server architectures in the 1990s, and marketed through the 2000s with glossy brochures, USB license dongles, it smells of a past era. Every time I hear *GIS*, I don’t think of modern software system and data infrastructure. I think of bored analysts digitizing maps, endless licensing negotiations, and old men in suits selling shrink-wrapped software as if it were magic. The irony? Geography is not a “special system” at all. It’s not an add-on, not a desktop app, not a vendor license. Geography is intrinsic. It’s embedded in every dataset, every transaction, every movement in logistics, cities, and the environment. Yet by calling it *GIS*, we keep it fenced off, treated like an exotic artifact instead of what it is: the connective tissue of the modern world. The challenge ahead isn’t to save *GIS*. It’s to bury it. To replace it with something truly geographic-centric, woven into the same data infrastructure that already powers everything else. Something that reflects the messy, dynamic complexity of the real world instead of reducing it to shapefiles and layers. That requires courage, because it means throwing away not just old tools, but an entire way of thinking. ## Monolithic, Desktop-Centric Design Early *GIS* software was built as a one-stop monolith: a gigantic desktop application that imported, analyzed, and mapped data all on a single machine. That made sense in the 1980s and 90s, when networks were slow or nonexistent, but it’s laughable today. A 2025 review of *GIS* architecture admits the obvious that initially *GIS* were designed as standalone desktop applications with most of the data being stored, accessed and processed locally. These systems laid the groundwork for spatial analysis, but they were limited in terms of data sharing, scalability, and computational efficiency. That is, they were fine for a lone analyst in a stuffy 1990s office, but unsuitable for collaboration, distribution, or scaling. Sharing meant passing files around on floppy disks or, later, email attachments. Scaling up meant praying your workstation had enough RAM. Today's distributed, cloud-native approach is the complete opposite. Vendors have tried to modernize by containerizing or virtualizing these monoliths. But wrapping traditional *GIS* in containers doesn’t make it modern. It’s like putting a rotary phone in a smartphone case: the look changes, the experience does not. ## File-Based Workflows (Hello, Shapefiles!) And then there are shapefiles. The zombie format that refuses to die. If you've ever worked in *GIS*, you know the ritual: .shp, .shx, .dbf, and the sacred .prj file that tells your software what projection you're in. Forget one, and good luck. A funny [redbubble sticker](https://www.redbubble.com/i/sticker/Shapefile-Drama-Funny-GIS-Meme-for-Geographers-and-Map-Nerds-by-creative-nomad/170755233.EJUG5?ref=ricofritzsche.me) nails the absurdity of the shapefile drama. ![Thumbnail 3 of 3, Sticker, Shapefile Drama – Funny GIS Meme for Geographers & Map Nerds designed and sold by creative-nomad.](https://ih1.redbubble.net/image.5835351848.5233/bg,f8f8f8-flat,750x,075,f-pad,750x1000,f8f8f8.jpg) [Shapefile Drama](https://www.redbubble.com/i/sticker/Shapefile-Drama-Funny-GIS-Meme-for-Geographers-and-Map-Nerds-by-creative-nomad/170755233.EJUG5?ref=ricofritzsche.me): everyday pain of missing file dependencies. The limitations would be funny if they weren't still real. Shapefiles max out at 2 GB and about 70 million features. Maybe you’re hearing about the .dbf file for the first time. It’s not a database in any modern sense, but a leftover from [dBase](https://en.wikipedia.org/wiki/DBase?ref=ricofritzsche.me), a tool released in 1979 (46 years ago!) for storing tabular data on early PCs. Think of it as a flat spreadsheet with severe restrictions: field names cut off at ten characters, no proper handling of text encodings, and only a handful of basic data types. Field names are chopped at 10 characters (so "CustomerName" becomes "CustomerNa"). Worse: they don't even support null values for numeric or text fields. In an era where any relational database has handled nulls for decades, shapefiles still force hacks like "-9999" to mean "no data." Yet this fossil was grafted onto shapefiles to hold attributes, and decades later it’s still haunting *GIS* workflows. In other words, much of geodata today is shackled to a file format invented before the IBM PC even existed. And let's not forget projections. A shapefile doesn't actually store its coordinate system internally but it depends on that fragile projection sidecar file. Lose it, and you get the dreaded "Unknown Spatial Reference" error. Requiring multiple files to represent a single dataset is absurd in 2025\. Yet shapefiles remain everywhere, relics of a CAD-style workflow where maps were traced and filed like blueprints. It's a perfect symbol of the problem: ***GIS* never really escaped its file-based, desktop-centric roots.** ## The Walled Garden One of the biggest reasons *GIS* feels outdated is the way it fenced itself off from the rest of computing. While most technologies embraced open formats, APIs, and cloud-native services, *GIS* built its own ecosystem, complete with proprietary formats, complex licensing, and standards that often looked more like barriers than bridges. Esri's file geodatabase is a good example (...or rather a bad one). It was introduced as the successor to the shapefile, fixing some technical limits but introducing a new problem: portability. Full support is tied to Esri's own software. Open-source tools can read parts of it, but write access is limited. What should have been a step forward in data management became another form of vendor lock-in. Even the industry’s standards reveal the same pattern. The OGC protocols, WMS and WFS, were intended to promote interoperability. They worked — to a point. But they are rooted in the web service design of the early 2000s: verbose, XML-based, and hard to integrate into modern, API-driven data workflows.They feel more like relics than solutions. This is why the OGC has launched a new line of simpler, REST-style [OGC APIs](https://ogcapi.ogc.org/?ref=ricofritzsche.me). Licensing reflects the same mindset. In most IT domains, scaling is an infrastructure question: add more machines, expand resources, and you’re done. In *GIS*, scaling is often a licensing question. Historically, products like ArcGIS Server tied cost to machine roles or processor cores, which does not sit well with today’s elastic cloud environments. Spinning up ten more containers is trivial from a technical perspective, but not from a contractual one. The result is that *GIS* isolated itself. Geography became something managed in a separate garden with its own tools, formats, and rules, rather than treated as a native part of broader data infrastructure. Meanwhile, the rest of the tech world moved on: cloud warehouses like BigQuery and Snowflake now support spatial SQL directly, vector tile servers expose geography through lightweight APIs, and formats like GeoParquet and COG bring spatial data into mainstream analytics. It is not that geography is difficult to integrate. It is that *GIS* has too often chosen not to. ## The Desktop Burden Desktop tools reveal how *GIS* is experienced today. Open any traditional *GIS* and you face a cluttered screen of menus, toolbars, and icons. Functionality piled upon functionality until the interface resembles an archive rather than a tool. The philosophy is simple: add any imaginable feature to the menu. The result? Software with thousands of functions but minimal guidance. Newcomers are overwhelmed while veterans develop muscle memory to navigate the clutter, not because workflows are elegant. This isn't about open source versus proprietary. QGIS mirrors the same assumptions as systems it aimed to replace: that *GIS* requires specialists trained to memorize interfaces rather than offering accessible capabilities. It values features over clarity or design. Modern developer tools stand in stark contrast, emphasizing fewer buttons, clearer abstractions, and integration with existing workflows. *GIS* has instead embraced complexity as if tradition matters more than usability. As a result, the geography remains siloed, both architecturally and experientially. It is hidden behind menus rather than functioning as a natural extension of data work. These failings are not accidental. They are architectural and cultural. Which is why the solution cannot be another GIS layer or yet another toolbox. It has to be a new foundation. ## Toward a Geographic-Centric Information Infrastructure The way forward is not a better GIS. It is an entirely different foundation. Geography should not live in its own silo. It should be treated as what it really is: a basic property of data. Voices such as[ Spatial Spirits](https://spatialspirits.com/?ref=ricofritzsche.me) are already probing these deeper shifts, asking how geography can be woven into the core fabric of data systems itself. Geodata should be treated as what it really is: a basic property of data. A coordinate is just another number. A road network is simply a graph. A boundary is a polygon. None of this requires a separate kingdom to manage. A geographic-centric information infrastructure means folding geography directly into the systems we already use. Databases, data lakes, and event streams should handle spatial information alongside everything else. Imagine a logistics platform that queries which vehicle is closest to a pickup point through the same API that retrieves inventory data. Or a city dashboard where traffic sensor feeds are analyzed in real time alongside weather forecasts, without exporting to a separate *GIS*. Or an environmental monitoring system that ingests satellite imagery directly into a cloud data lake and triggers alerts automatically, rather than waiting for an analyst to click through menus. These are not speculative futures. They are the kinds of workflows that become possible once geography is treated as intrinsic. The technology for this already exists. Systems like Google BigQuery, Snowflake, or PostgreSQL with PostGIS can store and query billions of rows of spatial data. Formats such as GeoParquet or cloud-optimized GeoTIFFs make geodata efficient to use in data lakes. And stream processors like Apache Kafka or Flink can evaluate events the moment they arrive. Modern file formats handle vast volumes without splitting into sidecars. Geography can live comfortably in this environment, but only if we let go of the idea that it requires its own ecosystem. The harder part is not the technology but the mindset. For decades, *GIS* encouraged the belief that geography was special, requiring its own tools, training, and rituals. Breaking that belief means accepting that geography belongs everywhere, not in a corner. It means building models that reflect the complexity of the real world without reducing it to layers and shapefiles. And it means trusting that geography, when treated as intrinsic, will not lose its importance. Voices like [Spatial Spirits](https://spatialspirits.com/?ref=ricofritzsche.me) are already surfacing the questions we need to ask: how do we stop treating geography as something special, and start weaving it into the core of our data systems? ## The Strategic Outlook For decision-makers, the real risk is not that *GIS* is old, but that it keeps organizations anchored to old ways of working. Systems built around desktop software, proprietary formats, and licensing models cannot keep pace with the demands of real-time logistics, smart cities, or environmental monitoring. Geography matters more than ever, but the tools that once carried its name now stand in the way. The shift is not about chasing the latest technology trend. It is about recognizing geography as a core dimension of business and society. Location is present in every transaction, every movement, every connection. When treated as a side discipline managed by specialists, its value remains trapped. When embedded directly into infrastructure, it becomes a source of speed, scale, and resilience. The organizations that succeed will not be those with the largest *GIS* departments or the most certifications. They will be those that see geography for what it is: part of the business data itself. They will integrate it into their platforms as naturally as time, identity, or events, and they will act on it without waiting for someone to click through menus. The message is simple. The acronym *GIS* belongs to the past. What follows is not a rebranding exercise or a new layer of tooling, but a different way of thinking: geography everywhere, no longer confined to its own system, but part of the foundations on which modern decisions are made. *Cheers*! ### How I Built a High-Performance Geocoding Engine from Scratch URL: https://ricofritzsche.me/how-i-built-a-high-performance-geocoding-engine-from-scratch/ Last updated: 2025-09-05T10:34:07.000Z During years working in logistics, and automotive, I often encountered the same technical bottleneck. Whenever a route needed planning, or a delivery had to be scheduled, there was an address behind it. And that address had to be resolved into coordinates. This task is known as geocoding. For some systems, a delay of a few hundred milliseconds is acceptable. But in logistics, especially when processing large volumes or optimizing entire fleets, every additional millisecond has an impact. In a typical logistics morning, it’s routine to geocode 1–5 million stops within minutes; shaving even 200–300 µs per lookup can save tens of minutes end-to-end. I wanted to see what’s possible when you reduce everything to the essentials without relying on heavy infrastructure, complex setups, or oversized hardware. My goal was simple: resolve addresses as fast as possible, using open data, efficient formats, and focused code. So I built a geocoding engine from scratch. It’s pure Rust, **memory-mapped** ([mmap](https://crates.io/crates/mmap?ref=ricofritzsche.me)) and keyed by a **minimal-perfect hash** ([boomphf](https://docs.rs/boomphf/latest/boomphf/?ref=ricofritzsche.me)). The server is read-only at runtime; freshness comes from nightly rebuilds. It processes address queries and returns precise coordinates, with sub-millisecond latency and very low resource usage. The system works without a database, runs entirely on a memory-mapped file, and stays fully read-only during operation. ### What is Geocoding? Geocoding is essentially address translation. Give the geocoder a street address, and it returns the latitude and longitude of that location on Earth. For example, the address “*Pariser Platz 1, 10117 Berlin, Germany”* (the area of the Brandenburger Tor in Berlin) might geocode to roughly *52.5163° N, 13.3777° E*. In everyday life, you use geocoding whenever you type an address into a maps app and see a pin on the map. Geocoding can be forward (address → coordinates) or reverse (coordinates → nearest address). In this article, I want to focus on forward geocoding: given a full address, find its coordinates almost instantly. ![](https://cdn-images-1.medium.com/max/1600/1*LRX6ZWyNjDoNoF_UPuwT_Q.png) But why does speed matter? In logistics, many workflows begin with a batch of addresses that need to be turned into coordinates, often in large volumes. Delivery platforms geocode thousands of stops each morning to prepare optimized routes. Autonomous vehicles receive destination addresses that must be resolved before navigation can begin. And location validation systems check every incoming order or shipment in real time to confirm that the address is accurate and complete. These lookups happen continuously, sometimes at a rate of hundreds of thousands per hour. In such scenarios, even small delays per address accumulate quickly. A geocoder that can respond in microseconds allows these systems to stay responsive at scale, reduce lead times, and support smooth daily operations. ### What the Engine Produces The geocoder takes an address query and responds with a lightweight, consistent result. Each response includes the precise coordinates, a geohash, a rough accuracy estimate in meters, and metadata about the original data source. It also returns a normalized address and a formatted version that’s suitable for display. For example, a query for: ```bash GET /geocode?addr=Brunnenstraße 107, 13355 Berlin ``` The response also includes method, indicating how the point was obtained (e.g., “rooftop”, “interpolated”, “postcode\_centroid”). ```json { "formatted": "Brunnenstraße 107, 13355 Berlin, Deutschland", "components": { "house_number": "107", "street": "Brunnenstraße", "postcode": "13355", "city": "Berlin", "state": "Berlin", "country": "Deutschland" }, "location": { "lat": 52.50934168844568, "lon": 13.419302240918833, "geohash": "u33d9r59h", "method": "rooftop", "accuracy_m": 3 }, "lookup_ns": 320000, "source": "osm+oa" } ``` This information includes the rooftop-level coordinate, an estimate of the spatial accuracy in meters, a geohash representation of the point, and metadata about the source dataset. This result is typically returned in well under one millisecond, even on standard hardware. The formatted field represents the human-readable form of the address. The components field holds the structured parts, such as city, postcode, and house number. accuracy\_m is a 95% error‑radius estimate in meters derived from the match method. Typical ranges: \- rooftop/entrance: 2–10 m \- building‑centroid: 5–20 m \- interpolated house number: 20–150 m (short urban blocks → low end; long rural segments → high end) \- street‑level: 20–150 m \- postcode centroid: 500–5,000 m (area‑based) \- city centroid: 1–20 km (area‑based) The source flag is “osm”, “oa”, or “osm+oa” when both datasets agree. By keeping the structure predictable and compact, the response can be parsed quickly and reliably by downstream systems, whether it’s used for routing, enrichment, or display. ### What Is a Geohash? Geohash is a compact way to reference a place on the earth by encoding latitude and longitude into a short string. It works by dividing the world into a grid, then further subdividing each cell and translating each division into a Base32 character. The longer the geohash, the smaller the grid cell — and the more precise the location. For example, a 5-character geohash represents an area roughly 5 km wide, whereas 8 characters zoom into tens of meters. As you add letters, you zoom in. A handy feature of geohash is that nearby places often share the same prefix. If two addresses are geographically close, their geohash strings tend to match in the first few characters. This property makes geohash useful not just for encoding. It becomes a lightweight way to group nearby locations together. To avoid border misses, reverse geocoding scans the target cell **and** its eight neighbors. ![](https://cdn-images-1.medium.com/max/1600/1*Zl09ZdQCEwVyPOGE34aY3g.png) 3×3 geohash neighborhood. Search the center cell (prefix p) plus N, NE, E, SE, S, SW, W, NW; perform prefix range scans per cell. In my Rust engine, every address record includes its precision‑9 geohash. We sort the records by that value. Because neighboring addresses share prefixes, they also sit next to each other in the sorted list. This makes future features like reverse geocoding fast and simple: to find addresses around a point, you compute its geohash, then scan records that share the same prefix. It becomes a search by string range, not spatial math. I still have not implemented the reverse geocoding part yet, but will do it soon. Once the geohashes are stored in the sorted blob, close addresses naturally cluster. This spatial clustering gives us a clear path to efficient geographic queries without heavy indexing structures. We can rely on simple string comparisons and rely on the file layout to guide us. ![](https://cdn-images-1.medium.com/max/1600/1*3XKnsNLfoJBB57G89fesYg.png) Geohash Precision from World to Street ### How Geohash Fits into the Lookup Flow The previous section covered how geohash encodes location and creates spatial groupings. Now let’s look at how this plays a role inside the actual geocoding engine both during the build and at runtime. Each address in the system is stored as a small binary record. It includes latitude and longitude, accuracy in meters, a source flag, and the address parts in both canonical and display form. Alongside this, we store a geohash with precision 9. Precision 9 covers an area of around 4.7 × 4.7 meters, roughly the size of a small room or entrance. That level of detail is well-suited for rooftop-level results and still keeps the string short and indexable. Here’s what a few addresses in Berlin might look like after geohashing: ```json { "address": "Brunnenstraße 107, Berlin", "geohash": "u33dc0fj0" }, { "address": "Brunnenstraße 110, Berlin", "geohash": "u33dc0fj3" }, { "address": "Torstraße 1, Berlin", "geohash": "u33dc0fsn" } ``` All of them start with the same prefix: *u33dc0*, which represents a shared area on the grid. When sorted by geohash, they appear next to each other. Reverse geocoding isn’t implemented yet; the geohash-sorted layout makes it straightforward future work. That’s what I use. ```bash 1 unter den linden 10117 berlin 52.5164 13.3777 u33db2m65 1 unter den linden 10117 berlin de 1 Unter den Linden 10117 Berlin Berlin DE 1 brandenburger tor 10117 berlin 52.5163 13.3777 u33db2m3e 1 brandenburger tor 10117 berlin de 1 Brandenburger Tor 10117 Berlin Berlin DE 1 hauptstr 10117 berlin 52.5200 13.4050 u33dc0cpp 1 hauptstr 10117 berlin de 1 Hauptstr 10117 Berlin Berlin DE 1 kanzlerstr 09127 chemnitz 50.8325 12.9081 u311jtz2u 1 kanzlerstr 09127 chemnitz de 1 Kanzlerstr 09127 Chemnitz Sachsen DE 5 marienplatz 80331 munchen 48.1372 11.5756 u281z7j5e 5 marienplatz 80331 munchen de 5 Marienplatz 80331 München Bayern DE 107 honinger weg 50969 koln 50.9155 6.94124 u1hctuykq 107 honinger weg 50969 koln de 107 Höninger Weg 50969 Köln Nordrhein-Westfalen DE ``` During the build step, we assign geohashes to all address records and sort the full list by their value. This ensures that spatially close addresses are also physically close in the final file layout. > Implementation note: many libraries (including Rust geohash) expect (lon, lat) as Coordinate { x, y }. Use precision 9 for rooftop-level cells. At runtime, this layout enables efficient fallbacks. If a query only includes a postcode or a city, the engine doesn’t need to scan all records. It can take a known coordinate (like a postcode centroid), compute its geohash, and scan only the records that start with the same prefix. That covers a reasonable area and returns nearby results quickly. Because the file is sorted by geohash, this is just a range scan over a section of the data. There’s no tree traversal or coordinate math required during lookup. In short, geohash helps us cluster addresses during the build, and limit the search space during lookups. Precision 9 gives enough detail for rooftop matches, and its prefix makes group-based lookups efficient and predictable. To make all this work, each query goes through a tightly defined flow, from parsing the address to reading a memory-mapped record. The next part starts with how addresses are normalized and looked up using a minimal perfect hash. ### How Addresses Are Canonicalized and Indexed Before any lookup can happen, the incoming address needs to be parsed and brought into a consistent form. This step is important. Street names can vary in casing, spacing, or abbreviations. House numbers might include extra characters. Some users enter “Straße”, others type “Str.” or “Str” or “Strasse”. To keep lookups fast and reliable, I normalize every address into a canonical format. The parser breaks the input into components (house number, street, postcode, city, state, and country) and applies a few simple rules. At ingest I use *libpostal* to expand abbreviations and variants (e.g., Str., Straße, Strasse → brunnenstrasse), lower-case and strip diacritics; at query time a lightweight parser applies the same *normalizer* to build the canonical key. For example, the input: ```bash Brunnenstraße 107, 13355 Berlin ``` …becomes (country | state | city | street | house-number | postcode — all lower-cased, diacritics removed, abbreviations expanded): ```bash de|berlin|berlin|brunnenstrasse|107|13355 ``` This canonical string is what the engine actually indexes. It serves as the key in a minimal perfect hash function, a structure that maps each unique string to an exact numeric index. No collisions, no lookup trees, and no fallback probing. Because an MPHF returns an index for any input, the engine verifies membership by checking a short fingerprint (or canonical key bytes) stored with the record before using it. If the fingerprint doesn’t match, the lookup is treated as ‘not found’. The library I used for this is *boomphf*, which builds a minimal perfect hash from the full set of canonicalized addresses at blob build time. The hash takes just a few bits per entry, and produces an exact lookup index in constant time. At runtime, the engine follows this path: ```yaml input string ↓ parse and normalize → canonical string ↓ MPHF → index ↓ offset[index] → byte position in mmap ↓ read → candidate record ↓ verify fingerprint (or canonical bytes) ↓ match → return record | no match → not found ``` Each lookup skips branching logic or conditionals. It’s a direct path from string to memory address. ![](https://cdn-images-1.medium.com/max/1600/1*7isixgaPcdA7FLD9A7ajbQ.png) Minimal Perfect Hash Function (MPHF) This works well because the hash and offset array are both baked into the blob file. Once the hash is loaded, each canonical address string can be mapped directly to its corresponding record location with just two memory accesses. It also fails fast: if the fingerprint (or canonical‑bytes) comparison fails, the address isn’t in the index. ### How the Blob File Is Structured Every address record is stored inside a single binary file: compact, read-only, and designed for fast access. This file is called the blob. The blob is built once per day, typically as part of a nightly CI run. It contains everything the lookup engine needs: the hash function, the offsets, and the records themselves. The structure is simple and linear: ```yaml [header] [MPHF bytes] [offset array] // one offset per address [address records] // variable-length binary records ``` Each part plays a clear role: - The minimal perfect hash (MPHF) maps the canonical address string to a unique index. - The offset array maps that index to a byte position in the file. - The record at that position contains all the data needed to answer the query. ![](https://cdn-images-1.medium.com/max/1600/1*m572twe8nO-Ht6yn7HoQKg.png) Summarizes the on-disk blob and the end-to-end lookup path. All of this is loaded into memory using [mmap().](https://crates.io/crates/mmap?ref=ricofritzsche.me) The memory map makes the file feel like an array in RAM. The operating system handles paging in the background. The application doesn't need to read from disk or manage buffers. Access becomes a pointer arithmetic operation: 1. Use the MPHF to get an index. 2. Use the offset array to get the byte position. 3. Jump to that position and read the record. ![](https://cdn-images-1.medium.com/max/1600/1*0p6Q-fJzLgncja1ICzh_hA.png) Storage Layout Each address record starts with a small fixed-size header — latitude, longitude, accuracy, source flags, and geohash. After that come the variable-length address parts, both in canonical and display form. For example, a record includes: - lat, lon as f64 - accuracy\_m as u16 meters (0–65,535). One extra byte per record keeps the model simple and expressive. - source\_flags (bitmask: OSM, OA, etc.) - geohash (9 bytes, precision-9) - fingerprint of the canonical key (e.g., 32–64 bits) *or* canonical key bytes (length‑prefixed) for exact match verification - Address parts: house number, street, postcode, city, state, country (canonical + display) The fingerprint (or canonical bytes) allows a membership check at read time, which is necessary because an MPHF maps any input to an index. On average, each record is around 110 bytes, small enough to keep the memory footprint tight even with millions of entries. The blob stays entirely read-only. The server never mutates it. Updates happen by building a new version and swapping it out atomically, for example, via a new file path or environment variable. This separation between write-time and read-time keeps the design clean. Lookups never block. There’s nothing to lock or cache. The engine just maps the blob, reads memory, and returns a result. ### How the Blob Gets Built Every Night The geocoder doesn’t rely on a live database or background workers. Instead, it uses a prebuilt snapshot of the full address index generated once per day as part of a CI job. This process starts from scratch every night. It pulls in fresh data, cleans it up, merges everything, and writes out a new blob file. The steps are fixed and deterministic: 1) Fetch OpenStreetMap extract + diff 2) Download OpenAddresses CSV files per Bundesland 3) extract\_osm → osm.tsv 4) extract\_oa → oa.tsv 5) merge\_and\_rank → merged.tsv 6) interpolate → addr.tsv 7) centroid\_builder → postcode\_centroid.tsv, city\_centroid.tsv 8) build\_blob → addr\_index\_YYYY-MM-DD.blob 9) compress + upload to Azure Blob or S3 Each step has a single job: - *extract\_osm* pulls structured address nodes and ways from the latest .*pbf* extract. - extract\_oa parses [OpenAddresses](https://openaddresses.io/?ref=ricofritzsche.me) CSVs per state (Berlin, Bayern, etc.). - merge\_and\_rank combines both sources, resolves duplicates, and picks the most accurate version. - interpolate fills in missing house numbers from ranges. - centroid\_builder adds fallback records for postcodes and cities. - build\_blob writes the actual memory-mapped blob with MPHF, offsets, and records. These timings are from a 16-vCPU CI runner; adjust proportionally for smaller or larger machines. The result is a file like: addr\_index\_2025-08-07.blob This file is immutable and versioned by date. The blob is uploaded to an Azure Blob Storage or S3\. On cold start, the API server downloads the blob and memory-maps it locally. After that, every lookup is local and fast. This build-once, read-only model keeps the system simple and reliable. Each build is complete and self-contained. ### What This Design Enables Once the blob is built and loaded, the engine is ready to serve lookups with minimal latency and very little resource usage. There’s no warm-up phase, no preloading, and no caching logic – the file structure and the operating system take care of that. The complete index covers millions of addresses that can be easily stored in memory with maximum precision, including all offsets and metadata. The lookup path stays short: • Canonicalize the input address. • Use the hash to get an index. • Use the index to read an offset. • Jump to that byte in the memory-mapped file. • Return the record. On a warmed server, end-to-end lookups typically finish in a few hundred microseconds (around 200–400 µs p50) with tails under a millisecond, which roughly works out to a few thousand requests per core per second. Right after a restart or blob swap, expect brief millisecond-level spikes until the page cache settles, then it returns to the sub-ms profile. Because the system doesn’t rely on a live database, scaling is a matter of replicating the blob. Multiple API servers can map the same file and serve requests independently. This keeps deployment simple and avoids infrastructure overhead. In practice, this setup is well-suited for logistics, mapping platforms, on-device geocoding, or any application that needs fast, predictable address resolution without the weight of a full spatial database. ### Closing Thoughts This project started as a practical challenge: how fast can an address lookup get if you remove everything that doesn’t need to be there? What I ended up with wasn’t just a faster geocoder. For me, it was a reminder of how far you can go with a small, well-shaped system. No layers, no background queues, no service orchestration. Just clear data, focused transformations, and a file you can memory-map. The work also gave me a better sense of how data wants to be laid out when it’s read much more than it’s written. Sorting by geohash, writing once, serving many, these ideas aren’t new, but putting them together for this use case felt like the right tool meeting the right shape of problem. And maybe that’s the real outcome here: not just speed or throughput, but clarity. A simple build step. A direct read path. No surprises during lookup. Geocoding, like many things in logistics, tends to disappear into the background when it works. But underneath that smoothness, there’s room for good engineering even at the level of byte layouts and prefix strings. This is the kind of work I enjoy: sharp edges, measurable results, and a clear sense of *done*. *Cheers*! This article was originally published here: [https://levelup.gitconnected.com/how-i-built-a-high-performance-geocoding-engine-from-scratch-17449df01315](https://levelup.gitconnected.com/how-i-built-a-high-performance-geocoding-engine-from-scratch-17449df01315?ref=ricofritzsche.me) ### Road Networks Explained: Turning Geography into a Navigable Graph URL: https://ricofritzsche.me/road-networks-explained-turning-geography-into-a-navigable-graph/ Last updated: 2025-08-29T13:09:30.000Z When you open a navigation app, type in a destination, and receive a turn-by-turn route in less than a second, it feels like magic. But behind that instant result lies a carefully built model of the world’s roads. Road networks are not just lines drawn on maps. They are structured, rule-bound graphs of intersections, road segments, and constraints. They capture not only where roads go but also how traffic is allowed to flow through them. This article takes a deep look at how road networks are constructed as data models, how they are enriched with rules and restrictions, and how they become routable. Let's start with the basics, such as vertices and edges, before moving on to the algorithms and enhancements that enable navigation. The aim is to explain what happens between asking for directions and the route appearing on your phone. ## From Geography to Graphs The foundation of a routable road network is a graph. If you remember graph theory from school, it’s the same idea: vertices (nodes) connected by edges (links). In the context of roads, this abstraction works perfectly. - **Vertices (nodes):** These represent points where decisions are made: intersections, road ends, highway exits, roundabouts, or even ferry terminals. Any place where you can enter or leave a road becomes a vertex in the network. - **Edges (links):** These are the road segments between vertices. An edge carries the essential attributes of that piece of road: its length, its geometry, the speed limit, the number of lanes, or its surface type. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/08/Screenshot-2025-08-29-at-14.19.40.png) Graph Basics (Vertices + Edges) By breaking down a road system into vertices and edges, we can model a city, a region, or even an entire continent in a consistent mathematical structure. A street map with thousands of intersections and segments becomes a navigable graph of nodes and lines. But edges and vertices alone are not enough. Real-world driving comes with rules. To make the network useful, each element must carry attributes that reflect those rules. ## Attributes That Define Movement Each edge in the road network needs more than a shape. It must encode the conditions under which vehicles can travel along it. 1. **Directionality.** Some roads are one-way. In graph terms, that means the edge exists only in one direction. From A to B, the edge is valid. From B to A, it is not. Navigation software must respect this or it would happily send you the wrong way down a one-way street. 2. **Weights.** Routing depends on comparing alternatives. Each edge is assigned a cost, also called a weight. That cost might be distance in meters, expected travel time in seconds, or a more complex combination that reflects tolls, fuel use, or reliability. The weight is what routing algorithms minimize when they search for the best path. 3. **Hierarchy.** Road classifications influence preferences. A navigation engine usually favors arterial roads and highways over side streets. The hierarchy is encoded as attributes so that routing can prioritize fast, reliable roads while still using local streets where needed. 4. **Geometry.** An edge is not a straight line mathematically. For map display and instructions, each edge carries its polyline geometry—the sequence of coordinates that shows the curve of the road. Without geometry, routing could compute paths but could not produce realistic maps or turn-by-turn guidance. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/08/Screenshot-2025-08-29-at-14.22.34.png) Edge Attributes (Directionality + Weights) These attributes transform a simple graph into a model of real-world driving. They ensure the network reflects not only where roads are, but how they are used. ## Modeling Constraints: Barriers and Turn Restrictions Drivers know the frustration of seeing "No left turn" signs or encountering a gate that blocks a road. For a network to be truly routable, it must model these constraints. - **Barriers.** Some vertices are blocked: a bollard in a pedestrian zone, a gate to a private property, or a barrier that allows bicycles but not cars. These nodes effectively stop traversal. In the network, such vertices either connect to no edges for that vehicle type or carry infinite cost. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/08/Screenshot-2025-08-29-at-14.23.50.png) Barrier Example - **Turn restrictions.** At intersections, not every movement is allowed. You may be able to turn right or go straight, but not left. Or U-turns might be forbidden. In graph terms, this means that not every pair of incoming and outgoing edges at a vertex forms a valid connection. Routing engines must check turn restrictions whenever they compute paths. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/08/Screenshot-2025-08-29-at-14.44.21.png) Turn Restriction - **Access rules.** Some edges are available only to certain users. Bus lanes, tram tracks, pedestrian streets, and restricted industrial roads all fall into this category. These are either removed from the graph for unauthorized modes or given prohibitively high costs. - **Conditional restrictions.** Rules can change over time. Some roads are closed at night, some turns are restricted during rush hour, and some mountain passes are open only seasonally. This adds a temporal dimension to the network. A “valid edge” is not absolute but conditional on time of day or season. Encoding these constraints is what separates a naive graph from a routable one. Without them, algorithms might suggest impossible or illegal routes. ## Making the Network Routable: Algorithms Once the road network is represented as a graph with attributes and constraints, routing algorithms can come into play. 1. **Dijkstra’s Algorithm.** The classic shortest-path algorithm systematically explores edges from the start vertex, always expanding the lowest-cost option until the destination is reached. It guarantees the optimal route but can be slow on very large networks. 2. **A\* Search.** An enhancement to Dijkstra, A\* adds a heuristic (usually straight-line distance) that guides the search toward the destination. This reduces the number of nodes explored and speeds up results. 3. **Advanced techniques.** For networks the size of Europe or North America, even A\* can be too slow if done naively. That’s why modern engines use preprocessing techniques like Contraction Hierarchies or Multi-Level Dijkstra. These methods add shortcut edges or partition the graph to reduce the search space. The result: continental-scale routing in milliseconds. These algorithms are the beating heart of navigation software. But they can only work efficiently if the underlying network is carefully prepared. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/08/Screenshot-2025-08-29-at-14.26.01.png) ## Beyond the Basics: Enhancements A truly useful road network model goes beyond static graphs. Several enhancements make routing realistic and responsive. - **Traffic data.** Real-time or historical traffic conditions adjust edge weights dynamically. A highway segment that normally takes 30 seconds might take 90 during rush hour. Routing engines integrate traffic feeds to recalculate travel times on the fly. - **Turn costs.** Not all turns are equal. A left turn across heavy traffic is legal but slow. Engines assign penalties to such maneuvers, making the chosen route more realistic. - **Ferries and multimodal links.** Edges don’t always represent roads. They can also represent ferry crossings, tunnels, or transfers to trains or subways. Multimodal routing depends on treating all such connections as edges in the same graph. - **Safety and accessibility.** Some networks include additional attributes: accident risk, road lighting, steep gradients, or wheelchair accessibility. These allow specialized routing for different user groups. With these layers, a network becomes not just a static representation of roads but a **digital twin of mobility**, reflecting the conditions drivers, cyclists, or pedestrians actually experience. ## From Graph to Directions The final step is turning a computed path into human-friendly guidance. This requires more than just knowing which edges form the route. 1. **Map matching.** The system maps your GPS position to the nearest edge in the graph. This is harder than it sounds because GPS is imprecise, and multiple roads may be close. Sophisticated map-matching algorithms decide which road you’re actually on. 2. **Path computation.** The algorithm finds the optimal path, respecting directionality, weights, and restrictions. 3. **Instruction generation.** The route is converted into instructions like *"Turn left onto Main Street in 200 meters"*. This involves analyzing edge geometries and attributes, grouping small edges, and choosing the right level of detail. 4. **Presentation.** The polyline geometries of the edges are stitched together and displayed on the map, with highlighted turns and labels. This pipeline from GPS input to rendered map and spoken instructions happens in fractions of a second, thanks to the underlying graph model. ## Beyond Maps: Routability as the Hidden Backbone of Mobility For developers, product owners, logistics managers, and CTOs, the idea of a routable road network is far from academic. It explains why navigation sometimes produces routes that feel counterintuitive: the cause is often not the algorithm itself but the data behind it. An unmapped turn restriction, a misplaced barrier, or an incorrect one-way attribute can send vehicles along detours that make little sense on the ground. It also reveals where genuine innovation is happening. Faster algorithms, the integration of real-time traffic, and the use of AI for predicting travel times all build on the same underlying model of nodes, edges, and constraints. Once you understand this foundation, it becomes easier to separate true progress from cosmetic improvements. Routability also broadens the horizon of what is possible. A well-structured network is not limited to cars: with the right attributes it can guide bicycles through safe corridors, optimize routes for trucks with size and weight restrictions, assist emergency services where seconds matter, or ensure accessibility for pedestrians. What changes is not the model but the richness of the data that feeds it. Perhaps the most important lesson is that routing accuracy depends less on sophisticated algorithms than on the quality of the underlying data. Completeness, consistency, and timely updates matter more than clever tricks in code. Every error or omission in the network, whether a missing restriction or an outdated classification, translates directly into inefficiency, higher costs, or even safety risks. Seen this way, routability is not an abstract construct. It is the hidden backbone of modern mobility, shaping the reliability of logistics operations, the efficiency of transport systems, and the everyday experience of millions of travelers. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/08/Screenshot-2025-08-29-at-14.49.52.png) Delivery blocked by barrier unless routing accounts for access rules. ## Looking Ahead As road networks become smarter, the graph model will continue to evolve. Real-time sensors, connected vehicles, and AI will add layers of dynamic information: live congestion data, accident detection, even predictive travel times. The fundamental model of edges, vertices, and restrictions will remain. But it will be enriched by continuous streams of real-world data. In the future, the line between "map" and "traffic system" will blur. Road networks will not only represent the world; they will interact with it, receiving data from vehicles and sending guidance back in real time. For now, the essential lesson is simple: behind every route suggestion is a graph, carefully built from road segments, intersections, and rules. What seems like magic is really the power of graph theory applied to geography. ## Conclusion A routable road network is not a picture. It is a structured, rule-based model of mobility. It starts with vertices and edges, grows richer with attributes and constraints, and becomes usable through algorithms that compute optimal paths. Barriers, turn restrictions, and access rules make it realistic. Responsiveness is achieved through enhancements such as traffic data and turn costs. By understanding this, we can better appreciate the hidden complexity in everyday navigation. And for those building products or managing logistics, we can see where improvements matter most: high-quality data, accurate rules, and efficient routing engines. Every time a driver gets a clear set of directions, the system has solved a complex graph problem in milliseconds. That is how a road network becomes routable, and why it is one of the most fascinating examples of applied computing in daily life. *Cheers*! ### Beyond Static Models: Behavior-First Design for Changing Domains URL: https://ricofritzsche.me/beyond-static-models-behavior-first-design-for-changing-domains/ Last updated: 2025-08-21T09:37:47.000Z I want to share some thoughts that emerged from the discussions around my recent articles over the past few weeks. > *“I don't quite follow the ‘behavior first’, ‘event sourcing fits everywhere’ perspective. Honestly, you are using quite a large brush and generalizing way too much.”* That pushback is healthy. “Events everywhere” becomes a mantra if it’s not anchored in purpose. The goal is not to sprinkle events over code and declare victory. The goal is to build systems that **explain themselves,** systems where decisions, constraints, and outcomes remain visible long after the current state has changed. This article unpacks what **behavior-first** actually means, contrasts it with **structure-first** modeling, and shows how to handle very practical needs like access control, interdependent fields, and analytical reporting without collapsing back into a monolithic “proper data model” that erases time and intent. The position is pragmatic: Event Sourcing is a tool; behavior-first is a mindset. Both are useful when they illuminate causality and make change safer. Neither is pixie dust. ## The Misread: Behavior-First ≠ Events-Everywhere > *“Events are not pixie dust. You can take any backend API and trivially make it event driven… What kind of value are you providing with that, though?”* Agreed. Turning method calls into “events” on a bus adds ceremony without adding meaning. Behavior-first is not about shoving messages through a queue. It’s a shift in **what the system chooses to remember**. - **Structure-first** systems center on *what is*: the latest shape of data in tables or objects. - **Behavior-first** systems center on *what happened*: the sequence of decisions and the facts that followed. Events are the record of those decisions and facts. They exist to carry intent (“what was attempted, under which rule?”) and outcome (“what changed, with which consequences?”). Whether that record lives in an append-only event store, an audit log, or a hybrid depends on context. The value comes from the narrative the system can tell (the chain from intent to effect) not from the transport mechanism. ## The trap of “proper” models > “Events are not pixie dust. You can take any backend API and trivially make it event driven: any method call becomes an event and every response goes back to the same bus. What kind of value are you providing with that, though?” Turning method calls into “events” does not create value. Value appears when a system keeps the intent and decision that created state, so it can evolve without surgery. “Proper data models” are great for well-understood, stable domains. They are not great during discovery. They push teams to decide too much up front. They entangle rules, permissions, and reporting into one central shape. They create migration work before learning has even started. **Quick distinction: event-sourced ≠ event-driven** - Event-sourced is a persistence pattern. The source of truth is an append-only log of domain events. State is derived via projections or snapshots. It is about keeping time and decisions. - Event-driven is an integration style. Components communicate via messages (events, commands) over a broker to decouple and scale. It is about coordination between parts. You can be event-driven without event sourcing (publish messages, store only snapshots). You can be event-sourced without being event-driven (single service, no broker, projections in the same process). Conflating them leads to “eventifying” RPC or pushing bus messages into places where a clear decision log would suffice. **Behavior-first flips the order:** 1. Start with a capability expressed as a command. 2. Encode the rules that make the decision. 3. Record the fact of what happened. 4. Project state into whatever tables or documents are needed right now. 5. Add more verbs and views as the domain clarifies. This delivers business value immediately and keeps options open. ## The Project Management Example, Reworked > *“Let’s consider a project management system.”* > *“What happened? We created a project, with 62 fields.”* > *“Why did it happen? Because user wanted a new project.”* > *“What was the intent? Creating a project.”* > *“Consequences? New project exists, and now has to appear in 76 reports each with 23–42 columns.”* This is an excellent example because this is the big-upfront-schema mindset. It assumes the world on day one is known, stable, and reportable in all the ways stakeholders might ask for later. Reality disagrees. Here’s a behavior-first reconstruction that keeps business value front and center. - **Command**: OpenProject (initiator, provisional\_name) Decision surface: “Is this user allowed to open projects?” That is it. No contracts. No budgets. No cross-field constraints. Not in v0. - **Event**: ProjectOpened (project\_id, initiator, at, provisional\_name) The fact recorded is minimal and honest. The project is open. Future actions can enrich it. - **Projection**: CurrentProjects (project\_id, name, opened\_by, opened\_at) This is the list screen product needs tomorrow. It exists in an operational table for fast reads. It can be rebuilt any time from events. That is a complete end-to-end slice. It is shippable. It unlocks feedback. It does not require a complete model. It gives a safe path to add the rest. ## Evolve with verbs, not fields When new knowledge arrives, add verbs that express an actual decision, not just a schema change: - **RenameProject** → ProjectRenamed (name\_before, name\_after, reason) - **AttachContract** → ContractAttached (contract\_ref, version, hash) - **AllocateBudget** → BudgetAllocated (amount, currency, source) - **SetAccessPolicy** → AccessPolicySet (rules) - **DefineMilestone** → MilestoneDefined (date, scope) - **ApproveMilestone** → MilestoneApproved (approver, at) Each verb has a **decision surface**: the minimal facts required to say yes or no. Keep the surface small. Keep rules explicit and close to the command. Test them as pure logic. State is still derived. The “project record” in a table becomes a **projection of decisions**. It looks like a familiar row, but it is not the source of truth and it does not need to be complete to be useful. ## Interdependent fields live in rules, not tables > “Very often, these systems need detailed content (what’s in the contract, interdependent fields).” Interdependent fields signal policy, not structure. Place the policy on the command path: - “A project with a termination date must include a termination clause.” Checked when attaching a contract or setting dates. - “Budget increases above threshold require a role with approval rights.” Checked when allocating budget. - “Start date cannot move before the earliest approved milestone.” Checked when redefining milestones. No schema migration is required to change a rule. Future decisions follow the new rule. Past decisions remain truthful. This is agility in practice, not in slide decks. ## Access control as a capability, not boilerplate > “Actual access control (who can do what).” Treat access control as a **first-class capability**: - Commands carry who attempts the action. - A policy layer decides whether that actor may do it, given current projections. - Access changes are verbs too: *SetAccessPolicy*, *GrantRole*, *RevokeRole*. For read-time checks, maintain a tiny projection of effective permissions. It is a small table keyed by principal and resource that answers “can X do Y here” in O(1). It is derived from *AccessPolicySet* and similar events. It evolves as the org chart and rules evolve. ## Reporting without the 76-report trap > “Analytical reporting (pieces of contract data end up in various places). > Consequences? New project exists, and now has to appear in 76 reports each with 23–42 columns.” Reports multiply when the central model tries to serve everyone. Behavior-first narrows the blast radius: - Create **report-specific projections** that flatten exactly what a report needs. - Emit them to a warehouse or keep them local. - Refresh on the cadence the report requires. - When definitions change, **replay** to rebuild from the same history. No master table has to bend to 76 shapes. Each report gets its own small, proper model. Adding a new one is a matter of projecting known facts, not negotiating schema politics. ## The agility you actually feel Behavior-first gives day-to-day agility in concrete ways: - **Start now** with one verb. No “schema committee.” - **Deliver thin vertical slices** that include a decision, a fact, and a small view. - **Refactor rules** without rewriting history. - **Change read shapes** without rewriting writes. - **Onboard new consumers** by adding projections, not touching the core. You ship earlier. You decide later. You keep options open. ## Addressing the core objection > “How are you going to make this without a proper data model?” By **deriving** proper models per purpose instead of pretending one model can serve all purposes. - Operational screens read from an operational projection. - Permissions read from a permissions projection. - BI reads from report projections. - History is always available because decisions were recorded, not only their end state. There is still modeling here, but it is **local, small, and reversible**. It happens when needed, not months in advance. ## A short, practical playbook This helped me a lot when building real-world systems. 1. **Name the first three verbs** that unblock value. Example: *OpenProject, RenameProject, AttachContract.* 2. **Write the decision rules** as pure logic. Keep each rule visible and testable. 3. **Record the facts** of accepted decisions. Keep events small and meaningful. 4. **Project the smallest useful view** for the UI. A table with five columns is fine. 5. **Add one policy** on the command path (permissions or a simple invariant). 6. **Deploy**. Collect feedback on vocabulary and flow. 7. **Add the next verb**. Let verbs evolve the language of the system. 8. **Introduce report projections** only when a report is real and needed. 9. **Refactor projections freely**; they are caches of answers, not the truth. 10. **Measure** where lag matters; keep critical reads synchronous if necessary. This is the opposite of **big-upfront** anything. It is also the opposite of chaos. Decisions stay explicit. Effects stay traceable. Structure stays light. ## What to avoid (and why) - **Eventifying RPC**: renaming function calls to “events” without modeling decisions. No value here. - **Snapshot events**: recording “ProjectUpdated” with the whole blob every time. Save the change and the reason, not the object. - **One “domain model” to rule them all**: forcing UI, security, and BI to share a single table set. That is where agility dies. - **Leaking reads into writes**: deciding on a command based on convenience fields meant for UI. Keep the decision surface minimal and principled. ## Where audit fits (and why it is not the point) A clean audit trail falls out for free when decisions are recorded. That can be useful. But: **It is NOT the goal.** The goal is **ability to change direction** without breaking what shipped yesterday. Behavior-first gives that by separating concerns along natural seams: verbs and rules on the write side, shapes and speed on the read side. ## Back to the critic, one more time > “Events are not pixie dust… When does it stop being a self-indulging exercise and provides either business value or some kind of implementation quality?” It stops being indulgent the moment a team ships *OpenProject* tomorrow instead of debating a 62-field record and 76 reports. It becomes valuable when the next change is a new verb and a small projection, not a migration and a freeze. It improves implementation quality because rules read like rules, not like triggers hidden in a schema. > “Very often, one needs just a basic audit history, if any.” Sometimes yes. Then just do that. Behavior-first doesn’t force Event Sourcing on every form. It asks for **verbs before fields** and **facts before snapshots** so the design can move as fast as the business. > “How are you going to make this without a proper data model?” By recognizing that “proper” is contextual. Keep the core as **decisions and outcomes**. Let each consumer have the **proper** model it needs, projected from the same truth. ## Closing: True agility is verbs first Big-upfront models feel solid until change arrives. Then they become anchors. Real agility looks smaller and moves faster: - Start with one verb that matters. - Decide with minimal context. - Record what happened. - Project only what you need. - Grow the language of verbs as you learn. If the first feature of your project management system is ***OpenProject***, you can ship in days. If the first feature is a “proper” 62-field schema and 76 reports, you can ship in quarters. Pick the path that leaves room to learn. *Cheers*! ### Beyond Aggregates: Correlation ≠ Shared State URL: https://ricofritzsche.me/beyond-aggregates-correlation-shared-state/ Last updated: 2025-08-13T06:33:34.000Z Greg Young recently asked me a fair question: > “Have you noticed that the events you depend on tend to correlate? For example, both check-in and check-out operations for inventory care about the same set of past events – check-ins, check-outs, audit changes, and metadata like max quantity. So why model them separately?” The observation is right. The usual conclusion is not. When we see correlation, the reflex is to centralize: wrap logic into one model, version it, and call it an aggregate. That move hides decisions behind abstractions, couples unrelated rules, and turns change into a negotiation with shared state. Yes, events correlate. No, that does not mean the logic belongs together. What correlation actually tells us is which **facts** are relevant to a decision**, not** that the decision should share a permanent model. ## Correlation Implies Centralization In traditional domain modeling, especially under the influence of Domain-Driven Design (DDD), event correlation is treated as a modeling constraint. If multiple commands depend on the same set of events, they’re assumed to operate on the same aggregate. And since aggregates are meant to guard invariants and encapsulate consistency boundaries, the logic must be grouped and versioned together. This seems rational until you look closer. Take Greg’s example: both CheckInInventory and CheckOutInventory need access to prior check-ins, check-outs, audits, and item metadata. The conventional move is to define an InventoryItem aggregate. It’s responsible for applying these commands, validating business rules, and emitting the resulting events. Everything flows through that one object. But that aggregate is now doing too much in my experience. It contains the rules for multiple decisions. It’s responsible for maintaining state that might only be relevant in some contexts. And worst of all, it introduces false coupling between commands that could have been entirely independent – just because they read from overlapping history. The result isn’t a clear boundary; it’s a choke point. Every change becomes harder. Every new behavior needs to be threaded through the same object, whether it fits there or not. The root issue is this assumption: if events correlate, the logic must too. But that’s not a law; it’s an inherited habit from common mainstream object-oriented programming, where behaviour is assumed to ‘belong’ to state of an object cluster. ## Context Over Structure Instead of building a shared object, let each **command define its own context**. - **CheckInInventory** queries the events it needs (recent check-outs, current quantity, audits), evaluates its rules, and emits its events. - **CheckOutInventory** does the same, its own read, its own rules, its own result. The overlap is a **fact of the domain**, not a reason to merge behavior. We protect **causal relevance**, not a shared state object. I call this **Command Context Consistency**: decide from the facts that matter to *this* command, and verify that those facts haven’t changed when you append. What you don’t need: an aggregate, object lifecycles, or a central “apply” method. ## Why This Changes Everything When you stop modeling correlation as shared structure, three important things happen. ### Behavior becomes explicit You no longer guess why a command produced an error or made a decision. The full input is right there: the event context it queried, and the logic it applied. Debugging becomes inspection, not archaeology. You also stop smuggling logic into abstractions like “apply” or “validate.” You write actual decisions, in terms of actual events. ### Change becomes localized Want to add a new validation to CheckOutInventory? Do it right there. You don’t risk breaking CheckInInventory, even if it reads similar events. That’s because nothing is centralized. You’re not editing a shared object. You are editing logic of one command. The context is narrow. The impact is isolated. And you don’t need to refactor a core abstraction to support a corner case. ### Race conditions disappear, or become manageable In an aggregate-based model, all commands contend for the same version. But here, CheckInInventory only fails if the specific events it read have changed. That’s a finer-grained consistency boundary. It’s based on **causal relevance**, not shared state. This doesn’t just improve throughput. It aligns consistency with meaning: commands fail when it makes sense for them to fail, not just because they bumped into another write. ## How We Got Here: From Event Sourcing 2010 to 2025 Event sourcing didn’t begin incorrectly; it simply began in a different world. In the early 2010s, most event-sourced systems were built by people coming from object-oriented backgrounds. The aggregate was the default unit of modeling, and Event Sourcing became a way to persist those aggregates by replaying and applying events instead of loading from a database row. This is how I’ve worked on many projects over many years. The structure looked like this: ![](https://miro.medium.com/v2/resize:fit:1400/1*iXN2UAa07ouQeMvnUM9plQ@2x.jpeg) Everything revolved around the aggregate. Even though the system was event-sourced, the mindset was still state-based. Commands were routed to objects. Consistency was tied to object versioning. Events were side effects of applying logic to an in-memory state object. What we’ve learned since then – especially in the last years – is that this model carries more friction than benefit: - Aggregates obscure why a decision was made. They collapse context into opaque state. - Versioning leads to unnecessary conflicts – commands fail just because they touched the same object, not because they violated a rule. - Behavior changes become harder over time, because the logic is scattered across lifecycle methods and shared abstractions. My model today looks different: ![](https://miro.medium.com/v2/resize:fit:1400/1*FQJTN_zd-P57FbjAnM5ieA@2x.jpeg) There is no aggregate, no central model, and no object lifecycle to manage. Each command lives in its own world, with a clear input (event context) and a clear output (resulting events). Consistency is enforced not via a version number, but by verifying that the context is still valid at append time. This shift from object state to event context is what enables true agility. You’re no longer building around long-lived structures. You’re designing short-lived, focused decisions that reflect the current rules of the business. And when those rules change, the change is local, explicit, and safe. ## Conclusion: Let Correlation Be a Clue, Not a Constraint Yes, events correlate. Commands often look at overlapping slices of history. But this doesn’t mean the logic belongs together. It doesn’t justify a shared model. It doesn’t require an aggregate. What it tells us is that certain facts in the system are causally relevant across multiple decisions. That’s not a signal to centralize – it’s a sign that the domain has stable backbones of meaning. You don’t need to wrap that into an abstraction. You just need to expose the facts and let each command evaluate what matters. For many years, I followed the typical object-oriented path. I applied DDDs tactical patterns, encapsulated state transitions, all of it. I tried to do it well, and in many ways, it worked. Eventually, the model started pushing back. I had to bend it to support new rules. Logic leaked. Everything slowed down. That’s when I began a process I can only describe as **deprogramming myself from object oriented thinking.** Questioning the assumptions I never thought to challenge. Peeling away abstractions I once defended. And finding simpler models underneath – more direct, more explicit, and more aligned with how decisions actually happen. That’s where this aggregateless approach ([AES](https://ricofritzsche.me/aggregateless-event-sourcing/)) came from – **not from theory, but from years of friction**. From watching things break in production. From debugging systems I had designed myself. So when someone points out that “events correlate“, I agree. But I don’t follow it with “therefore, we need an aggregate.” I follow it with: **„Good! Now we know which events matter“.** *Cheers*! This article was originally published on [Medium](https://blog.ricofritzsche.de/beyond-aggregates-why-correlated-events-dont-mean-shared-state-0228a08a6709?ref=ricofritzsche.me). ### Event Stores and Tags: A Misunderstood Optimization URL: https://ricofritzsche.me/event-stores-and-tags-a-misunderstood-optimization/ Last updated: 2025-07-31T06:32:01.000Z ### Tags in event stores sound helpful, even obvious. They promise easier correlation, cleaner queries, and clearer intent. Instead of digging into payloads, you just filter by labels. The event type tells you **what** happened; the tag tells you *to *whom**. That is the argument made in [this article on DCB](https://dcb.events/topics/tags/?ref=ricofritzsche.me) (Dynamic Consistency Boundary), which presents tagging as a pragmatic enhancement to event-sourced systems. I used to see it the same way. My stance was: *you can use tags, but you don't have to*. Ralf Westphal challenged that. Repeatedly. And to be honest, I did not fully appreciate the depth of his point, until now. > Tags add nothing to the concept of an event store. Tags make it harder to understand and more difficult to implement. [Tags for Event Stores? | Ralf WestphalTags for Event Stores? I understand the motivation, but I still disagree. This sounds very reasonable: 1\. “Tags help correlate events with specific instances in the domain” 2\. “While an event type tells us what happened, tags tell us to whom or to what it happened.” But then... I cannot shed the feeling it’s excessive. It’s more than needed. It’s diluting the conceptional beauty of Event Sourcing. As for 1: What are “instances in the domain”? Something is referred to without explaining it. I would have understood, if there was a need to tie together events, to related events to each other. But what’s this ominous stuff in the domain? (Of course I am playing dumb here. I know what’s meant. But to me the phrasing is a petitio principi.) To me it’s the other way around: “instances” of whatever can be constructed at any time from events as needed as an act of creation. It’s a matter of need and imagination. To make the event payload opaque would hinder that. To give preference to something like tags would hinder that. As for 2: “to whom or what it happened” again is referring to something else. An “instance in the domain”, probably. But do you see that in nature? Do events come with a label? Or do we construct labels from events? I believe the latter is the case. That’s what the brain is doing all day long: processing raw events, correlating them (in time and space), looking for patterns, and then constructing “regions of stability” from them. Objects are abstractions. We’d rob the users of Event Stores of the ability to do that by making the payload opaque and limiting them to correlation by preconceived tags. We are talking about Event Stores. Events are more than blobs of data. Otherwise we’d be talking about blob stores.😉 That means to me it’s conceptually sounds to separate event types (why) and associated partial application state aka data aka payload (what). Opacity of payload (black box) to me is not part of this concept (black). Defining event types or payload structures up-front (like a database schema) on the other hand would be in opposition to the concept (white). So I don’t think a faithful implementation should be black or white - but grey. That means: the Event Store knows the meta-schema of payloads, for example that it’s always JSON. With this knowledge an Event Store supports (re)construction of whatever consumers of events see fit. It allows selection of events according to arbitrary patterns. That, to me, is perfectly in line with the original concept. This also easily supports all sorts of “ES fads”😉: - opaque payloads: { dataUrl: “https://...” } - streams: { streamID: ”...”, data: { ... } } - tags: { data: { ... }, tags: \[ {customer: “alice-smith”} \] } Grey payloads are highly flexible, conceptionally parsimonious, fast enough until proven (!) otherwise for concrete use cases. Tags are adding nothing to the concept of an Event Store. Tags are making it harder to understand, and more difficult to implement.![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/icon/al2o9zrvru7aqj8e1x2rzsrca-2)LinkedInRalf Westphal![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/thumbnail/c45fy346jw096z9pbphyyhdz7)](https://www.linkedin.com/posts/ralf-westphal%5Ftags-the-key-to-flexible-event-correlation-activity-7356227965350768641-nT5%5F?utm%5Fsource=share&utm%5Fmedium=member%5Fdesktop&rcm=ACoAAAs0DNgBz9UpVAhM0J0p-Cfziy7deWaQsFk) He was right. What *tags* introduce is a premature structure. They assume that correlation must be declared *at write time*. That events must carry external identifiers describing their "subject". Although it sounds efficient, it quietly undermines the very basis of event sourcing. ### Event Sourcing Is About Deferring Meaning The strength of event sourcing is that events do not require immediate interpretation. They simply record what happened, allowing meaning to emerge later depending on context and intent. Events do not declare what they mean; they become meaningful through interpretation. Event logs are not spreadsheets. They are streams of recorded facts, waiting to be understood from different perspectives, at different times, by different consumers. This article is not an attack on tags as metadata. It's a critique of tagging as a **core** feature of event stores because that crosses a line: It introduces a black-and-white mindset into a system that thrives in shades of grey. ## The Tag Temptation Tags are seductive. They promise clarity and convenience. Add a "*customer: alice-smith*" or "*order: 12345*" to your event, and correlation becomes simple. It feels like good housekeeping like documenting what the event is *about*. But it's a shortcut with a cost. In the DCB article, tags are said to correlate events with “instances in the domain.” That wording carries an assumption: that such instances already exist, independently of the events. That they are real, stable things the system should recognize and tag. But that's not how event-sourced systems work. "Instances" (customers, deliveries, projects) are not predefined. They are retrospective constructs, shaped by event interpretation. They are derived from patterns and context but not from tags. By tagging events, we are no longer just annotating. We are declaring. We are baking one interpretation into the storage layer, saying: *this event happened to Alice*, even though “Alice” may not exist yet, or that meaning might change later. Worse, this shifts focus away from *what happened* toward metadata about *what we think it meant*. **That's a shift from event-first to structure-first thinking. It's reversing causality.** And once tags become the primary mechanism for querying or grouping events, they stop being metadata, and start being structural. At that point: We are no longer sourcing events. We are indexing snapshots. That's database thinking. ## Black, White, Grey When it comes to event modeling, two extremes are easy to recognize: - **Black box**: Events are opaque blobs. They're appendable and replayable but you can't inspect or query them. - **White box**: Events follow rigid schemas. They're tightly versioned, fully structured, and easily queryable but inflexible. Both are wrong. The sweet spot is **grey**. Events that are introspectable, typically JSON, but without fixed contracts. No enforced schemas, no premature structure. Just enough form to support interpretation, but never to prescribe it. As Ralf puts it: > *"Grey payloads are highly flexible, conceptually parsimonious, fast enough until proven (!) otherwise."* Tags pull you out of grey. They impose a fixed structure for correlation. They encode assumptions about identity, ownership, and domain semantics into every write. They hardcode a worldview that might not hold tomorrow. ## Tags Are Not Free At first glance, tags seem harmless. Just a few fields to help with querying. But here's what they really do. Tags must be attached at write time. That means the writer needs to *know* the correct subject of an event in advance. Which identity? Which domain concept? According to which rule? You're committing to an interpretation *before* you've even seen what happens next. That's not just brittle; it's also backwards in terms of how we know things. Tags are informal contracts. A tag like *order\_id = 123* or *customer = alice-smith* assumes those concepts are universal and stable. Over time, they become required conventions. Queries start to rely on them. If they’re missing or change format, things break. It's schema drift just without migrations. To make tags useful, you need to index them. That means query paths, maintenance overhead, consistency rules. All of this, just to query facts that are *already* in the payload. Why add a second layer of structure, when the payload already contains the truth? Why trust a label, when you can inspect the fact? **A Note on Performance** Performance matters deeply. But it must be approached with precision, not assumption. Premature optimizations like tagging for correlation often solve unproven problems while introducing hidden complexity. If your system needs indexing, prove it with real workloads and real bottlenecks. Until then, favor clarity over convenience. In most systems, a JSON-based event store with introspectable payloads and projection-driven queries is *fast enough* until it's measurably not. That's when you optimize. Not before. ## Want Tags? Model Them Let's be clear: correlation is important. But the right place to model correlation is not in a metadata side-channel. It's in the stream itself. If tagging matters in your domain, make it explicit. Treat it as behavior. Record it as an event. Instead of tagging "*customer: alice-smith*", record: CustomerWasTagged *{ customerId: "alice-smith", tag: "vip" }.* Now that information is part of the log: versioned, auditable, replayable. It reflects a real action, not an implicit assumption. You can track when a correlation was made, by whom, and in response to what. This approach keeps the event store honest. It avoids structural assumptions. It respects causality. And it aligns with a foundational truth of event-sourced systems: **Principle of the Event Producer** The producer of events does not care about — nor take care of — event consumers. Its sole responsibility is to faithfully record what happened. In that, the producer must be ego-less. It must not encode interpretation into the event. That’s difficult because decisions must still be made: when to record, what to record, at what granularity. But the goal is always the same: capture what happened, without collapsing it into what we think it means. When producers start embedding their own interpretations (*whether through tags, stream IDs, or inferred identities*) they close off future possibilities. They decide too early what matters, and in doing so, they let potential information fall through the cracks. That's why event sourcing works best when producers stay impartial and let meaning emerge later, when it's actually needed. ## Let Event Stores Be Event Stores An event store is not a document store. Not a blob store. Not a read-optimized table. It is a log of what happened in the order it happened with no declared meaning beyond the facts it records. The moment you start injecting tags, identities, and labels, you are no longer modeling behavior. You are modeling structure. You are turning facts into opinions. You are asking the store to behave like a relational index, and that's not its job. Let the event store be the source of truth and not the source of structure. Let correlation happen downstream. Let meaning emerge. Let consumers decide. That’s the whole point: The job of the event store is simple, capture what happened. Faithfully. Durably. Transparently. Everything else is interpretation. *Cheers*! ### How to Enforce Consistency Without Aggregates in Event-Sourced Systems URL: https://ricofritzsche.me/how-to-enforce-consistency-without-aggregates-in-event-sourced-systems/ Last updated: 2025-07-29T08:11:35.000Z Consistency is essential. However, the way in which it is enforced is more important than most developers realize. In traditional event-sourced systems, consistency is tied to the aggregate. You replay past events, to restore the aggregate state, make a decision and then try to append new events, but only if the version of the aggregate hasn’t changed. This approach is safe, but also coarse. It assumes that any change to the aggregate renders every command unsafe. Today, however, I think differently. I’ve shifted the consistency boundary to the command context. I no longer assume that a central state object needs to enforce consistency. Instead, I now enforce consistency per command, relative to the event context that the command actually reads. This might sound like a small difference, but it fundamentally changes how you design and evolve behavior. You stop coupling unrelated commands just because they operate on the same entity. You gain flexibility, clarity and fewer unnecessary conflicts. In this article, I’ll explain the technicalities step by step. This will include the exact interface, logic and SQL query that we use in our PostgreSQL event store implementation. ## How Aggregate Versioning Works (And Why It’s Too Broad) In most event-sourced systems, consistency is enforced using aggregate versioning. The idea is simple: every time you apply a command, you load the aggregate’s event history, make a decision, and try to append new events but only if no other events have been added in the meantime. This is usually implemented by tracking a version number (or the highest sequence number) of the last event. When appending, the system checks if that version still matches. If not, the command fails and must be retried. This works. But it’s a **coarse-grained guarantee**. Let’s say two commands both target the same *InventoryItem* aggregate: - *CheckOutInventory* relies on past check-ins and audits. - *RenameItem* changes the display name of the item. They don’t care about each other. But because they touch the same aggregate, they block each other. A rename operation that has nothing to do with stock levels can make a valid check-out fail. That’s not enforcing meaningful consistency. That’s just treating all change as equal regardless of whether it affects the decision being made. This kind of model introduces: - **False conflicts**, where unrelated commands fail due to irrelevant changes - **Unnecessary retries**, even if the business rule was still satisfied - **Coupling by identity**, where logic becomes entangled just because it's routed through the same aggregate It’s safe, but it’s also a bottleneck. But what's the solution? Shift the boundary. ## Command Context Consistency Instead of versioning the whole aggregate, we enforce consistency relative to the **event context** a command actually uses to make its decision. Each command defines a filter: a combination of event types and optionally payload criteria that describe the subset of events it cares about. This filter forms the *command context*. When the command is run, it queries the store for all matching events and reads the highest sequence number (*maxSequenceNumber*) found. This *maxSequenceNumber* is a version of the context, rather than of the entire entity. When the command emits new events, it includes the same filter and the *maxSequenceNumber* observed. The append will only succeed if the number hasn't changed, meaning that no relevant events have been added in the meantime. What’s different here? - If something changed **outside** the command’s context, the command proceeds. - If something changed **within** the context, the append fails, and the command must be retried using a fresh query and decision. This aligns consistency with causality: - You don’t block unrelated operations. - You only reject commands that might now produce a different result based on new facts. This is especially powerful in systems with broad event types and many loosely coupled rules. Instead of centralizing everything into one object, you let each decision stand on its own, based on the facts it actually needs. The result is leaner logic, fewer conflicts, and a clearer connection between what a command reads and what it protects. ## The Core Algorithm: Query + Append with Context Check At the heart of this model is a simple two-step process: 1. **Query the event store** using a command-specific filter 2. **Append new events** only if the highest sequence number of the context hasn’t changed This is the core idea behind command context consistency: the command operates within a filtered view of the event log, and appends are allowed only if that view remains unchanged between reading and writing. ### The Interface Our *EventStore* interface reflects this pattern directly: ```TypeScript export interface EventStore { query(filter: EventFilter): Promise; append( events: Event[], filter?: EventFilter, expectedMaxSequenceNumber?: number ): Promise; } ``` - *query()* loads all events matching a given *EventFilter* and returns both the events and the highest sequence number of the query context (*maxSequenceNumber*). - *append()* writes new events, but only if the *maxSequenceNumber* for the same filter still matches the expected value. This gives you **causal protection** per command. ### The Contract Let’s make this explicit: - A command queries the event store using a filter ***F*** - It receives a QueryResult, which includes a set of relevant past events and their maxSequenceNumber, which is ***N***. - The decision function runs based on those events and returns a list of new events. - When appending, we re-apply the **same filter *F,*** check whether the **current *maxSequenceNumber* is still *N ,*** append only if the condition holds. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/07/Screenshot-2025-07-28-at-18.26.10.png) Optimistic Concurrency Control Flow If the *maxSequenceNumber* has changed, the context is no longer valid. Something relevant has happened since the decision was made. The append is aborted and the command must be retried, starting with a new query and decision. This guarantees that every decision is made against the most recent context without assuming full control over an entire aggregate. ## A Concrete Example: Check-Out Inventory Without False Conflicts Imagine we’re developing a basic warehouse system. Each item can be checked in or out, renamed, or audited. All of these actions generate events that are stored in a single events table. Now consider the following command: *CheckOutInventory*. This command: - Reduces stock for an item - Fails if not enough stock is available To make this decision, it needs a very specific context: - Past *InventoryCheckedIn* events - Past *InventoryCheckedOut* events - Possibly *InventoryAudited* events (if used to correct quantity) ### The Query We use a filter like: ```json { eventTypes: ['InventoryCheckedIn', 'InventoryCheckedOut', 'InventoryAudited'], payloadPredicates: [{ itemId: 'abc-123' }] } ``` The event store returns all matching events and the highest sequence number in that set, which is *1842*. The decision is made based on this context. If it passes, we emit: ```json [ { eventType: 'InventoryCheckedOut', payload: { itemId: 'abc-123', quantity: 2 } } ] ``` We then append the new event using the same filter and the expected *maxSequenceNumber* (*1842*). Now, here’s the key part: #### Valid Case: Non-conflicting update The item was renamed (*ItemRenamed* event) at sequence number *1843*. This does not affect the outcome of the *CheckOutInventory* process. The context we care about remains unchanged. Append proceeds successfully. #### Invalid Case: Conflicting update In the meantime, someone checked out five units (*InventoryCheckedOut* at sequence number *1843*). The relevant context has changed. *Append* is aborted. The command must re-query and re-evaluate to produce a different result, if necessary. This is more than just an optimization. It’s a more precise model of causality. Only relevant changes result in retries. Commands protect the facts they are concerned with. There are no global locks or false conflicts. ## PostgreSQL Implementation To enforce this context-local consistency at the database level, we use a **single atomic SQL statement** that both: 1. Re-evaluates the current max(sequence\_number) of the context 2. Appends new events only if the context has not changed ### The *append* Operation Here’s how the core append logic works: ```TypeScript await eventStore.append(events, filter, expectedMaxSequenceNumber); ``` - *events*: The new events that persist after the decision function has run. - *filter*: The same event filter that defined the context of the command. - *expectedMaxSequenceNumber*: The value observed during the query phase. This combination ensures **only** relevant changes trigger a retry. ### How It’s Enforced in SQL We generate a query like this: ```ts export function buildCteInsertQuery(filter: EventFilter, expectedMaxSeq: number): { sql: string, params: unknown[] } { const contextVersionQueryConditions = buildContextVersionQuery(filter); const contextParamCount = contextVersionQueryConditions.params.length; const eventTypesParam = contextParamCount + 1; const payloadsParam = contextParamCount + 2; return { sql: ` WITH context AS ( SELECT MAX(sequence_number) AS max_seq FROM events WHERE ${contextVersionQueryConditions.sql} ) INSERT INTO events (event_type, payload) SELECT unnest($${eventTypesParam}::text[]), unnest($${payloadsParam}::jsonb[]) FROM context WHERE COALESCE(max_seq, 0) = ${expectedMaxSeq} RETURNING *; `, params: contextVersionQueryConditions.params }; } ``` The context *CTE* (Common Table Expression) uses the same event filter as during the decision phase, matching event types and optional payload predicates. It returns the maximum sequence number found in that context. The *INSERT* only proceeds if this value matches the *expectedMaxSequenceNumber* that was passed in. If not, no events are appended. The operation fails silently and the caller is aware that the context has changed. From there, the command can simply be retried: re-queried and re-evaluated, with another attempt to append being made. This approach has several advantages: - Atomicity: the check and insert happen in a single query. - Efficiency: no locks or serializable isolation are required. - Precision: only relevant changes cause a conflict. - Concurrency safety: multiple commands can operate in parallel with different filters without interfering with each other. ## Implementation and Reference Code The approach described here is fully implemented in the PostgreSQL-backed event store of the [EventStore NPM package](https://www.npmjs.com/package/@ricofritzsche/eventstore?ref=ricofritzsche.me). [@ricofritzsche/eventstoreA TypeScript event sourcing library with Postgres persistence, real-time subscriptions, and projection support for building responsive event-sourced applications. Latest version: 1.0.5, last published: 3 days ago. Start using @ricofritzsche/eventstore in your project by running \`npm i @ricofritzsche/eventstore\`. There are no other projects in the npm registry using @ricofritzsche/eventstore.![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/icon/3dc95981de4241b35cd55fe126ab6b2c.png)npm![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/thumbnail/338e4905a2684ca96e08c7780fc68412.png)](https://www.npmjs.com/package/@ricofritzsche/eventstore?ref=ricofritzsche.me) It’s part of an ongoing collaboration with [Ralf Westphal](https://ralfwestphal.substack.com/), focused on building practical tools for [Aggregateless Event Sourcing ](https://ricofritzsche.me/aggregateless-event-sourcing/)(AES) with [Command Context Consistency](https://ralfwestphal.substack.com/p/command-context-consistency) (CCC). The code is open source and includes: - A minimal *EventStore* interface with clear semantics - A PostgreSQL adapter that enforces context-local consistency via SQL - Utilities to build filters, queries, and safe conditional inserts - Examples of command-side usage and basic projections You can explore the repository and usage examples here: [GitHub - ricofritzsche/eventstore-typescript: A TypeScript implementation of a functional event sourcing system that provides persistent event storage with optimistic locking and payload-based querying.A TypeScript implementation of a functional event sourcing system that provides persistent event storage with optimistic locking and payload-based querying. - GitHub - ricofritzsche/eventstore-typ…![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/icon/pinned-octocat-093da3e6fa40.svg)GitHubricofritzsche![](https://opengraph.githubassets.com/b7a5db090be30081e07a3c27ad776ef7189dea22e213271a36f3fb52c8bdb014/ricofritzsche/eventstore-typescript)](https://github.com/ricofritzsche/eventstore-typescript?ref=ricofritzsche.me) It's not a framework. There are no hidden conventions. Just a thin, explicit layer over PostgreSQL that helps you model decisions with precision without aggregates, and without unnecessary complexity. *Cheers*! ### Simplicity Wins: Command Context Consistency URL: https://ricofritzsche.me/simplicity-wins-command-context-consistency/ Last updated: 2025-07-23T12:40:28.000Z How can you deliver features from day one without running into the trap of a bad architecture? That’s the question I asked myself for years. And for years, I was on the wrong path, following object-oriented paradigms and object models that simulated the real world, and doing lots of upfront design. It always felt like preparing for something that never really came. The real change began when I came across Jimmy Bogard's [Vertical Slice Architecture](https://www.jimmybogard.com/vertical-slice-architecture/?ref=ricofritzsche.me). His approach remained within the realm of object-oriented programming, with layered logic encapsulated within feature folders. However, he described something different that made me think more deeply. Code shaped like a request, followed by *'do something'*, followed by a response. That stuck. It aligned with the concept of separating commands and queries. It showed me a direction worth exploring. The more I researched, the clearer it became that what we have been taught for decades is ineffective. These legacy architecture approaches are not just slightly outdated; they are completely incompatible with the way real systems grow. They assume that you know everything from the outset. They rely on a central, single model what Domain-Driven Design refers to as 'Bounded Contexts'. They expect the domain to be captured in objects before you’ve even had your first real conversation with your users. And they fall apart the moment change arrives. ## What's Broken in Legacy Architectures Most legacy architectures, such as Clean Architecture, Uncle Bobs Onion approach, the Hexagonal Architecture and any kind of horizontal layered approaches, all share the same hidden flaw. They assume there is a single, central model of truth. They also require you to define that model at the outset. This leads to lengthy design phases. Endless domain discussions. Abstract object hierarchies. Weeks are lost modelling entities and aggregates before a single useful feature is delivered. The problem is simple: these models do not reflect how software actually evolves. You think you're building a universal structure. But every new story brings surprises. Developers sit with domain experts and discover rules that nobody had mentioned before. Or contradictions. They also encounter real-world chaos that doesn’t fit the model. So the model is patched up. Workarounds increase. The design becomes fragile, not because of poor coding, but because of flawed assumptions. Even good ideas such as CQRS and Event Sourcing can fall into this trap. When Greg Young introduced them, his intention was to escape the rigid model. However, DDD and aggregates came along for the ride. Suddenly, people were spending weeks trying to define the 'right' aggregate boundaries, once again resorting to central modelling under a new label. It's unnecessary, it's not agile, and it's not how real systems succeed. ## A Different Mindset: Simplicity, Not Structure Worship Here’s the shift: You don't need a single model. You don't need aggregates either. You don't need an upfront design. What you need is a mindset that builds from the bottom up: feature by feature, fact by fact. Each feature is self-contained. It knows just what it needs to know. It doesn’t depend on a centralized object model. It doesn’t share fragile abstractions with other features. It focuses on one job and does it well. The secret is this: > **You don’t model objects, you record facts.** When a command (intent) comes in, it either produces new facts (events) or not. You take in data, apply business rules, and emit events. That’s it. You don’t need to know how the system will use those events later. You just capture what happened. These events are stored in an **append-only log**. That log is the truth of the domain. It’s the raw, immutable material of your system. You never go back and rewrite it. You just keep adding facts. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/07/Screenshot-2025-07-23-at-14.14.45.png) Append-only event log. **And to be clear:** I am not advocating for an unstructured mess of data and code. That's not my goal. My goal is to get rid of unnecessary complexity and data structures in the right place. The goal is to build robust systems with **simplicity that has boundaries**. ## Commands Act in Context A command does not update an object. It does not load a parcel from the database. And, is does not rehydrate an aggregate or mutate anything. It simply looks at the facts (the relevant events that happened before), and decides what to do. That’s the point. Each command runs in its own local context. It knows exactly what it needs. It is independent of the global state and does not share mutable objects. It takes the input, checks the facts, applies the rules, and appends the new events to the log. But, let's take a real example from the logistics domain. ### Delivery Attempt Tracking Failed parcel deliveries happen every day. We want to track them and decide what to do when too many attempts fail. The rule is simple: after three failed delivery attempts, the parcel should be marked as *undeliverable*. There is no object called 'Parcel' here. We don't simulate its state. We simply record what happened. The command looks like this: ```ts type AttemptDelivery = { type: "AttemptDelivery"; parcelId: string; }; ``` In order to make the right decision, we rebuild just enough state for this specific context. We check how many attempts have been made and whether the parcel has been delivered or marked as *undeliverable*. This is the only information that the feature requires. ```ts function decide(events: EventRecord[], command: AttemptDelivery): DeliveryAttemptResult { const attempts = events.filter(e => e.eventType === "DeliveryAttempted").length; const alreadyDelivered = events.some(e => e.eventType === "DeliverySucceeded"); const undeliverable = events.some(e => e.eventType === "DeliveryMarkedUndeliverable"); if (alreadyDelivered) { return { success: false, error: { type: 'ParcelAlreadyDelivered', message: 'Parcel has already been delivered.' } }; } if (undeliverable) { return { success: false, error: { type: 'ParcelUndeliverable', message: 'Parcel was already marked as undeliverable.' } }; } if (attempts >= 3) { return { success: true, event: { eventType: "DeliveryMarkedUndeliverable", payload: { parcelId: command.parcelId } } }; } return { success: true, event: { eventType: "DeliveryAttempted", payload: { parcelId: command.parcelId } } }; } ``` This function does not check database records. It doesn't deal with side effects. It's deterministic: the same input produces the same output. You always know exactly what it does. It’s easy to test and difficult to break. This is what writing business logic in a functional style means. There is no simulation of real-world objects. There are no hidden state transitions in opaque objects. Just clear, fact-based decisions. Each feature defines its own rules based on the facts relevant to its function. If something changes, it’s easy to update the relevant rules without worrying about breaking other parts of the system. If something is completely irrelevant, rewrite it. ## Functional Thinking When I say that all these legacy architectural styles do not match, I mean exactly that. They’re all built around the idea of modeling life-cycles and simulating the real world with objects. But that’s not how real systems behave. Real systems respond to intent. Something happens. Rules apply. A decision is made. That’s it. I found the answer in functional thinking. The model I follow is simple: action, calculations, data. A command comes in, the logic decides what should happen, and a new event is recorded. There is no need for objects that try to hold state. There is no need for aggregates or base classes or factories. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/07/Screenshot-2025-07-23-at-14.17.22.png) Functional programming: Actions, Calculations, Data (ACD) I know, I know... of course technical dependencies still exist. We still need to store events, call external APIs and handle inputs. This is where the idea of separating a functional core from an imperative shell comes in useful. The functional core is where the rules reside. It's pure. It doesn't deal with side effects. It takes input and returns a result. That’s what makes it testable without mocks, and deterministic by design. The shell wraps around it and handles the messy stuff, reading facts from the event store, validating the request, calling APIs, and persisting the result. This way, the logic stays clean and focused, while the technical noise stays on the outside. I chose this model because it aligns with my approach to building software. I don't want centralized entities where everything depends on everything else. I also don't want to solve this problem using Dependency Inversion (DIP) via indirect methods, because functional dependencies would still remain. I don't want a shared object model that forces teams to agree on the contents of a 'Parcel' or 'Device'. **I want fully self-contained feature slices.** I want to build something that does its job without worrying about the rest of the system. This approach gives me exactly that. The only thing shared across the system is the event log: the raw, immutable sequence of facts. Every feature can read from it. Every feature can write to it. But what happens inside the slice is completely under its own control. ## What About Reading? Reading data follows the same principle. No shared object model. No generic repository. No central read layer that tries to cover every case. Just views built for a **specific purpose**, shaped by the needs of a single feature. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/07/Screenshot-2025-07-23-at-14.27.06.png) Each read model is a projection. It’s built from facts that were already recorded. When something happens in the system, the projection updates. And if nothing relevant happened, it stays the same. That’s how it works. This projection can be a database table like *device\_list*, *failed\_deliveries*, or *driver\_route\_overview*. In other cases, it’s an in-memory view or a pre-computed JSON blob. It doesn't matter. The important part is that it exists only for one use case. It’s not trying to be universal. It doesn't aim to reflect 'the truth' in general. It only reflects enough truth to answer one question quickly. This means the query side stays decoupled from the write side. You don't need to model your data upfront just to answer future questions. You can always add new projections later, even after the system is in production. That's the power of using facts instead of states. The facts don’t go away. You can reprocess them anytime and shape them into any form you need. This also means features like *AttemptDelivery* or *MarkAsDelivered* don’t have to care about how someone will present these facts later. They just record what happened. The way this information gets displayed or analyzed can evolve later; separately, and without needing to change the feature logic that produced the events. ## Build Systems That Flow Common architectures based on object-oriented thinking try to freeze the world into objects and layers. They assume structure is more important than flow. But the real world does not work like that. Things happen. People change their minds. Requirements evolve. That's why upfront models break. That's why centralized entities collapse under the weight of change. What works instead is a system that flows with intent and reacts to facts. A system where each feature lives on its own, sees what it needs, and makes decisions without asking permission from the rest of the codebase. A system that grows without becoming fragile. This isn't about patterns. It's not about buzzwords. It's about creating software that's straightforward, clear, and designed for change. From day one. *Cheers*! ### How I Built an Aggregateless Event Store with TypeScript and PostgreSQL URL: https://ricofritzsche.me/how-i-built-an-aggregateless-event-store-with-typescript-and-postgresql/ Last updated: 2025-07-09T12:31:58.000Z I’m picking up from my two of my latest posts, [Functional Event Sourcing](https://ricofritzsche.me/functional-event-sourcing/) and [Aggregateless Event Sourcing](https://ricofritzsche.me/aggregateless-event-sourcing/). In those pieces I showed why we can drop aggregates, shared object graphs, and the everyday habits from mainstream object -oriented thinking (yes, I know that Alan Kay’s original idea was different). We also drop the idea of central, universal entities, which is also anchored in object-oriented thinking: *"a person is a person is a person"*. The core idea is simple: each feature slice is fully self-contained. It pulls only the events it needs, folds them into the state required by the current command, and ignores everything else. Events are the one thing what all the features share, everything else stays local. After those posts the main question was sometimes: > “Nice theory, but how do I build it?” This post is my answer. I’ll walk through a working event store. It runs on plain *PostgreSQL*, uses *TypeScript* for the examples, and stays 100 % framework-free. I used the same pattern in Rust originally for an internal event store in a large asset-tracking system handling millions of events. The *TypeScript* version follows the same core logic. The goal is not to sell you yet another library. The goal is to show how the idea survives contact with real code. Let's discover optimistic locking, query filters, and so on while staying small enough to understand in a single sitting. ## What Aggregateless Means in Practice *Aggregateless* simply means I do not keep a big “User” or “Order” object cluster in memory, containing all the business rules and structure. When a command arrives, I ask the store only for the events that matter to that command, build the tiny bit of state of what is specifically needed in the context of the command, and throw that state away once the decision is made. Each feature slice works the same way. They all query the event store using a filter to rebuild their own view from scratch. Consistency is handled inside the command context itself. The command carries a filter that describes its context. In the feature slice, we read the context, decide what new events are needed, and then try to write them back with the very same filter. *PostgreSQL* makes the write succeed only if nothing in that context has changed. If it has, I retry or tell the caller to try later. The decision code is pure functions: given past events and a command, return new events or an error. That purity keeps tests straightforward, I feed in events and check the result. ## Functional Core / Imperative Shell in a Nutshell The pattern is simple. - **Functional core** – Pure functions. They take a list of past events plus the incoming command and return either new events or an error. No database calls, no timestamps from the outside, no hidden state. Give the same input, get the same output. - **Imperative shell** – This is the part that handles all input and output. It loads the context from a database, an API, or any other source, passes the relevant data to the functional core, and, if a decision is made, tries to persist the computation results. The write is typically done in one atomic step, so if the context is still valid, the events are saved. If not, the operation fails. Because the core is pure, unit tests run in milliseconds without a database. And because the shell is small, you can swap *PostgreSQL* for anything else that supports the same two operations: query by filter and append with a guard. Read more here: ## Hands-On I have already unpacked these ideas across several posts. If you missed them, feel free to catch up later. For now, let’s jump straight into the hands‑on part. ### Setting Up PostgreSQL If you don't have a local *PostgreSQL* instance running, you can bring one up like this: ```bash docker run --name eventstore-pg \ -e POSTGRES_PASSWORD=postgres \ -p 5432:5432 -d postgres:17 ``` Then create a *.env* file so the code knows where to connect: ```bash echo "DATABASE_URL=postgres://postgres:postgres@localhost:5432/bank" > .env ``` If you're already running *PostgreSQL* elsewhere, just point the *.env* to that instance. Make sure your *PostgreSQL* user has permission to create the database and schema. That setup runs automatically on first use when you run one of the examples in the [repo](https://github.com/ricofritzsche/eventstore-ts-example?ref=ricofritzsche.me). No extra setup is needed. When you run *store.migrate()* it creates everything: - A single *events* table - GIN and B-tree indexes for fast lookups - Nothing per event type Here’s what the table looks like: ```sql CREATE TABLE events ( sequence_number BIGSERIAL PRIMARY KEY, occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(), event_type TEXT NOT NULL, payload JSONB NOT NULL, metadata JSONB NOT NULL DEFAULT '{}' ); ``` Each row is a fact. The *payload* holds the full event data, *event\_type* tells us what kind it is and *metadata* can track tracing info, source system, user ID, whatever you need later. No separate schema per event type. No table per aggregate. Just one simple table that holds every event. This is flexible enough to handle anything the system needs. ### Query Filters A command decides what data it needs by building an *EventFilter*. The filter names event types and, if needed, key‑value checks inside the JSON payload. ```typescript // Find all deposits for a given account const filter = EventFilter .createFilter(['MoneyDeposited']) .withPayloadPredicate('accountId', accountId); ``` The **same filter** goes back into *append* when you write new events. This is important, because that single reuse is the trick: it asks the database to ensure the view you read is still the view you write against. ```typescript await store.append(filter, newEvents, maxSequenceNumber); ``` There are no shared entities and no global version numbers involved. The only thing that defines the context for a command is the filter it provides. ### Optimistic Locking with a CTE One question keeps coming up: > How does this work without version numbers or locking rows? The answer is simple. To guarantee consistency during concurrent operations, the event store relies on **optimistic locking**. Rather than locking rows or maintaining explicit version numbers, it uses a Common Table Expression (CTE) to ensure that new events are only inserted if the original context remains unchanged. Here’s the core idea: ```sql WITH context AS ( SELECT MAX(sequence_number) AS max_seq FROM events WHERE ${contextCondition} ) INSERT INTO events (event_type, payload, metadata) SELECT unnest($${eventTypesParam}::text[]), unnest($${payloadsParam}::jsonb[]), unnest($${metadataParam}::jsonb[]) FROM context WHERE COALESCE(max_seq, 0) = $${expectedMaxSeqParam} ``` Here’s what happens step by step: - First, the *ctx* block checks which events matched the filter when we loaded the context. It stores the latest *sequence\_number* it finds. - Then the *INSERT* only runs if that number is still the same. - If another writer has added a matching event since then, the number will have changed and the *INSERT* will fail silently. This is our signal that the context is no longer valid. This gives us strong consistency without any locks or version columns. Nothing is blocked. Multiple writers can act in parallel, and if the context shifts underneath, the write is rejected. The whole check happens inside one atomic SQL statement. Here's the full flow, from command to final write: ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/07/Command2Write.png) Let’s now look at how this maps to real *TypeScript* code. ## The EventStore in TypeScript (Code Walkthrough) The full implementation is available in [this repo](https://github.com/ricofritzsche/eventstore-ts-example?ref=ricofritzsche.me), but here is a quick walkthrough of the core parts. At the center of the system are two Event Store operations: 1. Load events based on a filter (*query*) 2. Append new events, but only if the context has not changed (*append*) Everything else builds around that. ### Defining Events Each event is just a plain object that knows how to describe itself. It must implement a small interface: ```ts export interface HasEventType { eventType(): string; eventVersion?(): string; } ``` An example event might look like this: ```ts class BankAccountOpened implements HasEventType { constructor( public readonly accountId: string, public readonly customerName: string, public readonly accountType: string, public readonly initialDeposit: number, public readonly currency: string, public readonly openedAt: Date = new Date() ) {} eventType(): string { return 'BankAccountOpened'; } eventVersion(): string { return '1.0'; } } ``` There are no base classes and no inheritance trees. Just clear data with a type. ### Building a Filter A command starts by declaring what it needs from the event store. This is done using an *EventFilter*, which specifies the event types and optional payload fields. ```ts const filter = EventFilter.createFilter( ['BankAccountOpened', 'MoneyDeposited', 'MoneyWithdrawn', 'MoneyTransferred'], [ { accountId: fromAccountId }, { accountId: toAccountId }, { toAccountId: fromAccountId }, { fromAccountId: toAccountId } ] ); ``` This filter does two things: it defines what context to load for the decision logic, and it becomes the guard condition when trying to append new events. That reuse is what keeps everything consistent. ### The Store Interface The store itself exposes a minimal interface: ```ts export interface QueryResult { events: T[]; maxSequenceNumber: number; } export interface IEventStore { query(filter: EventFilter): Promise>; append(filter: EventFilter, events: T[], expectedMaxSequence: number): Promise; close(): Promise; } ``` The *query* function loads all events that match the given filter. The *append* function tries to write new events, but only if the same filter still matches the same context. If something has changed in between, the write fails silently. This interface is storage-agnostic. While the example here uses *PostgreSQL*, the same pattern works with any backend that supports filtered reads and guarded writes. You could implement it using MongoDB, DynamoDB, or even a file-based store, as long as you can enforce the condition that new events are only written if the original context is still valid. ### A Closer Look at the PostgreSQL Implementation The *PostgreSQL* store is built around the two core operations: *query* and *append*. It follows the *IEventStore* interface exactly, and all logic sits in a single class: *PostgresEventStore*. Here’s a simplified look at how the query and append parts work: ```ts async query(filter: EventFilter): Promise { const query = ` SELECT * FROM events WHERE event_type = ANY($1) AND payload @> $2 ORDER BY sequence_number ASC `; const result = await this.pool.query(query, [filter.eventTypes, filter.payloadPredicates]); return result.rows.map(row => this.deserializeEvent(row)); } ``` This loads all matching events for the context a command is interested in. It’s fully filter-based. **Appending new events (with consistency check):** ```ts async append( filter: EventFilter, events: T[], expectedMaxSequence: number ): Promise { const eventTypes = events.map(e => e.eventType()); const payloads = events.map(e => JSON.stringify(e)); const metadata = events.map(e => JSON.stringify({ version: e.eventVersion?.() || '1.0' })); const contextQuery = this.buildContextQuery(filter); const insertQuery = this.buildCteInsertQuery(filter, expectedMaxSequence); const params = [ ...contextQuery.params, // context filter expectedMaxSequence, // value from query time eventTypes, // values to insert payloads, metadata ]; const result = await this.pool.query(insertQuery, params); if (result.rowCount === 0) { throw new Error('Context changed: events were modified between query and append'); } } ``` Notice that *expectedMaxSequence* is passed in from the calling code. This value must come from the original *query()* step. The *append()* function does not re-read the context. It trusts the caller to provide the sequence number that reflects what they saw. The actual insert is guarded by a CTE that re-evaluates the same filter and only proceeds if the sequence number hasn’t changed. That is optimistic locking, scoped to exactly what the command cares about. This approach guarantees consistency without global versioning or stream-based locking. And since the boundary is defined by the filter, it is visible, testable, and flexible. ### No Extra Abstractions The code stays close to the problem. There are no decorators, no framework hooks, and no code generation involved. The **store** is just a small adapter over *PostgreSQL* that does exactly what you ask: read events, check context, write facts. That simplicity is what makes it easy to reason about. Every line of logic is visible. Every piece of state comes from events. There is no global registry needed. There is no hidden coupling, and nothing to configure. Just data, filters, and pure functions. ## Pure Business Logic Example Let’s look at how the decision logic works. This is the part that folds the current state from events and evaluates the command. It does not talk to the database, it does not know anything about filters or storage, and it has no side effects. It is just a plain function that takes input and returns either new events or an error. Here’s an example from the banking use case: opening a new account. First, we define the shape of the state we want to fold: ```ts type AccountState = { exists: boolean; }; ``` Then, we build that state by folding past events: ```ts function foldAccountState(events: BankAccountOpened[]): AccountState { return { exists: events.length > 0 }; } ``` It doesn’t get much simpler. If we find at least one *BankAccountOpened* event, the account exists. Next comes the decision logic. It takes the folded state and the command and decides whether we are allowed to proceed: ```ts function decideOpenAccount( state: AccountState, command: OpenBankAccountCommand ): Result { if (state.exists) { return { success: false, error: { type: 'AlreadyExists', message: 'Account already opened' } }; } const event = new BankAccountOpened( command.accountId, command.customerName, command.accountType || 'checking', command.initialDeposit || 0, command.currency || 'USD' ); return { success: true, event }; } ``` This function is easy to read and easy to test. If the account already exists, we return an error. Otherwise, we emit one event that captures everything needed to describe what just happened. There are no external dependencies. Even the timestamp is part of the event constructor, so the logic stays free of global state. This separation makes testing straightforward. I can write unit tests that feed in a list of past events and a command, then check if the result is what I expect. There’s no setup, no mocks, and nothing to clean up afterward. This is what functional core means in practice: a function that sees everything it needs and nothing else. No framework required. ## Running the Examples To see the full flow in action, the repo includes a simple CLI with a set of step-by-step examples. These examples show how commands are handled, events are written, and state is rebuilt, without you needing to wire anything yourself. Start by installing the dependencies: ```bash npm install ``` Then run the examples and try it out: ```bash npm run cli ``` This will guide you through a few typical operations: opening an account, depositing money, and checking the current balance. Everything runs against the real *PostgreSQL* instance and uses the event store exactly as described above. Once the CLI is running, you can walk through common actions like opening an account, depositing money, and checking balances. Each operation runs through the real event store, talks to *PostgreSQL*, and stores facts in the events table, just like in production. You can inspect the table at any point to see the raw events. Since everything is stored as JSONB, it’s easy to query and debug directly. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/07/Screenshot-2025-07-07-at-13.56.47.png) Running the example adding a new bank account Let's add some money... ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/07/Screenshot-2025-07-07-at-13.58.05.png) Adding money Let's check the events table in the *PostgreSQL* database. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/07/Screenshot-2025-07-07-at-13.59.10.png) Events in the database. After adding another amount, lets simply check the balance. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/07/Screenshot-2025-07-07-at-14.00.29.png) Checking the balance. You’ll find the CLI in the src/cli.ts file. It’s self-contained and easy to extend. ## Testing for Confidence The system is split cleanly into two layers, and that makes testing straightforward. The functional core contains all the decision logic. These are just pure functions. You can pass in a list of events and a command, and check the result. That’s it. There is no database involved, no mocks, and no setup or tear-down. These tests run fast and tell you exactly where your logic breaks. The imperative shell is the part that talks to the event store. Here you’ll want integration tests. These load a real *PostgreSQL* database, send commands through the full pipeline, and verify that the events were written correctly. Since the store is just *PostgreSQL* under the hood, this kind of testing is simple and reliable. You don’t need a mocking framework. You don’t need test doubles. The separation makes everything easier to isolate and reason about. ## Conclusion The event store is designed to stay simple in structure, yet fully capable of supporting large systems and high load. It gives you the control you need today, and the flexibility to scale as your system grows. If your event volume increases, you can batch inserts, index more fields, or add simple partitioning on the database level. *PostgreSQL* scales well for this kind of workload, especially when events are stored as [*JSONB*](https://levelup.gitconnected.com/dynamic-jsonb-queries-in-postgresql-via-rest-a-deep-dive-with-net-73fb4decfea1?sk=87e9412b63377ae627c2bcc056c54d17&ref=ricofritzsche.me) and indexed properly. If needed, you can later add a tags field to your events, similar to how the [DCB approach](https://dcb.events/?ref=ricofritzsche.me) works. In my experience, filtering on *JSONB* alone is fast and flexible, even at scale. But tags can help when you want to optimize query performance. Nothing in this setup prevents that. And if you decide to re-implement this in Rust, Go, or Python, it works the same way. The idea is not tied to *TypeScript*. All you need is a way to load events by filter, make a decision, and try to append with the same guard. This approach is simple, but powerful. It avoids the usual complexity that comes with aggregates, shared states, and framework rules. Instead, it gives you clear boundaries, stable behavior, and full control over how decisions are made. You work with real events. You define context through filters. You decide through pure functions. You store facts, not state. If you want to dive into the code, here’s the repo: [github.com/ricofritzsche/eventstore-ts-example](https://github.com/ricofritzsche/eventstore-ts-example?ref=ricofritzsche.me) *Cheers*! **Note:** *A few of the questions I've received about projections, streaming from the event store, and partitioning strategies deserve more space. I’ll cover those in upcoming articles. I'll show how I use PostgreSQL to stream events into read models, how tenant-level partitioning works in practice, and how to keep projections fast and isolated without turning everything into infrastructure overhead. Stay tuned.* ## Weekly Tips to Master Software Architecture Newsletter I provide lean, effective software-architecture strategies that cut through the noise. Subscribe Email sent! Check your inbox to complete your signup. Trusted by 2,500+ developers on Medium. ### Objects Are Dead, Long Live Feature Slices URL: https://ricofritzsche.me/objects-are-dead-long-live-feature-slices/ Last updated: 2025-07-15T09:12:39.000Z For years, we’ve been trained to chase reuse. Don’t repeat yourself. Extract logic. Share rules. Centralize state. Wrap it all in nice little objects. It sounds smart. Efficient. Clean. But in practice, it leads to the opposite. You start with one rule. Let's say, a device can’t be bound once it’s retired. Then someone adds “*a retired device can’t be renamed.*” Next, someone else adds an edge case: “*except when it's marked for re-inspection.*” So you extract a shared validator or something like that. Then this helper grows conditions, and you need to test the helper. Then you wonder what breaks when you touch it. **That’s not reuse! That’s coupling.** I used to build code like that. It felt organized, abstract, elegant. But over time, it got messy. Hard to follow. Full of invisible dependencies. Today, I see it differently. I don’t want shared rules. I want explicit ones. I don’t want smart objects. I want dumb data and focused decisions. I don’t want reuse in my domain logic, not unless it’s earned. Let’s talk about why. ## Why Explicit Rules Beat Shared Logic Let’s take a simple domain rule: **“** *A retired device can’t be bound to an asset.* **”** That rule only matters when you bind a device. Not when you rename it. Not when you inspect it. Just in that one feature. So why should it live in a shared helper? Why should it become a global *isRetired*() method that everyone calls without context? In my model, each feature owns its own rules. The *bind-device-to-asset* slice loads the events it needs, reconstructs the relevant state, and checks whether the device was retired. It does that explicitly, right there, in the decision function. No indirection. No lookup. No magical helper trying to guess the situation. Now, you might say: “*But what if I need the same check in another feature?*” That’s fine. Then the *other* feature will load the events *it* needs and check retirement *in its own way*. It might care about different events. It might apply different exceptions. That’s the point. **Rules look similar but behave differently depending on context.** By scoping the rule to one place, you avoid overgeneralizing. This means you won't end up with a complex helper that tries to serve every case with edge flags, optional parameters, and increasing complexity. ## Shared Events vs. Shared Logic I’m not against sharing. I’m just careful about what we share. **Events are shared. Logic is not.** The event store is the source of truth. If a *DeviceRetired* event occurs, it is visible to all. Every feature slice can query it. But how you *interpret* that event? That’s contextual. That belongs to the feature. The *bind-device-to-asset* feature may interpret *DeviceRetired* as an absolute barrier. However, the *rename-device* feature may allow renaming even after retirement, unless another event such as *DeviceLocked* is present. **The same underlying data applies. Different rules. Different decisions.** That’s why I don’t believe in centralizing these checks. A rule is only meaningful in a specific flow. When you abstract it too early, you lose that meaning and invite bugs the moment someone “reuses” it in a place it doesn’t belong. So yes: **Events are shared across slices. Rules are not.** And that separation is what keeps your system flexible, understandable, and easy to evolve. ## Why Objects Make It Worse Objects were supposed to make things clearer. Encapsulate state. Bind data and behavior. Model the real world. But here is the problem: **The real world doesn’t think in objects.** Your domain experts don’t say, *“Let’s instantiate a device and call the bindToAsset() method.”* They say, *“If the device is retired, it can’t be bound to anything.”* That’s a rule. A decision. A constraint. It’s not tied to a class. It doesn’t belong to an object. It exists because of the process, not the structure. Objects blur that line. They collect behavior in one place, even if the rules come from different contexts. So now your *Device* object has *rename()*, *retire()*, *bindToAsset()*, maybe even *getInspectionHistory()*. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/06/OOP-1.png) It does too much. It knows too much. And every time you change one part, you risk breaking another.One method mutates, another retrieves. Same object. Mixed responsibilities. No isolation. That’s why I say: **objects are a bad fit for domain logic.** They promote coupling over clarity. They group behaviors not by intent, but by data. And they make it hard to reason about rules, because everything depends on everything. Let the data stay dumb. Let rules live in the feature itself. And let behavior emerge from pure functions, not from methods on an object. ## Feature Slices Think Differently A feature slice doesn’t start with an object. It starts with a **use case**. Take *bind-device-to-asset*, for example. That’s not a method of the *Device* object. It’s a flow. It has an input (device ID and asset ID), a current context (what is the state of the device?), some rules (can it be bound?), and an outcome (success or failure, expressed as an event). That’s it. You don’t need to know how the device was created. You don’t care if it has other capabilities. All you need is what’s relevant for *this one thing*. And that’s the mindset: **Do one thing. Use only what’s needed. Keep it local.** ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/06/DoOneThing.png) Feature slices decouple Feature slices don’t share state. They don’t reuse rules. They build their own view of the truth, using events from the event store. Yes, sometimes that means repeating the way events are queried and interpreted. That’s totally fine. It’s explicit. It’s visible. It’s traceable. You know exactly which events a feature cares about. And when a rule changes, you know exactly where to look. The result? - Smaller, simpler units - Fewer side effects - Better alignment with how the business actually thinks - And a system that’s easy to grow because each feature stands on its own This isn’t just a different code structure. It’s a different way of thinking. *Note*: Vertical Slice Architecture, with its built-in separation of commands and queries, is fundamentally orthogonal to layered architectures and object-oriented design. It structures code by intent and outcome, not by technical role or class hierarchy. ## Conclusion For a long time, I thought good software meant reusable software. I tried to model the domain with rich objects, centralize shared logic, and keep my code DRY. But over time, that created more problems than it solved. Small changes had wide effects. Simple rules turned into abstractions. And every reuse brought hidden assumptions I couldn’t see. The turning point was when I stopped chasing reuse and started chasing clarity. I began treating each feature as a small, self-contained unit: its own state, its own rules, its own decisions. The only thing shared across slices are pure events**.** That shift changed everything. Now, rules are visible. Decisions are local. The code reads like the business talks. And when something changes, I know exactly where to go. So no, I don’t want global states, aggregates, or smart entities anymore. I want rules in context, behavior in slices, and code that earns its simplicity not through abstraction, but through explicitness. *Cheers*! ### Context, Rules, Decisions: The Formula for Better Domain Models URL: https://ricofritzsche.me/context-rules-decisions-the-formula-for-better-domain-models/ Last updated: 2025-06-25T16:10:47.000Z Have you ever wondered why object-oriented design sometimes feels overly complex, despite its promise of clarity and encapsulation? What if the fundamental problem isn't how we encapsulate data, but rather how we combine and manipulate it? Could it be that our software becomes complicated not because of the domain itself, but because of the way we've learned to structure it? For many years, object-oriented practices defined my software development approach. Instantiating objects, manipulating them through their methods, and then persisting their state. But this cycle of instantiate–mutate–persist felt rigid, tangled, and unnecessarily complicated. Then I started evaluating simpler, more natural paths, shifting toward pure domain logic through *Rules* and *Decisions*. Instead of mutating complex object graphs, imagine providing immutable data as input, evaluating clear and deterministic rules, and receiving new immutable data as output. No hidden state changes, no side effects, just straightforward, predictable transformations. What if the true heart of your domain logic isn't the objects you instantiate, but rather the pure rules you apply? How might this change your perspective on complexity, maintainability, and clarity? In my previous article on [Aggregateless Event Sourcing](https://ricofritzsche.me/aggregateless-event-sourcing/), I described how removing aggregates simplifies event-driven architecture. Now, let’s build on that idea by exploring rules and decisions explicitly, and how moving away from mutable objects toward pure, explicit logic can simplify your domain modeling. Let's dive into a more intuitive, functional, and practical way of thinking about software, one free from unnecessary complexity and rich with clear, actionable insights. ## Rules: Capturing Your Domain Clearly When we build software, we’re translating real-world business rules into logic. These rules don't originate in code. They come directly from the domain experts, stakeholders, or users who deeply understand the problem you’re solving. A *Rule*, then, is first and foremost something your business defines clearly: - "A tracking device can only bind to one asset at a time." - "A sensitive asset can only accept certified tracking devices." - "Devices cannot be reassigned until unbound from their current asset." Notice something essential here: these rules are not technical. They express how the business itself naturally operates and what it expects from the system. They provide precise, explicit constraints and guidelines reflecting the real-world domain. Your job as a software developer isn't to create these rules; your job is to **clearly capture and faithfully reflect** them in code. ### Rules Are Defined by the Business, Not by Technology When business experts say, "*We can’t ship fragile goods without special packaging*" - that's a domain rule. It's simple, explicit, and free from implementation details. As software developers, our first responsibility is to make sure these business rules remain just as clear and explicit when we translate them into code. We don't want these rules scattered across multiple layers, objects, or services. Instead, we keep them simple, clearly expressed, and directly aligned with how the business thinks. ### From Domain Rules to Decisions In software, a decision is how we explicitly apply these business rules to a specific scenario: - "Given the current state and the requested action, what should happen?" - "Should this shipment be allowed to proceed?" - "Is this device allowed to bind to that asset?" Decisions take the clearly defined rules from your domain and explicitly apply them, resulting in an outcome, usually expressed as an event describing precisely what happened or why it couldn’t happen. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/06/Rules-----Decisions-----Events.png) Rules → Decisions → Events ### Keeping Rules Pure and Clear Once we've captured business rules clearly from the domain, our software benefits tremendously by representing these rules explicitly and purely: - No side effects. - No hidden state mutations. - No surprises. The clarity and purity of your rules reflect the clarity and explicitness of the business domain. It ensures the software remains understandable, maintainable, and directly aligned with real-world expectations. ## From Business Rules to Clear Code We've clarified that rules come directly from the business domain. They're the guidelines and constraints stakeholders define clearly. But how do we make these explicit domain rules visible and intuitive in code? When writing clear, maintainable code, it's essential to explicitly represent your business rules. To illustrate, let’s continue with our logistics scenario, specifically the feature of binding a device to an asset. The domain logic is expressed through a pure function named *decide*. Its signature clearly reflects the necessary input: the historical events of the device and the asset, and the command itself: ```rust pub fn decide( device_events: &[Event], asset_events: &[Event], cmd: BindDeviceCommand, ) -> Result, BindError> ``` Within this function, your business rules are clearly and explicitly represented by iterating over past events. For example, to determine if the device even exists, the function checks the historical events: ```Rust let device_exists = device_events.iter().any(|e| matches!(e, Event::DeviceRegistered { .. })); if !device_exists { return Err(BindError::DeviceNotFound); } ``` Similarly, before binding a device, we must know if it's already bound or retired. This, too, involves inspecting past events: ```Rust let device_retired = device_events.iter().any(|e| matches!(e, Event::DeviceRetired { .. })); if device_retired { return Err(BindError::DeviceRetired); } let current_binding = device_events.iter().rev().find_map(|e| match e { Event::DeviceBoundToAsset { asset_id, .. } => Some(asset_id.clone()), Event::DeviceUnboundFromAsset { .. } => None, _ => None, }); if let Some(bound_asset) = current_binding { if bound_asset == cmd.asset_id { return Err(BindError::DeviceAlreadyBound); } } ``` These checks are straightforward, transparent, and directly aligned with the rules your business domain defines. Notice how this logic makes it explicit exactly under which conditions the binding will proceed or fail. And if a rule isn't fulfilled, we explicitly indicate the reason through a clear, domain-specific error: ```Rust #[derive(Debug)] pub enum BindError { DeviceNotFound, AssetNotFound, DeviceAlreadyBound, DeviceRetired, } ``` The explicit domain errors in the code directly reflect and reinforce the Ubiquitous Language of your domain. By clearly naming errors such as *DeviceNotFound*, *AssetNotFound* and *DeviceAlreadyBound*, the code precisely reflects the way in which domain experts would discuss these situations. These explicit error messages closely mirror the language that domain experts naturally use in conversation, rather than using generic or technical terms. This close alignment of your code with the language of the domain creates clarity and transparency. It ensures that everyone involved, including developers, stakeholders and domain experts, understands precisely what is happening, why decisions were made and which business rules apply. It also overcomes the communication issues that are often encountered in software projects, providing direct support for the principle of Domain-Driven Design (DDD) of building software around a shared, consistent Ubiquitous Language. ## How a Command Defines the Context In typical object-oriented approaches, the context in your domain is typically predefined through aggregates (clusters of entities and value objects) bound together with rigid boundaries. But I've found this approach too limiting and rigid for real-world applications, especially in domains that are naturally dynamic. When embracing explicit domain rules and pure decisions, the context emerges naturally from your commands. Each command represents a specific intention or use case clearly defined by your domain experts, and therefore each command creates its unique context. Let’s consider again the simple command of binding a device to an asset. When your system receives this command, the context it needs to evaluate is explicitly shaped by that command: it must load exactly those events related to the specified device and the specified asset. There’s no need to load unrelated or unnecessary information, only the data explicitly relevant to the decision at hand. The context, therefore, is simply defined by these explicit boundaries of intent: "*I want to bind this specific device to this specific asset right now.*" Your decision-making logic then explicitly retrieves exactly the historical events that matter for evaluating your rules: events about the current status of this device, events indicating if the asset is ready or already in use. Your rules evaluate these relevant events directly, producing an explicit decision. This feature-related context is inherently simpler and more intuitive. It avoids the artificial complexity of rigid aggregates or complicated object graphs. Instead, the context is exactly as broad or narrow as required by the actual use case. The context would naturally change if the command were different (e.g. if it were to be "*unbinding a device*"). In this case, you would look for events relating to current bindings and previous assignments. The context would dynamically shift to match the command, making it clear and intuitive. This is precisely why explicit commands and pure rules simplify your software. You directly represent the domain's real-world intentions, clearly define exactly what's relevant, and avoid unnecessary complexity. The code reflects how your domain experts think about their business. ## Reconstructing State from Events With a clear understanding that each command defines its context, the next logical question arises: how exactly do we retrieve and rebuild the current state needed to make our decisions? The answer is straightforward. Your application's current state isn't stored in fixed objects or database tables that directly represent entities. Instead, your state is dynamically reconstructed from pure events. Events are immutable records of past actions or occurrences. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/06/Event-Filtering-----State-Reconstruction-----Decision.png) Event Filtering → State Reconstruction → Decision Whenever your application receives a command, it queries exactly those events relevant to its decision. To do this efficiently and explicitly, we define clear filters for the event store. Each filter explicitly describes: - **Which types of events** we’re interested in. - **Any specific attributes** those events must contain (for example, "device ID = 123"). Here's how a simple event filter might look in code, clearly expressing this idea: ```rust #[derive(Debug, Clone)] pub struct EventFilter<'a> { pub event_types: &'a [&'a str], pub payload_preds: HashMap<&'a str, Value>, } impl<'a> EventFilter<'a> { pub fn new(event_types: &'a [&'a str]) -> Self { assert!(!event_types.is_empty(), "EventFilter needs ≥1 event type"); EventFilter { event_types, payload_preds: HashMap::new(), } } pub fn with_pred>(mut self, key: &'a str, v: V) -> Self { self.payload_preds.insert(key, v.into()); self } } ``` ### Why is this important? Instead of retrieving everything or relying on arbitrary, fixed aggregates, your filters are explicit about what events matter. For example, to determine whether a device can be bound, you explicitly query events such as *DeviceRegistered*, *DeviceBoundToAsset* and *DeviceRetired*. You also explicitly filter for the specific device you're deciding about (e.g., "device\_id = 123"). By replaying just these relevant events, your application rebuilds a precise, meaningful snapshot of the current state needed for the decision. This state is fresh, accurate, and directly aligned with your current context. There’s no stale or redundant data, and no rigid boundaries limiting your flexibility. This approach makes state reconstruction clear, simple, and highly performant. Your decisions remain accurate, straightforward, and closely aligned with your business reality. ## Separation of Concerns It is important to understand that querying events from your event store is not one of the typical tasks of your pure decision logic. Queries involve side-effects (they access external resources like a databases or files) which means they should remain clearly separated in what we call the [*Imperative Shell*](https://ricofritzsche.me/simplify-succeed-replacing-layered-architectures-with-an-imperative-shell-and-functional-core/)*.* The imperative shell handles loading events explicitly from the event store, based on the filters defined earlier. Then, it passes these relevant events as immutable data into your pure domain logic. Here's a small, practical example demonstrating this clearly: ```rust pub async fn load(pool: &PgPool, filter: EventFilter<'_>) -> anyhow::Result> { let mut query = "SELECT payload FROM events WHERE event_type = ANY($1)".to_string(); let mut params: Vec<&(dyn sqlx::types::Type + Sync)> = vec![&filter.event_types]; for (i, (key, value)) in filter.payload_preds.iter().enumerate() { query.push_str(&format!(" AND payload @> ${}", i + 2)); params.push(value); } let rows = sqlx::query_with(&query, params) .fetch_all(pool) .await?; let events = rows .iter() .map(|row| serde_json::from_value(row.get("payload"))) .collect::, _>>()?; Ok(events) } ``` The imperative shell takes care of loading exactly what's needed from your event store. Then it passes the loaded, immutable events into your pure decision logic, which has no direct access to databases or external systems. The pure logic remains side-effect-free, predictable, and straightforward to test. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/06/Imperative-Shell-and-Pure-Logic-Separation.png) Imperative Shell and Pure Logic Separation ## Benefits of Clear, Pure Domain Logic At this point, you might wonder: is all this effort around explicit rules, pure decisions, and feature-related contexts really worth it? After all, it's quite a shift from the object-oriented ways of thinking. But here's the good news: adopting this simpler, more explicit approach to domain logic has clear and tangible benefits. First, there's testability. Because rules and decisions are pure and side-effect free, testing becomes straightforward and fast. There’s no complicated mocking, no database connections, and no cumbersome setups. Testing a decision simply involves passing immutable data in and confirming the expected result comes out. Clear rules mean your tests directly reflect business scenarios, making them valuable and relevant. Next, think about maintainability. If domain logic explicitly reflects the actual business rules provided by domain experts, the code naturally stays in sync with business needs. If a rule changes (say, the conditions under which devices can bind to assets) the exact place in your code to adjust is immediately clear. Changes in business logic no longer cascade unpredictably through complicated object graphs; instead, they're clearly defined and simple to manage. Finally, consider debugging and reasoning about your software. When unexpected behavior arises, finding its source is intuitive and fast. You can replay exactly the same events, observe explicitly how your rules evaluate these events, and clearly understand the resulting decisions. There's no hidden state, no buried logic, just clear, visible steps you can easily follow. Overall, clear and pure domain logic doesn't merely simplify code. It enhances your ability to reason about your system. It improves collaboration, aligns developers and domain experts, and ultimately creates software that’s robust, flexible, and deeply aligned with real-world needs. *Cheers*! ### ### Aggregateless Event Sourcing URL: https://ricofritzsche.me/aggregateless-event-sourcing/ Last updated: 2025-06-24T09:21:20.000Z When building systems using Domain-Driven Design (DDD) and Command Query Responsibility Segregation (CQRS), aggregates are usually at the core. An aggregate groups related entities and value objects into a single unit. This unit ensures consistency and enforces business rules through transactional boundaries. In theory, this makes perfect sense. But reality shows a different story. Aggregates introduce complexity. They quickly become outdated and force constant refactoring. The reason is that business processes naturally change, but aggregates tie data and logic together too tightly. When something changes in your business, the aggregate needs adjustment, even if the change seems small . For example, consider an **Asset Tracking** system managing devices and assets. Initially, two aggregates might seem logical: one aggregate for **Assets** and another for **Devices**. Each aggregate independently manages its lifecycle and business rules. The **Asset** aggregate ensures rules like "*only one device per asset at a time.*" The **Device** aggregate ensures rules like "*a device cannot be bound to multiple assets simultaneously.*" ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/06/Device-Asset-Aggregates-1.png) Binding a device to an asset crosses aggregate boundaries. But here’s the problem: Binding a device to an asset crosses aggregate boundaries. To handle this properly, developers introduce domain services or additional events, creating unwanted complexity and indirect coordination. Even worse, aggregates often blend technical management (device lifecycle) with pure business logic (asset tracking). Mixing these concerns makes it difficult to evolve each independently. Real business scenarios rarely match static consistency boundaries set by aggregates. Your initial aggregate boundaries quickly prove either too coarse or too fine, forcing constant and costly refactoring. Aggregates become complicated to maintain and difficult to evolve naturally. Developers and business experts both feel frustrated, struggling to clearly express the actual behavior of the business. Instead of focusing on business logic, everyone spends energy on handling overly rigid structures. The real issue is not aggregates themselves, but **consistency**. Aggregates were invented as a solution to maintain consistency. But they force us into rigid structures that rarely match evolving real-world needs. So, the right question isn’t how to define better aggregates. The right question is: **What exactly do we want to keep consistent?** ## Rethinking Consistency: It's All About Context If aggregates themselves aren't the real problem, what is? The true problem is consistency. Aggregates exist because consistency matters. But consistency doesn’t depend on aggregates or entities. It depends on context. ### What exactly is context? In object-oriented approaches, context is an object network. In relational databases, it's a set of data records. But what if context was simpler? Instead of complex object graphs or rigid data structures, what if context was defined only by the facts we need for a specific decision? Imagine you want to bind a device to an asset. What information do you really need? You need to know: - Does the asset exist and isn't retired? - Does the device exist and isn't retired? - Is the device currently free (not bound to another asset)? This defines your context. You don't need anything else. You don't care if the asset was renamed or if the device was previously retired and then activated again. These details are irrelevant for this specific decision. In practice, context is simply the set of facts (events) relevant to your decision. You define context by querying events. If the context remains unchanged since you checked it, you apply your changes. If not, you reconsider. It's as straightforward as that. But there's one important point: Since the context check takes some time, especially in distributed systems, the underlying context might have changed between checking and applying changes. To handle this, the check and the change must happen together. This combined step must be atomic. Atomic means the query and insert happen as a single, indivisible step. With that, we completely remove **rigid** entities and aggregates from our mental model. There's no object, no object graph, there are only data structures (events, commands) and pure functions (decisions). ## Pure Events: Facts without Boundaries In traditional aggregate-based systems, events are closely tied to aggregates. The event reflects the state or changes within the aggregate. This coupling makes events less flexible. An event like *DeviceBoundToAsset* could be duplicated across multiple aggregates, each with its own interpretation and state management. This duplication creates complexity and rigidity. But what if events could stand on their own? What if events were simply facts, pure and independent from any aggregate or rigid entity? This is exactly what we aim for with *aggregateless* event sourcing. We remove the notion of an event belonging to an aggregate entirely. Instead, an event simply records a fact that occurred: no assumptions, no boundaries, just facts. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/06/Pure-Events-vs-Aggregates.png) Pure Events vs. Aggregates Here’s what a pure event looks like in practice: ```json { "DeviceRegistered": { "deviceId": "550e8400-e29b-41d4-a716-446655440000", "registeredAt": "2025-06-22T10:00:00Z" } } ``` This event fully describes itself. It references an entity (the device) via its identity (ID), but it doesn't depend on aggregates, external context, or additional tags to convey its meaning. It clearly conveys the fact: a device with a certain ID was registered at a certain time. Similarly, here's another example without an explicit identifier: ```json { "SystemUpdated": { "version": "v2.0.5", "updatedAt": "2025-06-22T11:30:00Z" } } ``` This is another pure event. It has no associated entity, yet it fully describes what happened. Pure events simplify your system dramatically: - Events can be easily reused and interpreted by different feature slices independently. - There's no duplication across aggregates. - New behaviors or decisions can emerge at any time from existing events without needing structural refactoring. When events become pure facts, recorded chronologically, your consistency boundaries become flexible and simple. They depend entirely on what your current decision or action requires. A common question: if an event doesn't reference an aggregate or entity explicitly, how do we identify what it relates to? The answer is simple: you don't need tags or IDs in every scenario. The event itself already includes all information required. When needed, the context is always defined through querying the facts stored in your event log. Tagging IDs explicitly isn't necessary because the event itself is already self-descriptive. Even scenarios without explicit identifiers still fully convey their meaning through pure data. This naturally reduces complexity, as there's no need for additional indexing or indirect referencing. ## Entities Revisited: Context, not Rigid Representations You might wonder, if we still have IDs in events, aren’t we still dealing with entities? Conceptually, yes! But importantly, these entities are not rigid objects with fixed representations. This is exactly the pitfall of object-oriented thinking: it tries to define entities as static, uniform structures: “*A device is always a device, with the same attributes everywhere.*” In practice, entities like a device aren't rigid. They're flexible, context-dependent manifestations based on the needs of each individual decision or command. Sometimes a device is just an *ID, name* pair, other times it’s *ID, longitude, latitude*, or even entirely different attributes. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/06/Entities-Revisited_-Context--not-Rigid-Representations.png) Entities Revisited: Device in different contexts Every feature slice, every command, and every projection shapes its own representation of an entity. There is no single, canonical, or rigid definition enforced everywhere. This dynamic view clearly aligns with the idea of pure events and contexts defined by queries. It frees your system from rigid object-oriented representations, allowing natural adaptation to evolving real-world scenarios. ## Handling Concurrency Simply and Clearly Without aggregates, how do we handle concurrency and consistency? The traditional aggregate approach enforces concurrency through strict transactional boundaries. If we remove aggregates, we lose those boundaries. Does this create a problem? Actually, no. It simplifies things significantly. Instead of aggregates, consistency is managed through event queries. When a decision must be made, your context is clearly defined by the set of relevant facts you query. This query spans the context based on events. After querying, you make a decision. But there's one key requirement: **You must ensure the context remains unchanged from the moment you read it until you record the new event.** Here’s a practical example. Imagine you want to bind a device to an asset. You query events like these to build your context: ```sql SELECT * FROM events WHERE event_type IN ('DeviceRegistered', 'AssetRegistered', 'DeviceBoundToAsset', 'DeviceUnboundFromAsset') AND ( payload->>'deviceId' = '550e8400-e29b-41d4-a716-446655440000' OR payload->>'assetId' = '660e8400-e29b-41d4-a716-446655440000' ) ORDER BY sequence_number; ``` Based on this query, your system sees whether the device and asset exist and if the device is free for binding. After making your decision (e.g., deciding it's safe to bind), you apply your new event or events, but you apply it atomically. That means your insert operation checks again, ensuring no new events have appeared that would change your context. This ensures consistency clearly and simply. It removes complex locking mechanisms or pessimistic concurrency approaches. Instead, consistency is entirely about facts and queries. Here’s the practical step clearly stated again: 1. Query events defining your context. 2. Make your decision based on these events. 3. Atomically insert your new event, verifying the context hasn't changed. If the context changed meanwhile, you simply retry your decision. There's no complicated locking or shared aggregate state. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/06/Atomic-Concurrency-check.png) Ensuring consistency Consistency isn't about aggregates; it's about ensuring the context remains unchanged during decision-making. Since this context definition comes purely from queries against recorded facts, consistency becomes a straightforward atomic check. This is a natural fit for distributed systems. This simple and clear approach reduces overhead. It lets your feature slices remain truly independent. Each feature clearly defines and manages its consistency without complex orchestration or hidden dependencies. New feature slices or new decisions within slices are easy to add. They don't affect or break existing behavior. ## Pure Logic, Pure Persistence: Truly Independent Feature Slices One major benefit of removing aggregates is the natural separation of logic and persistence. Events become pure persistence. Decisions become pure logic. They live separately but complement each other clearly. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/06/TriggerDecisionFact-1.png) This perfectly fits the principles of functional programming, which separates pure logic from side effects (like persistence). **Events represent side effects: things that happened.** **Logic represents decisions: what should happen based on current facts.** In practical terms, events are stored in a simple, chronological event store. There’s no assumption about how events will be used. You don't predefine rigid structures or schemas. You simply store events as they happen: ```sql CREATE TABLE events ( sequence_number BIGSERIAL PRIMARY KEY, -- total ordering occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(), event_type TEXT NOT NULL, payload JSONB NOT NULL, metadata JSONB NOT NULL DEFAULT '{}' ); CREATE INDEX ix_events_type ON events(event_type); CREATE INDEX ix_events_payload_gin ON events USING GIN (payload); ``` On the logic side, feature slices define their contexts explicitly through queries. Each slice clearly determines what facts it needs to know to make decisions. Each feature slice is completely self-contained. It can evolve separately without affecting other slices. New slices or new logic within existing slices can appear anytime, easily consuming existing events. The system structure remains clear and straightforward. Logic remains pure, making testing easy and predictable. Persistence (event store) becomes an imperative shell that simply records facts. Logic remains pure, functional, and entirely free from side-effects. **The event store, therefore, becomes the *shared reality* for all feature slices, capturing everything that has happened in the system, clearly reflecting the sum of all business activity.** This pure separation finally lets us overcome the rigidity imposed by traditional aggregates and entities. Feature slices become truly independent, naturally adapting to real-world business changes. ## Embracing Simplicity: Just Clear Concepts and Practical Simplicity The concept is simple: - We remove aggregates, rigid entities, and domain services completely. - We define consistency purely through querying relevant facts. - We clearly separate pure logic (decisions) from pure persistence (events). Each feature slice remains clear and simple, making your entire system easy to understand and maintain. This simplicity naturally aligns with core architectural principles: - **Feature Slices**: Incremental evolution is clearly manifested in the code structure itself. - **Event Sourcing**: Pure facts are the simplest and smallest common denominator between slices. - **Command Query Separation (CQS)**: Separating state changes (commands) from state queries (projections) clearly identifies and isolates constraints. - **Functional Core, Imperative Shell (FCIS)**: Clearly separating consistency checks and domain logic from state changes and projections. Pure functions are then naturally applied to pure facts, projected specifically within each command context for clarity and simplicity. In my opinion, the real benefit is a system that stays flexible, understandable, and naturally aligned with evolving business needs. By **fully overcoming object-oriented thinking**, we build a simpler, clearer, and genuinely agile architecture. Introducing aggregates was a good intention, I guess, but they aren't necessary either. By clearly defining consistency through pure facts and queries, we achieve simplicity and flexibility far beyond what traditional approaches allow. It's time to shift from rigid object-oriented models to clear, flexible, and simple aggregateless event sourcing. Special thanks to [**Ralf Westphal** ](https://ralfw.de/?ref=ricofritzsche.me)for the insightful discussions, thoughtful critiques, and valuable ideas that helped clarify and refine this article. Read also Ralf's perspective on [this topic here](https://ralfwestphal.substack.com/p/command-context-consistency). *Cheers*! ### Beyond Aggregates: Lean, Functional Event Sourcing URL: https://ricofritzsche.me/functional-event-sourcing/ Last updated: 2025-06-13T09:29:06.000Z Event sourcing naturally aligns with functional programming principles. Why do I call it a functional programming principle? Because, it’s all about immutability. Once an event is recorded, it’s set in stone. It's a fact that can’t be altered. I’ve built systems myself following the tactical patterns as defined by Eric Evans in Domain-Driven Design and elaborated in the “Red Book” by Vaughn Vernon. These concepts are object-oriented constructs that combine data (state) and behavior (methods) to enforce business rules. They leaned heavily on indirection. The real goal is to design systems that remain clear, flexible and comprehensible, even when they are highly complex. **What I call “object-oriented” here.** I’m referring to the mainstream, state-plus-behaviour style popularized in enterprise Java/C# (big mutable objects that carry both data *and* methods) **not** Alan Kay’s original message-passing definition. The article pushes back on that specific flavor, not on objects or encapsulation in general. (***Read this twice!***) ## Weekly Tips to Master Software Architecture Newsletter I provide lean, effective software-architecture strategies that cut through the noise. Subscribe Email sent! Check your inbox to complete your signup. Trusted by 2,500+ developers on Medium. ## Beyond CRUD and Object-Models I get that many developers and teams stick to traditional relational data structures. Shifting to event-based thinking is a big mental leap. Most teams still think in CRUD and big object-models. Last year, I joined a logistics project late in the game, and the first thing that hit me was the focus on CRUD APIs and relational models. The actual data flow and processing? Barely mentioned. The push-back against event sourcing usually comes from seeing it buried under aggregate-heavy, object-oriented CQRS frameworks and endless arguments about which event store is “best.” My advice: skip the frameworks. Lightweight, expressive functions are all you need. Remember, CQRS is just Bertrand Meyer’s Command–Query Separation: a routine either changes state or returns data but never both. Layered designs such as Clean or Onion flip that on its head by slicing code along technical layers (entities, use cases, interfaces, infrastructure). Read and write logic end up side by side, muddying intent and locking you into rigid, horizontal stacks. Teams that try “vertical slices” often carry that baggage with them, so commands and queries still live in the same layers. However, this article isn’t about tearing down Clean Architecture’s flaws. It’s about event sourcing as the heart of behavior-driven software development. When Greg Young introduced CQRS, he used a killer real-world example: banking. It shows why event sourcing isn’t more common. Everyone gets that storing just the final balance of a bank account would be absurd. Imagine going to a bank, saying something’s off with your account, and the teller shrugging, “*I can only see your current balance, not the transactions that got you there.*” That’d be a disaster. Yet, in many business applications, we accept this approach. My goal with this article is to show that event sourcing doesn’t have to be complex. Quite the opposite. It’s all about how you approach it. **What you won’t find here:** - CRUD Repositories - Mainstream/Enterprise object-oriented principles - Frameworks trying to solve the problem generically **What you will find:** - Expressive code - Pure functions - I/O via imperative shell - Event-sourced architecture as a functional programming principle - A clear, feature sliced functional CQRS example ## Modeling with Pure Functions and Immutable Events A functional domain model comprises pure functions and immutable types. As the best parts of Domain-Driven Design teach us, it should be expressed in a language everyone on the project understands. For me, the core idea of an **event-sourced architecture** is simple: capture everything that happens in the system as immutable facts (events). No sophisticated framework is required, and you don’t need the stateful, over-engineered “aggregates” from DDD’s tactical design. It only adds needless complexity. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/06/Pure-Function.png) Each command is fed into a pure, deterministic function that always returns an immutable event. I will explain the I/O issues later in this article. Typical DDD aggregates as described in the "blue" and "red" DDD books bundle data and behavior into heavy object entity models, complete with repositories and layers of indirection. That pollution drags infrastructure concerns into your domain, tangling pure logic with I/O and interfaces. From my years building complex systems, I can say with confidence: object-oriented aggregates don’t tame complexity. **They create it!** ## Why I Avoid DDD Aggregates There’s a good reason I steer clear of the DDD term “aggregate” and its underlying concept, opting instead for a functional view of domain modeling. It simply doesn’t play well with fully self-contained feature slices. The issue comes up often when discussing Vertical Slice Architecture, especially among those stuck in an object-oriented mindset: where do you put the entities (an object model made up of nested objects and data structures)? If multiple feature slices rely on the same entity, they lose their independence. Alternatively, if one slice owns the entity, it ends up juggling multiple responsibilities, which defeats the purpose of slicing. Take a “Bank Account” as an example. In an object-oriented DDD approach, this entity would own all related operations, like depositing or withdrawing money, along with the state (e.g., balance) and rules (e.g., no negative balances). These operations are tightly bound to the entity, so you can’t split them into independent slices without creating problems. Either both the “Deposit Money” and “Withdraw Money” slices depend on the same entity, breaking their autonomy, or you cram both operations into a single slice, making it do too much. The object-oriented workaround is to centralize the entity and its logic in a shared model or service, leaving slices with just endpoints. That’s not slicing. It’s just a rehash of layered architecture with thin veneers. Let's try to solve this cleanly. Each slice (say, “Deposit Money” or “Withdraw Money”) operates on the same **event stream** for the bank account, which contains all events like *MoneyDeposited* and *MoneyWithdrawn*. To reconstruct the account’s state (e.g., the balance), a slice folds the stream’s events using pure functions. But the logic is fully separated: the “Deposit Money” slice handles deposit commands and generates *MoneyDeposited* events, while the “Withdraw Money” slice processes withdraw commands and produces *MoneyWithdrawn* events. This keeps slices independent, each focused on its own feature, without the object-oriented tangle of an aggregate which usually encapsulates data and behavior. That’s why I see event sourcing as a functional principle: it lets slices stay autonomous while sharing a single source of truth: the event stream. ## A Practical Example: Depositing Money with Functional Event Sourcing To bring event sourcing to life, let’s dive into the *deposit\_money* feature from a bank account example I built in Rust. This example shows how functional event sourcing can make feature slices truly self-contained, keeping everything (state, folding, and logic) right where it belongs. No centralized domain models, no object-oriented baggage, just pure functions and immutable events. I’ll walk through the code, zoom in on the “fold” concept for state reconstruction, and show why this approach beats the complexity of traditional designs. This functional event sourcing example in Rust shows precisely how a command → pure function → event flow works in an event-sourced architecture. ```rust use uuid::Uuid; use sqlx::{FromRow, Pool, Postgres, Transaction}; use crate::infrastructure::{ db::PostgresError, storage::{append_event, load_events}, }; use crate::events::{AccountEvent, AccountEventTrait}; fn default_version() -> String { "1.0".into() } #[derive(Debug, thiserror::Error)] pub enum ExecuteError { #[error(transparent)] Domain(#[from] DepositError), #[error(transparent)] Infrastructure(#[from] PostgresError), } #[derive(Debug, Clone, PartialEq)] pub struct AccountState { pub exists: bool, } pub fn fold_state(events: &[AccountEvent]) -> AccountState { AccountState { exists: events.iter().any(|e| matches!(e, AccountEvent::AccountCreated { .. })), } } #[derive(Debug, Clone, FromRow)] pub struct DepositMoney { pub account_id: Uuid, pub amount: f64, } #[derive(Debug, thiserror::Error, PartialEq)] pub enum DepositError { #[error("Account does not exist")] AccountNotFound, #[error("Amount must be positive")] NegativeOrZeroAmount, } pub fn process_command( state: &AccountState, cmd: &DepositMoney, ) -> Result, DepositError> { if !state.exists { return Err(DepositError::AccountNotFound); } if cmd.amount <= 0.0 { return Err(DepositError::NegativeOrZeroAmount); } Ok(vec![AccountEvent::MoneyDeposited { account_id: cmd.account_id, amount: cmd.amount, version: default_version(), }]) } ``` This code powers the *deposit\_money* slice, a self-contained module that handles everything needed to deposit money into a bank account. It’s a good example of how to keep domain logic pure and independent, with no reliance on shared models or infrastructure creeping in. Let’s break it down: - The *MoneyDeposited* event is defined in a minimal shared events module and captures the fact of a deposit with an account ID and amount. As functional programming demands, it is **immutable** and is part of the AccountEvent enum that is shared as a contract. - The State (*AccountState*) is a simple structure containing only an 'exists' flag, since deposits only need to know if the account already exists. It is owned by the slice rather than a centralized domain model. - *fold\_state* is a pure function that reconstructs the *AccountState* by checking whether an *AccountCreated* event exists in the stream. It is lightweight and local to the slice, thus avoiding shared folding logic. For deposits, we only care about existence, not balance. - The function validates the deposit command (i.e. that the account exists and the amount is positive) using a domain-specific *DepositError*. This keeps the core pure and free from infrastructure such as *PostgresError*, which remains in the shell. - The imperative shell (*fn execute*) ties the core to the infrastructure. It loads the event stream, converts it to determine the state, processes the command and adds events in a transactional manner, mapping domain errors to infrastructure errors as required. ```Rust // Imperative Shell pub async fn execute( pool: &Pool, command: DepositMoney, ) -> Result<(), ExecuteError> { let past = load_events(pool, command.account_id) .await .map_err(ExecuteError::Infrastructure)?; let state = fold_state(&past); let new_events = process_command(&state, &command) .map_err(ExecuteError::Domain)?; let mut tx: Transaction<'_, Postgres> = pool.begin().await.map_err(PostgresError::Sqlx)?; for ev in &new_events { tx = append_event(tx, command.account_id, ev, ev.event_type()) .await .map_err(ExecuteError::Infrastructure)?; } tx.commit().await.map_err(PostgresError::Sqlx)?; Ok(()) } ``` The code above represents the imperative shell. It is an outer layer that performs side-effects (DB calls, HTTP, etc), then hands pure data to the functional core and persists the events it gets back. State and I/O can’t be wished away, so the model is functional core + imperative shell: pure functions in the center, side-effects pushed to the outer rim. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/06/FCIS.png) Functional Core / Imperative Shell A command enters the shell, which loads past events, hands them to pure functions (fold -> process/decide), and then appends the new events back to the event store. **External reads (I/O).** Any data that isn’t in the stream, i.e. credit limits, holiday calendars, FX rates, etc., is fetched in the imperative shell before the pure core runs and is passed in as plain values. This keeps the core referential transparent while still solving real-world look-ups. **About the Repository pattern.** *load\_events* and *append\_event* are the repository here, but reduced to two explicit I/O functions in the shell. So yes, conceptually we do have a repository in the Fowler/Evans sense: there is still a boundary where domain state is re-hydrated and persisted atomically. But the difference is that I chose to expose that boundary as two very small, explicit I/O functions instead of a collection-like interface that returns rich objects. **Optimistic concurrency.** Each *append\_events* call runs in a single DB transaction. The composite primary key (account\_id, seq\_no) makes concurrent inserts collide; the duplicate-key error is surfaced as ConcurrencyConflict → HTTP 409\. (The seq\_no is simply SELECT COALESCE(MAX(seq\_no),0)+1 for that account, good enough for the demo, swap in an *expected\_version* check for production.) ```sql CREATE TABLE account_events ( id UUID PRIMARY KEY, account_id UUID NOT NULL, event_type VARCHAR NOT NULL, payload JSONB NOT NULL, sequence_number BIGINT NOT NULL, -- stream offset created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT unique_account_sequence UNIQUE (account_id, sequence_number) ); ``` **Note on aggregates.** *AccountState* is the aggregate/consistency boundary: every command folds the event stream for that *AccountId* and decides from there. What changes is the shape. Each vertical slice reconstructs only the fields it needs, so no monolithic *BankAccount* object graph travels through the codebase. *Note: If you never heard about the concept of Functional Core/Imperative Shell (FCIS) I recommend to read this article:* [Simplify & Succeed: Replacing Layered Architectures with an Imperative Shell and Functional CoreStreamline Testing by Eliminating Mocks and Focusing on Pure Functions![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/icon/rico-webseite.jpg)Rico FritzscheRico Fritzsche![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/thumbnail/Business-People-Using-Computer-Working-Concept-522795920_2453x1227.jpeg)](https://ricofritzsche.me/simplify-succeed-replacing-layered-architectures-with-an-imperative-shell-and-functional-core/) ## The Power of Folding In the *deposit\_money* feature, the *fold\_state* process is straightforward: it scans the *AccountEvent* stream to see if an *AccountCreated* event exists, setting s*tate.exists* to true if found. For a stream such as \[AccountCreated, MoneyDeposited(100), MoneyWithdrawn(30)\], folding yields an *AccountState* with exists set to true. The process is pure and predictable, and is owned by the feature slice, no shared folding module is needed. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/06/Event-Stream.png) Event Stream folding process Compare this to the *withdraw\_money* slice, which requires both existence and balance checks. Its *fold\_state* function processes all events *AccountCreated* sets exists to true, *MoneyDeposited* adds to the balance and *MoneyWithdrawn* subtracts yielding *AccountState*: ```json { exists: true, balance: 70.0 } ``` Each slice tailors its folding to its needs, keeping logic local and focused. This is what makes slices truly self-contained: they own their state reconstruction rather than relying on a centralized object model to dictate how events are applied. This approach strikes the right balance between autonomy and simplicity. The deposit\_money slice owns its own folding, state and error logic. The only shared element is the '*AccountEvent*' enum, which is a minimal contract and not an object model. By keeping fold state local, we avoid the anti-pattern of centralized logic, allowing each slice to decide how to interpret the event stream. The approach is functional to the core, with pure functions, immutable events and no infrastructure leaking into the domain. Contrast this with an object-oriented DDD setup, where a *BankAccount* entity would bundle all the logic, forcing the slices to depend on it or become bloated with multiple tasks. Here, folding is expressive because you can see exactly how *deposit\_money* checks account existence, nothing more, nothing less. There are no aggregates, no repositories and no indirection. There is just a self-contained slice doing its own thing, backed by a shared event stream that keeps everything consistent. This is why I prefer functional event sourcing for building systems that are easy to understand, even at scale. ### Deterministic Tests - No Mocks, No Maybes Functional event sourcing turns testing from a chore into a two‑line assert. Because the core is nothing but pure functions, you hand it input, collect the output, and you’re done. No fake databases, no DI contortions, no side effects. ```rust #[cfg(test)] mod tests { use super::*; use uuid::Uuid; #[test] fn process_command_success() { let id = Uuid::new_v4(); let state = AccountState { exists: true }; let cmd = DepositMoney { account_id: id, amount: 42.0 }; let evs = process_command(&state, &cmd).expect("should succeed"); assert_eq!(evs.len(), 1); if let AccountEvent::MoneyDeposited { account_id, amount, version, .. } = &evs[0] { assert_eq!(*account_id, id); assert_eq!(*amount, 42.0); assert_eq!(version, "1.0"); } else { panic!("unexpected event"); } } } ``` One input, one expectation. The test is repeatable, instantaneous, and survives refactors because it speaks the domain’s language: deposit money, get a *MoneyDeposited* fact. The shell (database, transactions, I/O) is tested separately with thin integration tests; the core stays pure and extremely fast. That’s confidence without the mocking circus. ## Conclusion Event sourcing is older than software. Accountants, doctors, and lawyers have always kept an *“append-only ledger”* so nothing gets lost in translation, or in history. What matters to us as developers is the translation of that timeless idea into code. **Functional fit, not dogma!** Recording immutable facts and replaying them with a *fold* lines up perfectly with pure functions and immutability, but that doesn’t mean the whole codebase must be pure FP. Keep the core referentially transparent; let the shell do the I/O. The goal isn’t to enforce a new dogma; it’s to keep the original elegance of the ledger while writing code that is easy to reason about, test, and evolve. Strip away the over-engineering, keep the ledger, and let the rest be just enough plumbing to ship. Finally, we should now focus on flexibility with regards to requirements. The good thing is you don’t need a framework for CQRS/ES; keep the plumbing thin so all these trade-offs stay visible. Take a look at the complete Bank Account example, including the fold logic, *process\_command* and execute shell functions, as well as a full suite of deterministic unit tests, all of which are available on GitHub. [View the full Rust example and tests on my GitHub](https://github.com/ricofritzsche/fcis-event-sourcing-rust?ref=ricofritzsche.me). *Cheers*! #### ### Getting Started in Real-Time: Commands, Events, and Brokers Demystified URL: https://ricofritzsche.me/getting-started-in-real-time-commands-events-and-brokers-demystified/ Last updated: 2025-06-04T12:33:50.000Z In team discussions, I’ve noticed some confusion about messaging terminology. Terms like “request,” “response,” “command,” “event,” and “topic” often come up, but everyone seems to have a slightly different understanding of what they mean. That’s why I write this article. I want to explain the core concepts of messaging from common message types to the key differences between synchronous and asynchronous messaging, and also point-to-point (queues) and fanout (topics) in message brokers. By the end of this read, you’ll learn how commands, replies, and events serve distinct purposes in distributed systems, and why brokers are essential for decoupling producers from consumers.We’ll explore what makes point-to-point and fanout scenarios unique, helping you and your team work more confidently with messaging systems. ## Weekly Tips to Master Software Architecture Newsletter I provide lean, effective software-architecture strategies that cut through the noise. Subscribe Email sent! Check your inbox to complete your signup. Trusted by 2,500+ developers on Medium. ## Synchronous vs. Asynchronous Messaging When talking about messaging, it’s easy to assume that everything is asynchronous by default.But in practice, many communication patterns rely on synchronous calls, where one party waits for the other to respond immediately.Let’s break down both approaches. ### Synchronous Messaging Synchronous messaging follows a request/response model similar to a traditional function call or HTTP request.The caller sends a message or makes a request and then blocks. It waits until it receives a response. The two parties become closely linked. One can’t really do its job without the other responding in real time.That tighter coupling can be just what you need for simple use cases, but it becomes trickier when systems get larger or the work grows more complex. For instance, a HTTP POST request for *RegisterAsset* would be considered synchronous if the client can’t proceed until the server responds. ### Asynchronous Messaging Asynchronous messaging allows the sender to fire off a message without waiting for an immediate response.The sender can continue doing other work, while the receiver processes the message at its own pace. If a reply is needed, it arrives via a separate message channel. This pattern comes into its own when you need to handle large bursts of activity or let services work at their own pace. It’s also essential for real-time notifications, where an event is triggered and anyone interested can respond immediately, without the need for constant polling.The trade-off is that you have to plan for scenarios where messages may arrive out of order or some services may be temporarily offline.But the upside is a more resilient, loosely coupled system that keeps moving even if some parts slow down. ### Comfort and Habit In my day-to-day work, even with large enterprises, I am always surprised at how strong the synchronous mindset still is. Even though message-based systems have been around for a long time, many teams stick to request-response APIs and end up polling for updates. In most cases, it’s not that developers don’t see the value of asynchronous approaches; it’s more about comfort and habit; synchronous calls are familiar, and the tooling around them is ubiquitous. But every time I see a service repeatedly calling an endpoint to find out if something has changed, I can’t help but wonder how much time and network overhead we’re wasting. There are so many more efficient ways to push updates in real time, and I’m still waiting for that to become the default in every organization. ## Common Message Types Conversations about messaging often jump straight to how messages move through queues, topics, or other infrastructure. But before we look at any of that, it’s worth clarifying what is actually being passed around.In most systems, we can group messages into three common types: **commands**, **replies**, and **events**. Knowing the differences helps everyone on the team speak the same language and design consistent, predictable message flows. ### Commands (Imperative Requests) Commands are all about intent: “do this” or “perform this action”. When you send a command, you’re telling another service or component to do something. For example, you might issue a *RegisterAsset* or a *BindDevice*. Each command makes a request for a specific piece of work, which may or may not succeed. Because commands are imperative by nature, they set expectations: when a client sends *RegisterAsset*, the client expects an asset to be registered somewhere. ![](https://miro.medium.com/v2/resize:fit:700/1*zSkORGF7h61II7fQB4Of6Q.png) In a synchronous setting, commands typically map one-to-one to request/response calls (like an HTTP POST). They are requesting an action. Commands can be accepted, rejected, or processed in various ways. In an asynchronous system, commands are enqueued and processed at the receiver’s pace; the sender doesn’t block while waiting. But the essence is always the same: **the system should do something in response.** ### Replies (Responses or Outcomes) Replies come into play when the sender needs to know **what happened** after issuing a command. They confirm whether an operation was successful, failed, or encountered some exception. For instance, a *RegisterAssetResponse* might include a success flag and the ID of the newly registered asset, or it could provide an error code if something went wrong. ![](https://miro.medium.com/v2/resize:fit:700/1*xmT3MR0aPg_25dflwjHImg.png) Replies are straightforward in a **synchronous** system: you send a command, and the reply arrives in the same connection right away. In an **asynchronous** setup, the reply is usually a separate message traveling back through another channel. By decoupling the request and response channels, the receiver can process commands at its own pace and send back replies whenever it’s done. ### Events (Facts) Events announce that “something has happened”. They don’t ask a system to do anything, but rather let anyone who is interested know that a certain event has occurred, such as *AssetRegistered*. Once published, events are simply out there for anyone to consume. This publish/subscribe paradigm means that the event producer doesn’t have to worry about who needs the information. He simply broadcasts the fact, and any number of subscribers can respond in their own way. ![](https://miro.medium.com/v2/resize:fit:700/1*1eZrUFPjpXd0xGGDK7Jwvg.png) Message Type: Event Events are perfect for scenarios where multiple services or components need the same information, but use it differently. One microservice might update a dashboard, another might trigger a notification email, and yet another might record analytics. Each subscriber gets the full event, making the system more extensible: you can add new event handlers later without touching the original event producer. ## Immutability of Messages Here is a really important principles in messaging: > Messages are not subject to change. In simple words, an immutable message is written in stone. Once created and sent, it can’t be changed afterward. This concept might seem strict at first, but it solves a host of potential issues and makes your system more predictable. **Audit and Traceability** When messages remain unchanged after they’re published, you have a crystal-clear audit trail. If you ever need to debug a problem or reconstruct a sequence of events, you can trust that the messages you retrieve accurately reflect what was originally sent. No one can go back and rewrite history. **Consistent State Across Services** In a distributed system, different services might process the same message at different times. If the content of a message could be altered mid-flight, some services might see one version while others see a different version. By locking the message content, you ensure that every subscriber is operating on the same, unchanging information. **Easier Reasoning About Data Flow** Immutability reduces the mental overhead of wondering, “Which version of the message am I handling?” or “Did this get updated after I subscribed?” When messages can’t be changed, what you see is what you get. The message you read today will look identical tomorrow, next week, or next year, making event replay (common in streaming systems) far more reliable. ### Simple Example of an Event in C# If you’re using C#, one way to enforce immutability is to use record types or classes with read-only properties: ```csharp public record DeviceBound { public required Guid MessageId { get; init; } public required Guid AssetId { get; init; } public required Guid DeviceId { get; init; } public required DateTime BoundAt { get; init; } public required Guid CorrelationId { get; init; } public required string Source { get; init; } public DateTime CreatedAt { get; init; } = DateTime.UtcNow; } ``` Once you construct the *DeviceBound* event, you cannot change the data values. If you wanted to change something, you’d publish a new event instead. This mirrors what happens in the real world: once you declare a fact, such as “*Asset 123 was bound to device 5678 at 3:00 pm*”, you don’t go back and change it; you send out a **new event** when the situation changes. ### Shift to a “New Fact” Mentality Adopting immutability pushes you to think in terms of facts rather than states. Instead of saying, “Update the existing message so it’s always current,” you say, “Publish a new message that reflects the latest truth. This perspective is at the heart of event sourcing and event-driven architectures. Instead of carrying around a single mutable state, you accumulate a set of immutable facts that accurately represent how your system evolves over time. Keeping messages immutable is a small design choice with a big impact. ## Message Brokers One of the most important concepts in asynchronous systems is the message broker. A broker sits in the middle between producers and consumers (also called receivers), acting like a post office that accepts messages and delivers them to the right place. Because producers and consumers don’t talk directly to each other, they can evolve independently. A producer only needs to know how to send messages to the broker, not which specific consumers are listening or how many there are. ### Point-to-Point (Queues) In a point-to-point model, messages go into a queue. Each message is then consumed by exactly one receiver. If multiple consumers are listening to the same queue, the broker’s job is to distribute the messages among them, with each piece of work being handled by exactly one consumer instance. A practical example might be a geofencing service that needs to handle individual “asset entered geofence” events. Each time an asset crosses a boundary, a message is queued. ![](https://miro.medium.com/v2/resize:fit:700/1*-_b8_F_yyAZTYNbYo48v3w.png) Point-to-Point Model. **Only one instance**of the geofencing consumer will pick up each message, perform any necessary logic (such as alerting, logging, or updating a map), and then acknowledge completion.This setup scales well because you can spin up more consumers when you have a spike in geofencing events, and the broker automatically balances the load. ### Fanout (Topics) In a fanout, or publish-subscribe model, messages are published to a topic. Any number of consumers can listen to that topic, and each consumer gets a copy of each message. Topics are perfect for scenarios where multiple services need to respond to the same event, such as an *AssetRegisteredTopic* announcing that a new asset has been added. ![](https://miro.medium.com/v2/resize:fit:700/1*kJJgGzwUALGMEhM1dpFV9A.png) Fanout model. Because each consumer does different worker, the event can trigger multiple tasks in parallel. The original producer doesn’t need to know who is subscribing or how the data will be used. The producer simply sends the *AssetRegistered* message to the topic. This flexibility enables decoupling: new services can join the topic whenever they need to act on the same event, without changing the publisher or existing subscribers. ## Bringing It All Together Imagine a REST API POST endpoint for registering assets. From the outside, this flow appears synchronous: the caller makes an HTTP request to POST /assets, and once the asset is successfully registered, they immediately receive a reply indicating success. Here’s what it looks like: **Client Sends a Command** The client calls your REST endpoint, effectively issuing a synchronous command*RegisterAsset*. Because it’s HTTP-based, the client awaits a response (success, failure, or error). ```csharp public record RegisterAssetRequest : AuthenticatedRequest { public required string Name { get; init; } public string? Description { get; init; } public Dictionary? Attributes { get; init; } } ``` **Synchronous Reply** Once the service has finished registering the asset in the database, it returns a response, usually a JSON response with details such as the new asset ID. At this point, the caller knows that the asset has been successfully registered. ```json { "value": { "id": "345abc89-410b-4005-912a-4952f41cb878" }, "statusCode": 201, "location": "/assets/345abc89-410b-4005-912a-4952f41cb878" } ``` **Asynchronous Event Publication** The same service immediately publishes an *AssetRegistered* event to a topic. It doesn’t wait for a specific service to consume it; it simply broadcasts the fact that “Asset X was registered at time Y”. This event is immutable and can be saved or processed later by any interested subscriber. **Subscriber Reactions** Other services (feature slices, microservices, serverless functions, etc.) subscribe to the *AssetRegistered* topic. One updates a search index, and maybe another logs analytics. They each receive the exact same event and handle it in their own way, independent of each other and independent of the original request.. ![](https://miro.medium.com/v2/resize:fit:700/1*gXaNe_DzFoxOUT7wBUrSaw.png) Register Asset example. ### Loose Coupling and High Cohesion The command (the incoming POST request) and its response remain narrowly focused on the “register asset” operation. They don’t need to know what else is happening in the system. This is highly cohesive: all the logic associated with registering an asset lives in a feature slice, from input validation to saving to the database to publishing the event. At the same time, other services that need this data (e.g., search indexing, notifications) are loosely coupled to the original service because they rely on the published event rather than a direct synchronous call. They don’t need to respond immediately, nor do they block the registration process. Each service can evolve independently as long as they all agree on the event contract (i.e., the schema of the *AssetRegistered* event). ## Conclusion Shifting from a synchronous mindset to an event-driven one isn’t just about reducing polling; it’s about building systems that stay responsive under load, scale more gracefully, and remain easier to evolve over time. By understanding the differences between commands, responses, and events, and by using brokers for point-to-point and fan-out scenarios, you lay the foundation for a resilient, decoupled infrastructure. Messages become the system’s source of truth, helping you accurately track changes and allowing each service to move at its own pace. Ultimately, using asynchronous messaging means fewer bottlenecks, more real-time insight, and an easier path to growth as your application and team expand. *Cheers*! This article was originally published on [here](https://medium.com/gitconnected/getting-started-in-real-time-commands-events-and-brokers-demystified-c85473fff402?ref=ricofritzsche.me). ### Pure Functions and Immutable Data: Simplifying Complexity by Design URL: https://ricofritzsche.me/pure-functions-and-immutable-data-simplifying-complexity-by-design/ Last updated: 2025-06-02T11:08:58.000Z Most complexity in software development doesn't arise from inherently complicated business logic. It emerges from the uncontrolled ways in which we manipulate data and perform side effects. For decades, we've treated data as mutable, stateful objects, living entities with changing behaviors. This object-oriented perspective created an entire ecosystem of accidental complexity, leading developers into tangled states, unpredictable behaviors, and frustrating bugs. In one of my latest articles on [Functional Core and Imperative Shell](https://ricofritzsche.me/applying-functional-core-and-imperative-shell-in-practice/), I explained how isolating side effects makes your business logic simpler and cleaner. Today, I’m diving deeper into the heart of functional programming: pure functions and immutable data. These two foundational concepts have profoundly reshaped my approach to solving complex domains. By embracing them, we can design software that's simpler, more predictable, and truly maintainable. ## Weekly Tips to Master Software Architecture Newsletter I provide lean, effective software-architecture strategies that cut through the noise. Subscribe Email sent! Check your inbox to complete your signup. Trusted by 2,500+ developers on Medium. ## Why Pure Functions Matter First, let’s establish clearly what a **pure function** is. A pure function is a function that satisfies two critical conditions: - **Deterministic**: Given the same inputs, it always produces the exact same outputs. - **Side-effect free**: It does not alter any external state or depend on any external mutable state. Consider this simple example: ```Rust fn add(a: i32, b: i32) -> i32 { a + b } ``` This function is pure. Every time you call the function *add(3, 2),* the answer is *5*. It doesn't matter if it's today, tomorrow, or running concurrently across 100 threads. The result is consistent. Pure functions promise absolute predictability. Contrast this with a non-pure function: ```Rust static mut TOTAL: i32 = 0; fn add_to_total(a: i32) { unsafe { TOTAL += a; } } ``` Here, each call modifies a global state *TOTAL*. Suddenly, predictability vanishes. Call this from multiple threads, and you're headed straight into race conditions and confusion. Pure functions remove this entire class of problems from your code. ### Pure Functions Simplify Testing and Debugging Testing pure functions is straightforward: - No mocks, no stubs, and no external setup. - Just inputs and outputs. Testing the earlier pure function is trivial: ```Rust assert_eq!(add(2, 3), 5); ``` That’s it. Testing becomes predictable and transparent. Eric Normand, in his excellent book [*Grokking Simplicity*](https://www.amazon.de/dp/1617296201?ref=ppx%5Fyo2ov%5Fdt%5Fb%5Ffed%5Fasin%5Ftitle), emphasizes that pure functions simplify code precisely because they avoid hidden side effects. **Every action is explicit, every outcome predictable.** This is critical for controlling complexity. This explicitness makes debugging substantially easier. Bugs frequently originate from unexpected mutations and state changes. By eliminating mutable state from your core logic, your functions become transparent, predictable, and easy to reason about. ## Immutable Data: The Essential Partner to Pure Functions Pure functions are great but incomplete without immutable data. Data immutability means: - Once created, data never changes. - Modifications result in a new data instance; the original remains untouched. Immutable data forces you to shift your mindset away from *stateful object* towards *data snapshots*. Here's an immutable data structure in Rust: ```Rust #[derive(Clone)] struct User { id: i32, name: String, active: bool, } impl User { fn deactivate(&self) -> Self { Self { active: false, ..self.clone() } } } ``` Check the full code here: [https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=1a71ff607802c89ba79b8ae0d8f32b19](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=1a71ff607802c89ba79b8ae0d8f32b19&ref=ricofritzsche.me) The *deactivate* method does not alter the original *User*. Instead, it returns a new instance with slight modifications. The original *User* remains unchanged and stable. Contrast this with mutable state: ```Rust struct MutableUser { id: i32, name: String, active: bool, } impl MutableUser { fn deactivate(&mut self) { self.active = false; } } ``` While mutability seems simpler at first glance, it's a trap. Mutable state creates complexity by: - Obscuring state transitions: What was the previous state? - Enabling unintended side effects: Multiple parts of code modifying the same data simultaneously. - Increasing cognitive overhead: Keeping track of the changing states is mentally taxing and error-prone. Eric Normand puts this succinctly: > “Mutability is complexity’s favorite hiding spot. Immutability, on the other hand, exposes state clearly, making it impossible for hidden interactions to cause chaos.” But what is the benefit of immutability? ## Real-world Benefits of Immutable Data and Pure Functions With immutable data, concurrent programming becomes dramatically simpler. Immutable data is inherently thread-safe, as there’s no chance for simultaneous threads to corrupt shared state. For instance, using Rust's ownership model: ```rust use std::sync::Arc; use std::thread; let user = Arc::new(User { id: 1, name: "Alice".into(), active: true, }); let handles: Vec<_> = (0..10).map(|_| { let user = Arc::clone(&user); thread::spawn(move || { println!("User active? {}", user.active); }) }).collect(); for handle in handles { handle.join().unwrap(); } ``` No locks, no race conditions, and no surprises. Every thread accesses a stable, immutable instance of data. Check out the prove in the playground: [https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=213075ee1ded41677f1175e42142a5c5](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=213075ee1ded41677f1175e42142a5c5&ref=ricofritzsche.me) Notice that no thread ever has mutable access to the '*User*'. If you tried to uncomment a line such as *user.active = false* within a thread, Rust would prevent you from doing so by issuing a compile-time error. Since '*active*' is simply a *boolean* value that never changes, each thread reliably prints '*true*'. This immutability, combined with [Arc](https://doc.rust-lang.org/std/sync/struct.Arc.html?ref=ricofritzsche.me), is precisely how you can guarantee 'no locks, no races, no surprises'. Immutable data inherently maintains historical states. When data changes result in new instances, past states remain accessible. This property naturally supports features like undo operations, event sourcing architectures and so on. ### Clear, Composable Logic Pure functions naturally compose, creating clarity even in complex domains. Consider how clear the composition of immutable data transformations becomes: ```rust fn update_name(user: &User, new_name: &str) -> User { User { name: new_name.to_string(), ..user.clone() } } fn deactivate_user(user: &User) -> User { user.deactivate() } // Compose transformations let user = User { id: 1, name: "Alice".into(), active: true }; let updated_user = deactivate_user(&update_name(&user, "Bob")); ``` Each step clearly defined, predictable, and immutable. No hidden interactions, just pure data transformation. Try it out here: [https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=b3a22512b65112d8dfe62990b3e2da3d](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=b3a22512b65112d8dfe62990b3e2da3d&ref=ricofritzsche.me) ## Moving Beyond Stateful Objects I've fully removed the concept of stateful data classes from my projects. Instead, each feature consumes immutable data and produces new immutable results: **Immutable data in → Pure functions → Immutable result out**. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/06/Screenshot-2025-06-02-at-12.54.24.png) This approach rejects the traditional OOP perspective that data is a stateful "thing." Data is simply information! Stable snapshots representing facts at given points in time. Recognizing this fact eliminates an entire category of complexity from our domain modeling. Initially, immutable data structures and pure functions may seem restrictive. However, constraints in design lead to simplicity. By deliberately choosing immutability and purity, we can prevent accidental complexity, and, most importantly, ensure predictable behavior. This makes code simpler to test, understand, and extend. Again, Eric Normand captures this brilliantly: > “Complexity doesn't disappear by accident. You tame it by consciously designing simpler solutions. Immutability and pure functions aren’t restrictive; they liberate us from hidden complexity.” ## Conclusion Shifting to pure functions and immutable data isn’t merely a technical decision. It's a profound philosophical shift in how you approach software design. It challenges deeply ingrained assumptions about data and state. Yet, it promises significant payoffs: - Clear, predictable business logic - Safer concurrency - Simple, mock-free testing - More maintainable codebases Complex domains don’t have to lead to complicated code. Pure functions and immutable data structures prove that complex problems can indeed be solved simply, elegantly, and reliably. As always, the goal remains clear: simplicity. Not simplicity for simplicity's sake, but because simplicity is the strongest ally in the battle against complexity. *Cheers*! ## ### Understanding Traits and Drop Glue in Rust URL: https://ricofritzsche.me/understanding-traits-and-drop-glue-in-rust/ Last updated: 2025-05-26T15:28:18.000Z In my recent exploration of Rust for high-performance geospatial applications, I've encountered several subtle but impactful language features. One that stands out prominently is Rust's handling of *impl Trait* , specifically when it interacts with [*drop glue*](https://doc.rust-lang.org/std/ops/trait.Drop.html?ref=ricofritzsche.me). Understanding this can significantly enhance the performance and accuracy of your high-performance applications, particularly with regard to asset tracking, geofencing and spatial data processing. ## What's Drop Glue? In Rust, when values go out of scope, they get dropped automatically. This "drop" involves cleaning up resources such as closing database connections or freeing memory. Sometimes Rust needs to insert extra code ("drop glue") to manage this correctly. While you usually don’t think about drop glue explicitly, certain patterns involving *impl Trait* can lead to subtle inefficiencies or unexpected behaviors. Let’s unpack this with examples relevant to geospatial systems, for instance. ## Example Scenario: Asset Tracking API Imagine an asset management API that tracks delivery vehicles' positions: ```rust trait GeoPosition { fn current_position(&self) -> (f64, f64); } struct Truck { id: String, lat: f64, lon: f64, } impl GeoPosition for Truck { fn current_position(&self) -> (f64, f64) { (self.lat, self.lon) } } fn get_asset() -> impl GeoPosition { Truck { id: "asset_123".into(), lat: 48.8566, lon: 2.3522, } } ``` *Try it in action:* [*https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=89253e8bcdceeac8fc4247aade216e4f*](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=89253e8bcdceeac8fc4247aade216e4f&ref=ricofritzsche.me) This function uses *impl Trait* to hide the concrete type *Truck*. It looks clean and intuitive. But what is happening behind the scenes? ## Weekly Tips to Master Software Architecture Newsletter I provide lean, effective software-architecture strategies that cut through the noise. Subscribe Email sent! Check your inbox to complete your signup. Trusted by 2,500+ developers on Medium. ## How Drop Glue Comes Into Play When returning *impl Trait*, Rust erases the concrete type at compile-time, which may result in additional runtime overhead for dropping the returned object (drop glue). While this overhead is typically minimal, understanding it can be beneficial, especially when handling large-scale geodata or many assets. Rust compiles the above function into an anonymous type internally, which might lead to hidden overhead when frequently called, as in live asset tracking scenarios. Importantly, drop glue is not only generated when a type explicitly implements the *Drop* trait. Rust will also generate drop glue for any type that contains fields which implement *Drop*, recursively. For example, the *Truck* struct includes a *String* field, which implements *Drop* to free its allocated memory, thus triggering drop glue even if *Truck* itself does not implement *Drop*. ## Practical Implication for Geofencing Let's look at my specific geofencing example: ```rust trait Geofence { fn contains(&self, lat: f64, lon: f64) -> bool; } struct CircularGeofence { center_lat: f64, center_lon: f64, radius_meters: f64, } impl Geofence for CircularGeofence { fn contains(&self, lat: f64, lon: f64) -> bool { let dist = haversine_distance(self.center_lat, self.center_lon, lat, lon); dist <= self.radius_meters } } fn active_geofence() -> impl Geofence { CircularGeofence { center_lat: 52.52, center_lon: 13.405, radius_meters: 1000.0, } } ``` *Play around here:* [*https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=7a05776364a737f11c6e54fd32599dc7*](https://play.rust-lang.org/?version=stable&mode=debug&edition=2024&gist=7a05776364a737f11c6e54fd32599dc7&ref=ricofritzsche.me) If you repeatedly call *active\_geofence()* in tight loops or real-time checks, you might unintentionally incur slight overhead, but this is **typically minimal, almost negligible in most cases.** For high-frequency checks, explicitly using concrete types or a boxed trait object can give more predictable performance. ## Alternative Approaches Here are a couple of straightforward solutions: 1. **Return Concrete Types Directly:** Instead of *impl Trait*, explicitly return concrete types if you know them. This avoids unnecessary drop glue overhead: ```rust fn active_geofence() -> CircularGeofence { CircularGeofence { center_lat: 52.52, center_lon: 13.405, radius_meters: 1000.0, } } ``` By returning a concrete type, you avoid the potential overhead associated with type erasure and drop glue generation. 1. **Use Explicit Boxing for Clarity:** If type flexibility is essential, explicitly box the trait object. Although there's still a small runtime cost, this approach clarifies the cost upfront: ```rust fn active_geofence() -> Box { Box::new(CircularGeofence { center_lat: 52.52, center_lon: 13.405, radius_meters: 1000.0, }) } ``` While boxing introduces a small allocation overhead, it makes the cost explicit and can be more predictable in performance-critical code. Alternatively, for types that are simple and bit-copyable, ensuring they implement *Copy* can guarantee no *drop glue*, as *Copy* types cannot have destructors. However, this is less common in geospatial applications where structs often contain non-*Copy* types like *String*. ## When Does This Matter? If your system scales significantly, managing thousands or millions of objects, these small performance hits can add up. Explicitly handling types can help you maintain predictable performance and minimize unnecessary complexity or overhead. In my geospatial applications, for instance, where you might be processing large datasets or performing real-time computations, even small performance overheads can become crucial. Profiling your application with tools like *perf* or Rust’s built-in profiling capabilities is key to determining whether these optimizations are necessary. ## Conclusion: Clarity Matters Rust's *impl Trait* provides convenience and clarity in many situations. However, understanding its interaction with drop glue empowers you to write clearer, more performant code in demanding applications like asset tracking. By understanding how *impl Trait* interacts with drop glue, you can make informed decisions about when to use abstraction and when to prioritize performance in applications. As always, measure and optimize based on real-world needs and avoid premature optimization but also unnecessary abstraction. *Cheers*! ### The Pragmatic Path: Embracing Simple, Lovable, and Complete Software URL: https://ricofritzsche.me/the-pragmatic-path-embracing-simple-lovable-and-complete-software/ Last updated: 2025-05-26T08:34:28.000Z ### In my previous article, ["Avoiding Over-Engineering: Focus on Real Problems in Software Development"](https://ricofritzsche.me/avoiding-over-engineering-focus-on-real-problems-in-software-development/), I highlighted the pitfalls of over-engineering, which sparked extensive and sometimes controversial discussions. Reflecting on these interactions, I realized there remains significant confusion around what exactly constitutes over-engineering and how to truly avoid it. Perhaps the best way to clarify my stance is to revisit and expand upon the concept of building products that are "[Simple, Lovable, and Complete (SLC)](https://ricofritzsche.me/my-journey-to-simple-lovable-complete-products/)." ## Weekly Tips to Master Software Architecture Newsletter I provide lean, effective software-architecture strategies that cut through the noise. Subscribe Email sent! Check your inbox to complete your signup. Trusted by 2,500+ developers on Medium. ### What I'm NOT Suggesting Let me clearly restate: advocating against over-engineering does not mean endorsing messy, poorly structured, or insecure code. Quite the contrary! Good software engineering requires thoughtful design, clear structure, and disciplined implementation from the outset. ### Understanding the Real Issue The core problem I'm addressing is the all-too-common scenario of teams adopting complex, cookie-cutter architectures or trendy frameworks in the mistaken belief that more complexity equates to better future-proofing. Unfortunately, these solutions lead to overly abstracted and unnecessarily complicated systems in the many cases. Examples of common pitfalls include: - **Cookie-cutter architectures:** Implementing additional architectural layers without clear, immediate benefits. - **Unnecessary abstractions:** Creating interfaces with only a single concrete implementation because "there might be another implementation someday." - **Dependency inversion overuse:** Abstracting everything behind interfaces, leading to confusing and indirect code paths that provide minimal value. These practices, although seemingly beneficial at first glance, but create complexity that becomes an obstacle rather than an asset. ### Clarifying Premature Optimization vs. Over-Engineering It's important to distinguish between these two often conflated issues: - **Premature optimization** involves optimizing performance prematurely, without clear evidence of bottlenecks. - **Over-engineering** refers broadly to adding complexity and abstractions prematurely, solving hypothetical future problems rather than immediate, real-world ones. Both are harmful in different ways but require different strategies to handle effectively. ### Embracing the SLC Approach The concept of building "Simple, Lovable, and Complete" products aligns closely with my critique of over-engineering. Here’s a quick refresher on the SLC philosophy: - **Simple:** Avoid unnecessary complexity. Build what solves today's problems clearly and effectively. - **Lovable:** Ensure your software genuinely delights users and meets their real needs right from the start. - **Complete:** Provide a comprehensive solution to the problem at hand without overextending or leaving critical gaps. This approach advocates incremental and pragmatic evolution rather than predictive complexity. ### Real-World Evolution, Not Speculative Design Every project teaches valuable lessons as it evolves. After three to six months in production, we understand far more about the domain and user needs than at the outset. Therefore, initial simplicity is not just beneficial. It's essential. Good architecture isn't about guessing every future scenario. Instead, it's about building software that can gracefully evolve as genuine requirements emerge. Pragmatism, rather than perfectionism, becomes key. ### Simplicity: Hard but Necessary As Edsger Dijkstra famously stated: > "Simplicity is a great virtue, but it requires hard work to achieve it and education to appreciate it. And to make matters worse: complexity sells better." Real simplicity isn't easy. It demands: - Careful consideration - Continuous feedback loops - Strong discipline to avoid introducing unnecessary complexity Yet despite these challenges, simplicity remains critical. It enables faster development, easier maintenance, and ultimately happier users. ### Practical Steps Toward SLC Here’s how you can practically apply the SLC philosophy and avoid unnecessary complexity: 1. **Solve Actual Problems First:** Address immediate user needs directly and effectively. 2. **Add Complexity Cautiously:** Introduce abstractions and optimizations only when clearly necessary. 3. **Validate Continuously:** Regularly gather user feedback and performance data to inform your next steps. 4. **Design for Incremental Growth:** Ensure your architecture supports gradual enhancements without major rewrites. 5. **Prioritize Simplicity:** Default to the simplest solution initially and evolve only as genuinely required. ### Conclusion: Pragmatism Drives Great Software The controversy around over-engineering reveals how deeply rooted this challenge is within our industry. Yet, clarity is possible. By embracing the "Simple, Lovable, and Complete" mindset, we can ensure our products meet today's needs while staying adaptable enough for tomorrow. Let's commit to pragmatic, thoughtful, and iterative software development. *Cheers*! ### Avoiding Over-Engineering: Focus on Real Problems in Software Development URL: https://ricofritzsche.me/avoiding-over-engineering-focus-on-real-problems-in-software-development/ Last updated: 2025-05-24T09:29:47.000Z One lesson has hit me repeatedly over the head: we waste a lot of effort trying to solve problems that we don't actually have yet. It’s an easy trap to fall into. We want our code to be lightning fast, perfectly designed and ready for millions of users. All before we've even released anything useful! The intentions are good, but the results can be disastrous. In this article, I’ll break down some of the most [common pitfalls of over-engineering ](https://ricofritzsche.me/why-vertical-slices-wont-evolve-from-clean-architecture/)that I’ve seen (and committed myself), and I’ll explore a more pragmatic approach that favors real-world feedback over theoretical perfection. ## The Cost of Premature Optimization Computer pioneer [Donald Knuth](https://wiki.c2.com/?PrematureOptimization&ref=ricofritzsche.me) warned that we should forget about small efficiencies 97% of the time, because obsessing over performance too early causes more problems than it solves. I couldn't agree more. Optimizing code before you know where the real bottlenecks are is a classic way to waste time for no good reason. What does premature optimization look like? It's when you spend days rewriting a function that works well and has already done its job. Believe me, I'm not wagging my finger at you. I've done it myself more than once. It’s pointless to worry about memory usage in a prototype that hasn’t been used by anyone yet. I’ve seen developers (including my younger self) meticulously micro-optimize pieces of code that never even appeared in a CPU profile. The result? Wasted time that could have been spent building features or fixing issues that users actually experience. ## Weekly Tips to Master Software Architecture Newsletter I provide lean, effective software-architecture strategies that cut through the noise. Subscribe Email sent! Check your inbox to complete your signup. Trusted by 2,500+ developers on Medium. The price includes not only time, but also complexity. Hand-optimized code is generally harder to read and maintain. It may rely on clever tricks or special-case logic that future team members (or even you, six months from now) will find hard to understand. And for what? In most cases, these 'optimizations' will have a minimal impact on overall performance. Meanwhile, the real performance hotspots (*the ones that really matter*!) remain undiscovered because you never tested the application under realistic conditions to identify them. To be clear, optimization itself isn’t bad. The important thing is to time it right. Use real usage data to profile your application and identify the 3% of code that truly needs to be speeded up. Focus your performance tuning on this code, and keep the other 97% simple. By resisting the urge to make everything 'fast' prematurely, you avoid creating overly complex code that you cannot easily change later. First make it work, then make it fast when necessary. ## Over-Abstraction and Premature Generalization Two related pitfalls in software design are over-abstraction and premature generalization. These often originate from a positive intention, as we aim to write elegant, DRY code and anticipate future requirements. However, if taken too far, they lead to architectures that are far more complicated than necessary. **Over-abstraction** is when you add extra layers, classes, or indirection that don't actually solve a problem you have right now. Maybe you create five layers of factories and interfaces to do something very simple, just in case you need to swap out implementations later. Or you build a generic framework for, say, “message processing” when your app only ever needed to handle one type of message. I’ve been there: I once designed an elaborate plugin architecture for a tool that, in the end, had exactly one plugin. All that abstraction was just dead weight. **Premature generalization** is similar: it involves writing your code to handle every possible future scenario instead of the one you are currently facing. It’s the “*What if we need to do X someday?*” mindset. For instance, you might generalize a function to handle an array of inputs when you currently only require one input, or create a polymorphic class hierarchy for three variations of behavior that could be covered by a simple if statement for now. **The result is complex solutions in search of a problem.** Ironically, these efforts to make the code 'future-proof' often make it more difficult to change in the future. In other words, applying an abstraction too soon tends to hinder rather than help maintainability. How can you avoid this? The key is to plan for the present, not the hypothetical future. Solve the concrete problem you have now in the simplest way possible. If you find yourself writing lots of code 'just in case' or creating layers of indirection without a clear, current need, take a step back. You probably don't need it. This doesn’t mean your code can never evolve; it means you’ll evolve it when you have more information. It's much easier to generalize a simple, working solution later (when the requirements are clearer) than to specialize a bloated, general solution built on guesswork. ## Real-World Feedback Trumps Theoretical Architecture Ultimately, it all comes down to a simple idea: you learn far more from real-world feedback than you do from perfecting an architecture in isolation. In theory, a design can appear perfect. It handles every case elegantly, can be scaled infinitely, and adheres to all best practices. On the whiteboard or in your head, it's perfect. However, as every experienced engineer knows, no application survives first contact with real users. Users do unexpected things. Requirements change once people start using the software. Bottlenecks appear in unexpected places. By releasing a working product or feature sooner, you can observe these issues and make adjustments. If you delay the release by months or even years while chasing the theoretically perfect design, you’re essentially flying blind the whole time. You’ll make investments that might turn out to be misguided. In my experience, projects that succeeded embraced an iterative, feedback-driven approach. We would build a simple version, present it to users or testers, and learn from its performance. This feedback would tell us where to strengthen the design or where our assumptions were incorrect. Conversely, projects in which we attempted to plan for every eventuality from the outset, involving extensive design phases and complex architectural diagrams for hypothetical edge cases, tended to fail. We either realized too late that the architecture did not meet the needs of the users, or we had to discard half of it because our assumptions were disproven by reality. Don't worship an architectural diagram just because it looks good. Favor an architecture that emerges from actual requirements and usage patterns. **Start with something simple that works.** Monitor it, profile it and gather user feedback. Then, where you see real pain points or growth needs, refactor and extend the architecture. This way, every bit of complexity you add is based on evidence that it is needed. ## Scaling Fantasies vs. Actual User Growth A particularly common imaginary problem is the '*we need to scale up to millions of users*' fantasy. Engineers love to daydream about systems on a cosmic scale, handling Netflix-level traffic or designing the backend for the next global social network is fun to think about. The result is that teams engineer their software preemptively to handle insane loads and complexity that they will most likely never see, or at least not for a long time. What dangers are there? Firstly, over-engineering for scale can be fatal before you even acquire a single user. If you’re a startup or developing a new product, complexity is your enemy. In the past, I’ve seen startups waste months building a [microservices architecture](https://medium.com/gitconnected/microservices-the-million-dollar-mistake-your-company-is-making-c50eb428f732?sk=b262a1e3c04f537cb6c3712b91965e01&ref=ricofritzsche.me) complete with multiple databases, queues and caches, just to handle traffic levels that only existed in their imagination. Meanwhile, they didn’t focus enough on creating a product that people actually wanted to use. This is a tragically common scenario: by the time they realize this, the money or motivation has run out and the fancy architecture has no real users to justify it. Even in established companies, I’ve seen teams introduce unnecessary complexity 'for future scalability' that never materialized. For example, we might split a service into dozens of tiny services or adopt an eventually consistent distributed datastore, all under the assumption that huge load is coming. In reality, the load might peak at just 5% of the predicted amount, and a simpler monolithic system would have handled that easily (with far fewer operational headaches). There is data to support this on the business side as well: studies have found that premature scaling is a leading cause of start-up failure. One [large-scale survey](https://startupgenome.com/articles/a-deep-dive-into-the-anatomy-of-premature-scaling-new-infographic?ref=ricofritzsche.me) concluded that 70% of failed startups had scaled up too early in terms of staffing, spending or technology before achieving product-market fit. Not a single startup that scaled prematurely in that study ever reached 100,000 users. The lesson is that scaling too soon doesn’t just waste effort. It can actively prevent you from ever needing to scale up at all. **So what should you do instead?** **Scale progressively, in step with real growth.** Start with a simple architecture that can handle your current needs plus a bit of headroom. Focus on acquiring users and delivering value. If you’re lucky enough to see rapid user growth, **that’s a *good* problem,** and you’ll have actual usage patterns and metrics to guide the scaling efforts. At that point, you can start identifying bottlenecks and refactoring the architecture to handle more load. By then, you’ll also hopefully have more engineers and resources to do it properly. Remember that many successful tech companies started out with very simple architectures. AirBnb, for example, started out as a monolithic Ruby on Rails application. Facebook began life as a PHP site backed by a single database. They certainly experienced growing pains, but they solved them as they arose, with a clear idea of what needed fixing. It's much better to handle scaling issues as they arise than to try to predict them all in advance. ## Evolving Architecture as You Go By now, it should be clear that I’m a big fan of [progressive, evolutionary architecture](https://ricofritzsche.me/applying-functional-core-and-imperative-shell-in-practice/), which involves designing your system step by step and being guided by real needs: **1) This doesn’t mean “no architecture” or simply piecing things together ad hoc.** **2) It means having a vision, but being flexible and willing to adapt as you learn more.** Think of your architecture as a living thing that grows alongside your application. Early on, you keep it lean and flexible. You avoid locking yourself into heavy patterns or one-size-fits-all edicts. As the system matures and you become more confident about certain requirements, e.g. '*we really do need multi-region redundancy*' or '*this module is clearly a performance hotspot*', you invest in those areas. One approach that embraces this idea is pragmatic architecture. The idea is to build the simplest thing that could possibly work, prove it out, and then enhance it. This is similar to the concept of evolutionary architecture, whereby the design supports incremental, guided change over time. You can start with a straightforward design and refine it through successive iterations. Each iteration is informed by real-world issues: perhaps you notice that database lock contention is increasing, so you introduce read replicas. Alternatively, users may request a new capability, prompting you to refactor part of the codebase to be more modular. The benefit is that you avoid taking big risks with unproven requirements. You always solve the most pressing problems, so your engineering effort directly creates user or business value. Meanwhile, you keep technical debt in check because you’re not developing unnecessary features. Yes, there’s a risk that if you hit it big and your simple design needs a major overhaul, you might have to do some heavy lifting further down the line. **But that would be a first-world problem!** It would mean that you had succeeded enough to warrant a re-architecture. By that time, you'll have a much clearer idea of what the new architecture needs to achieve, and you'll probably have the necessary funding and time to implement it properly. In contrast, if you do too much up front, you might never reach that level of success. *Cheers*! ### Rust's Explicit Error Handling: A Superior Alternative to Try/Catch URL: https://ricofritzsche.me/rusts-explicit-error-handling-a-superior-alternative-to-try-catch/ Last updated: 2025-05-22T07:51:08.000Z Can you imagine this situation? It’s late on a Friday, and your team is rushing to resolve a production issue. The root cause? A service crashed due to an unhandled exception that bubbled up. This is a scenario that is all too familiar in code bases that rely on implicit error handling. Having worked on large C#, Spring Boot and Node.js systems for years, I have seen many overnight outages and tricky bugs caused by exceptions that were caught too generically, or not at all. Traditional *try/catch* error handling, while convenient, can obscure the true control flow of a program. The code continues as if nothing went wrong until, suddenly, it doesn’t. ## The Hidden Pain of Implicit Error Handling Implicit error handling through exceptions has well-known pain points. ## Weekly Tips to Master Software Architecture Newsletter I provide lean, effective software-architecture strategies that cut through the noise. Join now! Subscribe Email sent! Check your inbox to complete your signup. Trusted by 2,500+ developers on Medium. Exceptions interrupt the normal execution path by jumping to the nearest catch block, which may be located far up the call stack. This non-linear flow makes it difficult to reason about the program's state. Reading a function does not reveal all its exit points if some are via thrown exceptions. It’s easy (and tempting) to catch a broad exception type without distinguishing error causes (e.g. catch (Exception e) in C# or a blanket catch in Java/TS). This results in minimal handling, often just logging, and masks different failure modes under one generic branch. Even worse, sometimes a catch block simply swallows the error, doing nothing or returning a default value. This causes the program to continue in an undefined state, which can lead to deeper problems. In one Spring Boot postmortem, for example, swallowing an exception meant that the app continued to run in an unexpected state, which could result in further errors and corrupted data. Since exceptions are not shown in function signatures, they can catch you off guard at runtime. For example, an innocuous method call might throw a *NullReferenceException* or an I/O error that wasn’t obvious to the caller. Teams often only discover these when an unhandled exception causes something to crash in production. Using exceptions for expected conditions can have a detrimental effect on performance and code. **Exceptions are designed for exceptional cases**, so using them for regular control flow is inefficient. Unwinding the stack incurs a cost. In .NET, for instance, excessive exception throwing can reduce throughput. Using exceptions for everyday logic in C# are messy and make your code harder to read. When even Microsoft's own guidelines caution against using exceptions for routine logic, it suggests there is a problem. These issues have prompted many developers to seek alternatives. From C# to JavaScript, there is a growing realization that implicit error handling (simply throwing and catching errors) is not the most effective approach. In the *TypeScript* community, for example, there is a great deal of frustration because the try/catch method provides no type safety if any value can be thrown, and the function signature is unaware of what could be thrown. In other words, neither humans nor tools can easily anticipate the errors that a given function might produce. It's a recipe for surprises. ## Rust's Approach: Errors as Part of the Control Flow Rust takes a very different approach to error handling. Rather than using exceptions to handle recoverable errors, Rust encourages us to make error conditions explicit in the type system. A function that might fail will return a *Result* (or an *Option* if the only 'error' is the absence of a value) instead of throwing an exception. This simple concept is underpinned by the idea that a function’s potential to fail is part of its output and contract. In languages such as C# and JavaScript, it's easy to overlook potential errors. You call a function and assume it will succeed unless an exception occurs. Rust, by contrast, makes the possibility of failure impossible to ignore. Some languages allow you to ignore potential errors, automatically propagating them as exceptions and causing the program to crash if they are not handled. However, Rust's design is more intentional. A Rust function returns an explicit ***Result*** or **Option**, and this *"optionality"* or *“result-ness”* travels with the value. You must either handle the error immediately or propagate it upwards; you cannot simply ignore it. At some point, you must check whether you got the value or an error. This leads to a kind of enforced discipline. Rust will literally not let you forget to deal with the error case. If you try to use a result without handling the *Err*, it won’t compile. If you treat an *Option* as if it were a guaranteed *T*, Rust will swiftly puncture your unwarranted optimism with a type error. The compiler is your guide, constantly asking you, *what about the failure case.* In practice, this means fewer unchecked edge cases sneaking through. It is better to have compile-time checks than runtime surprises. Yes, explicit errors require a little more typing, but that's the trade-off between clarity and convenience. Rust eases that burden with the '?' operator. It is essentially a shorthand for '*if this returns Err, return it now; otherwise unwrap the Ok*'. But this convenience can sometimes work against you. Consider this: ```Rust // Imagine fetching an asset might simply not find one. fn get_asset() -> Option { None } fn get_asset_name() -> Result> { let asset = get_asset()?; // ❌ this won’t compile Ok(asset) } ``` You’ll see: ``` error[E0277]: the `?` operator can only be used on `Result`s, not `Option`s, in a function that returns `Result` --> src/lib.rs:10:28 | 9 | fn get_asset_name() -> Result> { | ------------------------------------- this function returns a `Result` 10 | let asset = get_asset()?; | ^ use `.ok_or(...)?` to convert the `Option` into a `Result` ``` Why? Because *'?'* works on whatever your function returns. In this case, your function returns a *Result*, so *'?'* expects a *Result*. An *Option* is a different type with no built-in way to become a *Result*, unless you specify how to map the *None* case to an error. The solution is straightforward: if the absence of an asset truly constitutes an error, express it using a *Result* and explicitly convert the *Option*. ```Rust fn get_asset() -> Result { // ... Ok("Delivery Van".to_string()) } fn get_asset_name() -> Result { let asset = get_asset()?; // ✅ compiles and propagates any error // Do something... Ok(asset) } ``` Both functions now speak the same language: they both return a *Result*. If an asset cannot be found, *get\_asset()* returns an *Err("...")*, which is automatically bubbled up by *get\_asset\_name()*. Another straightforward option is to use a *match* statement. Since *Option* is just an *enum*, you can pattern-match on its variants and handle each case explicitly. In the *None* arm you return early with an error message; in the *Some* arm you continue with the value. That makes control flow crystal-clear: ```Rust // Simulate fetching an asset; returns None if not found fn get_asset() -> Option { None } fn get_asset_name() -> Result { // Be explicit: match on the Option, covering both cases let asset = match get_asset() { Some(name) => name, None => return Err("No asset found".into()), }; // Do something with `asset`… Ok(asset) } ``` Here, the compiler checks that you have handled both '*Some*' and '*None*'. In the '*None*' branch, you immediately return an '*Err*', whereas in the '*Some*' branch, you bind the value to a name. This method is slightly more verbose than the '*?'* method, but it clearly explains what is happening at each step, which is useful for learners. Because match is an expression, its final value is assigned to asset. Use '*match*' when you want maximum clarity, especially if you need to perform different actions in each branch. For quick conversions, you can use *?* or *ok\_or*, but match is the most explicit tool at your disposal. Use *Option* when the absence of an outcome is normal and expected; use *Result* when the absence of an outcome (or any other failure) is a genuine error. Rust’s compiler won’t silently allow you to mix them up. **It forces you to make your intent clear in the types, rather than hiding it behind a catch.** Fundamentally, Rust’s approach promotes a philosophy of **correctness through explicitness**. Error handling is not an afterthought or a separate control flow hidden from the type system; it’s woven into the logic of the program. This leads to more **robust code**. When you see a function signature, you immediately know if it can fail and you need to consider error cases. There’s no need to comb through documentation or implementation to discover “*oh, this might throw.*” The result is less ambiguity. As an example, consider how obvious it is when a Rust function returns an *Option. Y*ou *must* check for *None* or use a helper, otherwise the compiler will stop you. In contrast, in languages with implicit exceptions, you might call a method without realizing it might throw, or you might forget to surround it with the right *try/catch*. Rust shifts that knowledge left to compile time. ## Conclusion The transition from implicit error handling languages such as C#, Java and JavaScript to Rust's explicit *Result* and *Option* model can initially feel disorientating. It can seem as though you have to type more and think more about errors than you’re used to. However, this initial extra effort pays off in the form of cleaner, more reliable code. When every possible error is accounted for in the type system, your code paths achieve a certain closure. You either handle an error or pass it on, but you never ignore it. Developers who come from languages with *try/catch* often have stories about 'exception hell', where debugging is like detective work to find where an error occurred. In Rust, those scenarios are far less common: the compiler supports you from the outset, ensuring you never miss the error handling branch. And yes I know, I know... .NET 9 introduced a *Result* type, inspired by Rust, to handle errors without relying heavily on exceptions. Unlike Rust’s compile-time enforcement, the *.*NET *Result* uses runtime checks, requiring developers to manually verify the result’s state to avoid errors. While it reduces exception overhead, it lacks Rust’s guarantee of handling errors before the program runs. *Cheers*! ### Why Vertical Slices Won't Evolve from Clean Architecture URL: https://ricofritzsche.me/why-vertical-slices-wont-evolve-from-clean-architecture/ Last updated: 2025-05-15T15:13:56.000Z Yesterday, a comment on LinkedIn drove me to despair again. > "Side note: The natural evolution of Clean Architecture leads to Vertical Slices when you do fewer projects and more folders. Which speeds up development greatly" - **Anton Martyniuk on LinkedIn** If you’re a .NET developer or architect feeling the LinkedIn-fueled confusion, read on, and let me explain why Vertical Slice Architecture don’t just spring from Clean/Onion/Hexagonal Architecture. They challenge its core assumptions. First, architecture is not a religion. It’s a choice. As professionals, we should constantly question why we structure our projects a certain way. The myth that Clean Architecture somehow becomes Vertical Slice Architecture by merging projects reflects blind copying rather than genuine understanding. In my opinion, many people promoting 'vertical slice architecture' have simply spotted a new angle for videos and articles. That’s the first problem. Just because someone reads that vertical features might be the next big thing, they rush to pump out tutorials on how to build a vertical slice folder structure from the previously advertised clean/onion/hexagonal approach, complete with all the useless layers. The brutal truth is that it’s still over-engineered junk, just rearranged. This is obviously a ploy to generate clicks with fresh content. Anyone who has worked on a real project for a real client, not just tinkered with theory on a home PC, knows you can’t just reshuffle your entire structure every time a new buzzword appears. ## Understanding the Idea of Vertical Slice Architecture Second, I highly recommend studying Jimmy Bogard’s 2018 article on Vertical Slice Architecture. He argues that traditional layered approaches are monolithic at their core. As he puts it: > "The problem is this approach/architecture is really only appropriate in a minority of the typical requests in a system. Additionally, I tend to see these architectures mock-heavy, with rigid rules around dependency management. In practice, I've found these rules rarely useful, and you start to get many abstractions around concepts that really shouldn't be abstracted (Controller MUST talk to a Service that MUST use a Repository)." - **Jimmy Bogard** As I've said countless times, Clean/Onion/Hexagonal architectures have two major problems: 1. Code is scattered across technical layers. In real projects, I've seen it a thousand times: layers stuffed with a single line of code or loads of boilerplate. You end up jumping between folders to understand what a feature actually does. And for a simple API call that queries a database, all those extra layers are just over-engineered nonsense. 2. Dependency Inversion (DIP) creates functional dependencies. People claim it helps testing, but in reality you’re forced to mock everything and introduce pointless complexity. If you really want to test your logic, just keep it pure: data in, compute, data out. No side effects. No messing around. I go into more detail here: > "Instead, I want to take a tailored approach to my system, where I treat each request as a distinct use case in how to approach its code." - **Jimmy Bogard** In other words, if you want a more flexible approach that only does what you actually need for each feature, Vertical Slicing is the right choice. This flies in the face of all the book-selling layered architectures that want one-size-fits-all conformity. The core of Vertical Slicing is to break out of that rigid, encrusted mindset, as Jimmy shows in his example: ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/05/Picture0031.png) Source: [https://www.jimmybogard.com/vertical-slice-architecture/](https://www.jimmybogard.com/vertical-slice-architecture/?ref=ricofritzsche.me) With Vertical Slicing, each request can decide how it wants to handle its logic, which means you no longer need these shared layers like repositories, services, and controllers everywhere. You also don’t have to litter slices with MediatR. Its purpose is to implement the mediator pattern, routing all requests through a central handler. That can be useful if you genuinely need to enforce strict separation between inputs and processing, maybe because you’ve got logging, retries, or transactional behavior that must wrap around every request. But let’s be real: most use cases don’t need this ceremony. If you're just handling a basic read or write operation, use a transaction script, or a [plain SQL query](https://ricofritzsche.me/zero-abstraction-sql-why-raw-queries-beat-orms-for-clarity-and-control/). It’s faster, easier to debug, and doesn’t hide logic behind layers of abstraction. When MediatR recently switched to a paid license, the community went into full panic mode running around like headless chickens. That alone shows how blindly it was adopted. Tools like this should only be used if they solve a real problem. They should not be used just because they’re trendy, appear in a YouTube tutorial or are recommended in a daily LinkedIn tip. **If the added complexity brings no clear benefit, don’t use it.** ## The Allure of “Evolving” Clean Architecture into Vertical Slices Why do so many folks think that trimming the number of projects in a Clean Architecture solution and shuffling folders around suddenly means they’ve adopted Vertical Slices? So-called gurus on LinkedIn or in community chats love to post something like this: > “We refactored our app from the classic Clean Architecture (with separate Domain, Application, and Infrastructure projects) into a single-project structure. Now we group code by feature. Vertical Slices for the win!” It’s easy to see how this idea took hold. Clean Architecture, popularized by Uncle Bob and passionately embraced in the .NET world, usually involves multiple projects or layers (Web, Application, Domain, Infrastructure). That can feel slow and cumbersome. So developers cut the clutter by removing extra projects and indirections. On the surface, it looks simpler. They might even label folders by feature. Suddenly, people start calling it “Vertical Slice Architecture,” convinced they’ve just evolved Clean Architecture by stripping it down. Yet these annoying Clean/Onion Architecture templates reappear like cancerous tumors, popping up like metastases. It highlights the helplessness of a generation stuck in pattern propaganda, swamped by tutorials, libraries, and frameworks. Very few want to dig deeper and truly understand what they’re doing, let alone grapple with the domain itself to build real business value. Instead, they hide behind layers of complexity, abstractions and indirections nobody actually needs. For me, code organized purely around technical layers always signals a domain that isn’t understood. **And Clean Architecture can never be domain-driven anyway.** ## Where Did the Confusion Come From? Why does so much of the .NET community mix up these two approaches? .NET developers have leaned heavily on Clean/Onion Architecture. It was drilled into our heads that a “proper” enterprise app has strict layers, clear separations and dependency inversion. Over time, as projects ballooned, this layered approach started feeling painful with tons of indirection, endless tiny projects or folders. Then came Vertical Slice Architecture, claiming to soothe these pains by focusing on features. Many devs only knew the layered mindset, so they took Vertical Slices and treated them like a new structure only for Clean Architecture. They flattened the structure but never fully embraced a feature-first mentality. The problem isn’t helped by some high-profile voices mixing terms. LinkedIn and blogs are full of posts on “Combining Clean Architecture with Vertical Slices” or “domain- centric design and separation of core business logic within each vertical slice.” People read that and assume “Oh, Vertical Slices is just a simpler Clean Architecture.” They miss the nuance that a truly vertical-sliced app packages code differently at its core. Then there’s the jargon soup. “Feature Folders,” “Vertical Slices,” “Modular Monoliths,” and so on exploded around the same time in the .NET scene. Some folks lumped them all together. ASP.NET Core’s feature folders might group controllers and views by feature, but that doesn’t mean your backend is sliced. And using MediatR for requests doesn’t magically make your code feature-oriented either if everything else is layered. Tools and templates all came out around the same time, leaving a blur of “best practices” with no clear boundaries. It also doesn’t help that Clean Architecture had loads of examples, like the Jason Taylor template. Into that gap poured a bunch of half-baked experiments. People refactored their apps and blogged “We switched to vertical slices,” but often kept some Clean Architecture remnants. Readers saw a hybrid and assumed it’s a natural evolution path. Finally, if you have a massive Clean Architecture project and you’re sick of wading through layers to add features, grouping code by feature is an intuitive next step. Teams do this gradually: they start grouping commands and queries for Orders or Customers, and maybe put validators and mappers there too. Eventually they merge projects. At some point, the structure looks pretty feature-centric, so it’s tempting to say “We evolved into vertical slices.” But usually, some leftover shared repository or service layer remains. People see this half-and-half and think Vertical Slices Architecture is just Clean Architecture 2.0, rather than an entirely different way of structuring and thinking about the code. In the end, it’s no wonder the .NET LinkedIn crowd lumps them together. But remember: vertical slicing isn’t simply Clean Architecture upgraded. It’s a different mindset, emphasizing feature independence over strict layer decoupling. Both aim to manage complexity, but they tackle it in nearly opposite ways: one organizes by layer, the other by feature. ## Productivity and Clarity: The Real Benefits of Vertical Slices So, why do so many developers find the Vertical Slice Architecture appealing in the first place? If applied correctly, there are clear advantages to this approach over simply flattening your project structure. Firstly, you benefit from laser-focused development. When you implement a feature as a vertical slice, all related elements are kept together in one place. There's no need to click around between multiple projects or folders to follow the logic. Developers who have tried this often say that it makes their solutions far easier to navigate, debug and maintain because the entire feature is neatly packaged. This means more time is spent on real, meaningful coding. Secondly, real vertical slices mean fewer merge conflicts and safer changes. Since each feature is isolated, developers working on different slices rarely edit the same files. Adding new functionality typically involves creating new files rather than amending existing ones. This reduces the risk of accidental breakages and promotes stability by adopting an append-only coding style that leaves existing, tested logic untouched. Another clear advantage is high cohesion. Each vertical slice forms a logical, independent unit that is straightforward to understand. Rather than trying to understand the entire application's layering scheme, you focus on one straightforward section at a time. This dramatically reduces cognitive load, particularly for newcomers, who can focus on learning one feature area rather than grappling with an entire intricate architecture. Vertical slices also make the codebase and the team more scalable. Adding new features is straightforward: just add another slice. Even if your application as a whole grows significantly, each slice will remain manageable. Teams naturally divide into feature-based areas, which aligns perfectly with agile delivery methods. Each slice can evolve independently, adopting different technologies or storage solutions where appropriate. Such freedom is almost unheard of in traditional layered architectures. Lastly, there is strong alignment with agile practices and the domain itself. Vertical slices naturally correspond to user stories or specific use cases, ensuring that your architecture directly reflects the delivery of business value. Seeing a complete feature come to life in one place can be highly motivating for teams. When scaled up, this approach blends well with the concept of[ Bounded Contexts](https://ricofritzsche.me/what-are-problem-space-and-solution-space-in-domain-driven-design/) in Domain-Driven Design (DDD) each slice or group of slices represents a clearly defined domain capability rather than abstract technical layers. You may find that the same logic is used in different places. For example, two features might perform similar validation separately. Clean Architecture proponents prefer centralized logic and adhere strictly to the DRY principle, whereas vertical slices sometimes intentionally tolerate duplication in order to maintain independence. However, after spending over 30 years coding, modelling and designing real-world systems, I have become deeply skeptical of DRY. It is often the root of all evil. The real key is pragmatism: extract genuinely shared logic, but never force abstractions prematurely. Protecting slice autonomy is far more important than blindly chasing DRY. Despite of a few potential pitfalls, vertical slicing brings tremendous clarity and maintainability, especially in large, evolving code bases. Teams that fully embrace this approach consistently report faster development cycles and fewer pointless debates over 'Where should this code go?' Vertical slicing reduces cognitive load, streamlines feature development and finally gives every piece of code a logical and sensible home. *Cheers*! ## ### Mastering PostgreSQL Row-Level Security (RLS) for Rock-Solid Multi-Tenancy URL: https://ricofritzsche.me/mastering-postgresql-row-level-security-rls-for-rock-solid-multi-tenancy/ Last updated: 2025-05-05T20:41:58.000Z Multi-tenancy can be a minefield for platforms that serve multiple customers from a shared code base and database. One small mistake, such as forgetting a tenant filter in an SQL query, can expose customer A’s data to customer B, which is every SaaS founder’s worst nightmare. In the early days of SaaS in 2009, while working with a large telecommunications company in Germany, I learned that strong tenant isolation is best enforced close to the data itself. This is where PostgreSQL Row-Level Security (RLS) comes in. In this article, I will discuss how PostgreSQL RLS works in detail, demonstrate its setup with a simple example, and examine its application to views and stored procedures. You’ll see how RLS provides a secure-by-default data layer that significantly reduces the risk of cross-tenant data leakage. ## Why Row-Level Security for Multi-Tenancy? Multi-tenancy means a single application (and database) serves multiple customers (tenants), each expecting their data to remain invisible to others. The challenge in a shared database model is ensuring every query and command is scoped to the right tenant. Relying on application code to add a where-clause like ***WHERE tenant\_id = …*** everywhere is very fragile. One mistake in a query and sensitive data could be leaked. As one AWS engineer neatly put it in a blog post, you want to enforce tenant isolation centrally instead of leaving it to “the everyday variability of your source code.” This is where Row-Level Security comes in: by enforcing tenant-specific policies at the database level, RLS ensures isolation even if application code misses a filter. PostgreSQL Row-Level Security, introduced in PostgreSQL 9.5, directly addresses this problem. RLS allows you to define security policies on a table such that every SQL operation (SELECT, INSERT, UPDATE, DELETE) is automatically filtered according to those policies. In a multi-tenant application, we can create a policy that says “a row in this table is only visible if *tenant\_id* matches the current tenant’s ID.” The database then enforces that rule for every query, no matter if a developer forgot to add a filter in code. If a query doesn’t satisfy the policy, the database behaves as if those rows don’t even exist. In practical terms, RLS gives us defense in depth: even if our code has a bug, the database won’t return or modify data outside the tenant’s scope. This flips the multi-tenancy model to a safer default. Instead of hoping every developer remembers to include tenant conditions, we guarantee at the database level that cross-tenant data access cannot happen. Every SQL statement is automatically tenant-aware, so developers can write queries focusing on business logic, while Postgres handles the tenant isolation details. Let’s contrast RLS with two other approaches I’ve seen (and used) for multi-tenancy: ### Separate Schema or Database per Tenant In this silo model, each tenant’s data lives in its own schema or database. This provides strong isolation (tenants never share tables), but at the cost of huge operational complexity in managing potentially hundreds of schemas or databases, performing migrations across them all, and increased infrastructure overhead. It can be a good approach for a handful of large tenants, but it doesn’t scale well to many small tenants or speedy deployments. ### Discriminator Column (Tenant ID in Tables) This is the classic shared-database approach: add a *tenant\_id* column to every table and ensure every query filters by it. Simpler to set up and cost-efficient, but puts a lot of responsibility on developers and ORM mappings. Missing a filter in one place can be disastrous. You can mitigate this with patterns like global query filters or interceptors (I’ve written about using EF Core’s shadow properties to stamp *tenant\_id* automatically), but it still feels like placing a critical security control in application logic. > RLS combines the best of both worlds. It lets you keep a single, shared schema for all tenants (easy to deploy and scale) while centralizing the tenant filtering in the database engine. In other words, you get the operational simplicity of the shared model with a security guarantee approaching the silo model. The application can use one Postgres role for all tenants (so connection pooling is straightforward), and we tag each session or transaction with the tenant’s ID. The RLS policy then ensures each session sees only its tenant’s slice of the data. There’s “zero chance of forgetting a tenant filter” in code because Postgres itself won’t allow a cross-tenant query. ## Under the Hood: How Does RLS Enforce Tenant Isolation? In PostgreSQL, RLS policies revolve around **roles** and **conditions**. The general process looks like this: 1. You create one or more database roles (e.g. an ‘app’ role for your application) that do not have the BYPASSRLS privilege. This ensures that the role is subject to RLS policies, rather than ignoring them. 2. You enable RLS on a table by running ALTER TABLE ENABLE ROW LEVEL SECURITY;. 3. You then create one or more policies that define the allowed rows for each operation (SELECT, INSERT, UPDATE, DELETE). These policies check something like a *tenant\_id* column against a session-specific context to ensure that only rows belonging to the current tenant are visible. RLS effectively adds an automatic WHERE clause to every query the role executes. If a policy says ***tenant\_id = current\_setting(‘app.current\_tenant’)***, Postgres will internally translate any statement (e.g. SELECT \* FROM assets) into: ```SQL SELECT * FROM assets WHERE tenant_id = current_setting('app.current_tenant')::uuid; ``` … without requiring the developer to write that condition in the application code. > But how do we tell the database which tenant is “current” for the session or transaction? ### Using Custom GUC Variables for Tenant Context While you can assign different database roles to each tenant and use something like SESSION\_USER in your policy, this approach can become unwieldy if you have many tenants. Instead, a more flexible method is to rely on PostgreSQL’s own runtime parameters, also known as GUC (Grand Unified Configuration) variables. Define a custom variable namespace, such as *app.current\_tenant* At the start of each request (or transaction) in your application, set *app.current\_tenant* to the ID of the tenant making that request. The RLS policy references current\_setting(‘app.current\_tenant’) to enforce row-level constraints. ## RLS Setup Example Let’s say we have a table called **Assets**, which stores equipment or resources that belong to different tenants. This example is taken from my [asset tracking/geofencing venture](https://bluvolve.com/?ref=ricofritzsche.me) that I am currently working on. Each row in this table has a *tenant\_id* that identifies the tenant that owns that asset. ```SQL -- 1. Table and Policy Setup CREATE TABLE assets ( id UUID PRIMARY KEY, tenant_id UUID NOT NULL, name TEXT NOT NULL, description TEXT, status TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), retired_at TIMESTAMPTZ ); -- 2. Enable RLS ALTER TABLE assets ENABLE ROW LEVEL SECURITY; -- 3. RLS policy: only allow access to rows -- where tenant_id matches the current_tenant setting CREATE POLICY assets_tenant_isolation ON assets USING (tenant_id = current_setting('app.current_tenant')::UUID); -- 4. Ensure new inserts have the correct tenant_id CREATE POLICY assets_tenant_insert ON assets FOR INSERT WITH CHECK (tenant_id = current_setting('app.current_tenant')::UUID); ``` With these policies in place, the assets\_tenant\_isolation policy applies by default to all SELECT, UPDATE and DELETE statements, ensuring that any row retrieved or modified must satisfy *tenant\_id = current\_setting(‘app.current\_tenant’)*. The assets\_tenant\_insert policy applies to INSERT statements. It ensures that newly inserted rows have a tenant\_id that matches the current tenant context. If the INSERT attempts to use a different tenant\_id, the database will reject the operation. ### Important Notes for Production Environments You need to ensure that the *app.current\_tenant* variable is always set and reset appropriately in a connection pool environment. When a connection is reused for multiple tenants across different requests, you want to avoid a scenario where the previous tenant’s ID is still set. A common approach is to call **SET LOCAL app.current\_tenant = ‘…’** at the start of a transaction, which will automatically clear the variable at the end of the transaction. Alternatively, you can manually **RESET app.current\_tenant** after processing a request. ### Populating Test Data For demonstration, let’s insert sample assets for two different tenants, identified by their UUIDs: ```SQL INSERT INTO assets (id, tenant_id, name, description, status) VALUES ('f47ac10b-58cc-4372-a567-000000000001','11111111-1111-1111-1111-111111111111','Forklift FL-100','15-ton capacity','active'), ('f47ac10b-58cc-4372-a567-000000000002','11111111-1111-1111-1111-111111111111','Truck TR-200','GPS-enabled heavy-duty truck','active'), ('f47ac10b-58cc-4372-a567-000000000003','11111111-1111-1111-1111-111111111111','Container CT-300','Refrigerated shipping container','active'), ('f47ac10b-58cc-4372-a567-000000000004','11111111-1111-1111-1111-111111111111','Pallet Jack PJ-400','Manual pallet jack','retired','2025-03-15T10:00:00Z'), ('f47ac10b-58cc-4372-a567-000000000005','11111111-1111-1111-1111-111111111111','Drone DR-500','Aerial inventory drone','active'), ('f47ac10b-58cc-4372-a567-000000000006','11111111-1111-1111-1111-111111111111','AGV AG-600','Automated guided vehicle','retired','2025-04-01T12:00:00Z'), ('f47ac10b-58cc-4372-a567-000000000007','22222222-2222-2222-2222-222222222222','Delivery Van DV-110','Electric delivery van','active'), ('f47ac10b-58cc-4372-a567-000000000008','22222222-2222-2222-2222-222222222222','Pallet Jack PJ-210','Electric pallet jack','active'); ``` Now we have two sets of assets for two different tenants. If RLS is configured correctly, each tenant should only see their own rows if we set the correct *app.current\_tenant* value. ### Creating and Using the “app” Role To demonstrate how RLS is enforced at the role level, let’s create a dedicated Postgres role that our application will use. In many production setups, your application will connect to a single database user or role that’s what I want to illustrate here. ```SQL CREATE ROLE app LOGIN PASSWORD 'p@ssw0rd' NOINHERIT; ``` A few things to note here. **LOGIN** makes the role usable for client connections. It is critical not to grant **BYPASSRLS**. Any role with **BYPASSRLS** set to *true* would ignore the RLS policy, defeating its purpose. **NOINHERIT** means that if this role is granted membership in other roles, it won’t automatically inherit their permissions. This helps to keep our permission scheme tight. Next, we can set a default for *app.current\_tenan*t to an empty string or any other default: ```SQL ALTER ROLE app SET app.current_tenant TO ''; ``` Then we adjust privileges on the schema and table so that this new role can interact with the data: ```SQL REVOKE ALL ON SCHEMA public FROM PUBLIC; GRANT USAGE ON SCHEMA public TO app; GRANT SELECT, INSERT, UPDATE, DELETE ON assets TO app; ``` ### Using the “app” Role in SQL Sessions Now let’s simulate a session where we act as the *app* role, set a tenant context, and run queries: ```SQL SET ROLE app; SET app.current_tenant TO '11111111-1111-1111-1111-111111111111'; SELECT * FROM assets; ``` With this setup, the query should **only** return rows matching tenant as we can see here in the result output. ![](https://cdn-images-1.medium.com/max/1600/1*LD7Sr12C0HmE88WiEMbr1Q.png) If we switch to the second tenant: ```SQL SET app.current_tenant TO '22222222-2222-2222-2222-222222222222'; SELECT * FROM assets;s ``` We’ll only see that tenant’s records. And if we specify a non-existent tenant ID or forget to set *app.current\_tenant*, the result will be empty. It’s exactly what we’d expect from a robust isolation policy. ### Working with Views and RLS As discussed, RLS significantly simplifies multi-tenancy by centralizing tenant isolation. However, special considerations apply when you use database views or stored procedures. By default, a PostgreSQL view runs with the privileges of its creator (usually a superuser or table owner). This means views owned by roles with the BYPASSRLS privilege, such as a superuser like *postgres* in the example, will completely bypass Row-Level Security policies. Let us create a simple *active\_assets* view, as shown below: ``` SET ROLE postgres; CREATE VIEW active_assets AS SELECT id, tenant_id, name, status FROM assets WHERE status = 'active'; ``` With this setup, querying the view will unexpectedly return all tenants' rows, ignoring RLS policies. ```SQL SET ROLE app; SET app.current_tenant TO '22222222-2222-2222-2222-222222222222'; SELECT * FROM active_assets; ``` This happens because the view executes with the privileges of its owner (the superuser), effectively bypassing RLS. Let's fix the view to enforce RLS and ensure that it only returns the expected rows of the current tenant. Make the view execute with the privileges of the the invoker. ```SQL SET ROLE postgres; ALTER VIEW active_assets SET (security_invoker = true); ``` With this, when querying as the *app* role, RLS policies are correctly enforced. It's essential to double-check the ownership of your views. Unintentionally having views owned by superuser roles can lead to bypassing RLS, potentially causing serious security issues. ### RLS Enforcement per Request Cycle In a typical SaaS architecture, each incoming request arrives with a known tenant identifier, for instance from a JWT token. The application can then run something like: ```SQL BEGIN; SET LOCAL app.current_tenant = 'some-tenant-uuid'; -- Execute the needed SELECT, INSERT, UPDATE, or stored procedures -- RLS automatically applies the correct tenant filter. COMMIT; ``` Using **SET LOCAL** inside a transaction ensures that once the transaction ends, the tenant context resets automatically. This is crucial for safe multi-tenant environments where connections might be pooled and reused. You don’t want to accidentally “leak” the previous tenant’s context into the next request. ## RLS Checklist To make the most of PostgreSQL RLS for multi-tenancy, keep these points in mind. **Check for Superuser or BYPASSRLS Roles** RLS doesn't apply to superusers or roles with BYPASSRLS = TRUE. Make sure your application role doesn't have these privileges, otherwise your policies won't matter. **Beware of SECURITY DEFINER Functions**: A function marked as SECURITY DEFINER and owned by a superuser can bypass RLS if not managed carefully. Use SECURITY INVOKER for typical multi-tenant logic so that the caller's RLS context applies. **Performance**: RLS effectively adds an extra filtering step to your queries. Indexing your *tenant\_id* column is crucial. Fortunately, in many multi-tenant systems, queries typically revolve around the tenant’s data anyway, so the performance overhead is often negligible. **Keep the Tenant Context Clean in Connection Pools** Explicitly set and reset *app.current\_tenant* on each request. If you're using something like PgBouncer or a built-in connection pool (e.g. in an application server), make sure that each new request triggers a new context. Some frameworks provide hooks or middleware that you can use to do this automatically. **Auditing, Monitoring and Testing** As RLS is a critical security feature, you should consider capturing logs (e.g. via Postgres native logging or an external system). This can help you investigate suspicious queries or confirm that queries are indeed restricted to the expected tenant. Write integration tests to confirm that cross-tenant data remains inaccessible. For example, spin up the application, authenticate as Tenant A, and ensure you cannot query Tenant B’s records. Then do the reverse. Automated tests catching a misconfiguration early can save you from a data breach. ## Wrapping Up Row-Level Security in PostgreSQL provides a secure-by-default data layer that drastically reduces the risk of cross-tenant data leaks through global database-level tenant filters. This approach centralizes data isolation, the most critical security concern, and eliminates the risk of developer oversights in queries. Rather than depending on application code to maintain *tenant\_id* filters throughout, the filtering happens directly in the database. Using a custom GUC variable like *app.current\_tenant*, your application can set the tenant context for each session or transaction. The database then automatically enforces filtering for all operations (SELECT, INSERT, UPDATE, DELETE) on RLS-enabled tables. Views, stored procedures, and functions inherently respect these security rules, maintaining consistent protection even in complex reporting scenarios. For SaaS platforms, RLS is absolutely worth considering as your multi-tenancy foundation. Its power and flexibility can protect you from critical mistakes that could otherwise result in data breaches and lost customer trust. As with any security implementation, test thoroughly in your environment. Monitor connection pooling carefully and restrict tenant data access to non-superuser roles only. When combined with proper indexing, auditing, and logging practices, RLS becomes a valuable asset in delivering secure, scalable multi-tenancy. **Check out the** [**github repository**](https://github.com/ricofritzsche/multi-tenant-rls-demo?ref=ricofritzsche.me) **to explore the setup.** Have questions about this setup? Feel free to reach out or comment, and please share your own multi-tenancy experiences. *Cheers*! ### Why Building Software Feels Broken URL: https://ricofritzsche.me/why-building-software-feels-broken/ Last updated: 2025-05-22T08:06:21.000Z I've been in the software business for about three decades now. That's long enough to see all sorts of trends come and go. Lately, though, I feel that the industry has lost sight of what is truly important. Instead of solving real problems in a simple way, we keep piling on new frameworks and buzzwords. We seem more interested in following the latest fad than in producing software that helps people and businesses. ## The Rise of the “Problem Machine” Everywhere I look, someone’s inventing a new problem to solve. And the more trivial the problem, the more over-engineered the solution. It’s like we’ve built an entire ecosystem around fixing things that didn’t need fixing. Just so we can use the latest framework, architecture, or buzzword. Somewhere along the way, solving real problems got replaced by proving how modern our stack is. ## Weekly Tips to Master Software Architecture Newsletter I provide lean, effective software-architecture strategies that cut through the noise. Subscribe Email sent! Check your inbox to complete your signup. Trusted by 2,500+ developers on Medium. And it’s costing us. Recent industry reports show that nearly 70% of software projects go over budget. Almost a third miss their deadlines. In enterprise environments, that translates into millions lost on delays, rewrites, and features nobody asked for. We’re not short on tools. On the contrary, we have more than we know what to do with. What we are short on is clarity. It feels like we’re tangled up in our own complexity. The irony? All these tools, all this ceremony, were meant to make things faster. Instead, they’ve added layers of abstraction that slow us down. Sprints filled with backlog grooming for features that get changed mid-cycle. Planning sessions to plan the next planning session. Every new tech decision spawns more meetings, more training, and more dependencies. And with each new dependency, the system becomes harder to understand, let alone maintain. It’s no wonder projects run late and over budget. We've turned the act of building software into managing the chaos we created ourselves. Somehow, we’ve created a process that feeds on itself. Here some data: - 70% of projects go over budget - 29% are delivered late - The average cost overrun is 27% - For large-scale IT projects, the overrun jumps to 45% ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/05/Key-Statistics-on-Software-Project-Overruns--2024-.png) Software project performance in 2024 (sources: BCG, MuleSoft, Standish Group) This data reflects exactly what I have been observing for years in projects at large companies. It feels like implementation has never been so slow and expensive. ### The Age of Cargo Cults Cargo cult development means copying techniques or tools from successful teams without understanding their context, hoping success will magically follow. Teams choose a popular framework because it worked for someone else, even if their needs are completely different. You know the stack: React wrapped in Next.js wrapped in Vite, TypeScript sprinkled with decorators, talking to a NestJS backend that queues messages via Kafka through a Saga orchestrator, all dockerized, deployed to Kubernetes with 17 microservices each running Polly, MediatR, and custom logging middleware nobody asked for. All to serve a contact form. Design patterns and architectural styles have also become sacred texts. Everyone wants to push microservices or CQRS or some other big idea. While these concepts can work well, they're often applied where they're not needed. The result is more layers of abstraction, more dependencies, more everything but clarity. This obsession with the “right” frameworks and patterns can lead to months of refactoring code or re-engineering systems that were working just fine. I’m not opposed to continuous improvement, quite the contrary. But when improvement becomes an endless cycle of rework, it feels like we’re seeking problems instead of solving them. This isn't engineering anymore. It's theater. Patterns such as SOLID, or this religion of Clean Architecture, have become sacred. Tools are chosen before problems are understood, and developers are rewarded more for stack conformity than for delivering value. We've built an industry optimized not for results, but for rituals. ### The Disconnect Between Tools and Usefulness We have so many tools at our fingertips: containerization, cloud services, sophisticated IDEs, automation pipelines, and more. You’d think with all these advancements, developing software would be smoother and cheaper. Instead, we often see ballooning budgets and longer timelines. It might be because we’re not focusing on the most important parts of software development: [understanding the domain, the core problem and building](https://ricofritzsche.me/slow-down-understand-the-domain-and-then-build/) just enough software to address it. We often get lost in the overhead of our chosen stack, spinning our wheels trying to fit a solution into the shape of a fashionable tool. Sometimes, we forget to ask: “Who really needs this feature?” or “Will this tool actually improve our workflow?” We stop evaluating what’s genuinely useful, and we focus on shipping the next shiny piece of tech. The human aspect, asking questions, working closely with users, iterating on real feedback, gets drowned out by hype. The goal should never be to use “more tech". The goal should be to use *just enough* of the right tech to solve the right problem, and then get out of the way. ### A Call for Simplicity I don’t think frameworks or patterns are bad on their own. They are powerful and effective when used wisely. But the industry seems to have drifted into a frenzy of chasing trends, as if the sheer number of integrated frameworks is a badge of honor. We spend millions without a second thought, all in the name of “adopting best practices.” Often I see posts on LinkedIn listing the “9 essential libraries” every .NET project should have, or similar. Serilog, MediatR, FluentValidation, Polly, EFCore, Dapper, MassTransit and more. All packaged up as must-haves, no matter the context. It’s not that these tools are inherently wrong. They’re well-built and solve real problems. But somewhere along the way, we stopped asking whether we actually need them, or if we’re just reaching for them out of habit, or worse, to keep up appearances. My hope is that more teams will step back and think carefully about what they’re building. Does every product need microservices? Do we need 50 libraries for the simplest tasks? Are we automating processes that no one actually wants or needs? These are the kinds of questions we should ask. 1. Start Small – Implement features in the simplest possible way. 2. Validate Real Needs – Make sure there’s a genuine need behind each problem you decide to solve. 3. Choose Tools Wisely – Pick frameworks and patterns that match your actual requirements, not just today’s trends. 4. Refine, Don’t Reinvent – Improve existing solutions incrementally instead of constantly rebuilding from scratch. ### Conclusion Software development should be about creating real value. It should solve real problems in a sensible, cost-effective way. But we’re surrounded by examples where projects go off the rails, budgets balloon, and new frameworks multiply without a clear reason. It’s time to [cut through the noise](https://ricofritzsche.me/cutting-through-the-noise-a-reflection-on-the-true-essentials-of-software-development/). By focusing on simplicity and genuine usefulness, we can avoid becoming part of a “problem machine.” Instead, we can build products that actually help people, without burying ourselves in unnecessary complexity. That’s the software industry I’d like to see: one where technology serves us, not the other way around. *Cheers*! ### Zero-Abstraction SQL: Why Raw Queries Beat ORMs for Clarity and Control URL: https://ricofritzsche.me/zero-abstraction-sql-why-raw-queries-beat-orms-for-clarity-and-control/ Last updated: 2025-04-29T21:04:19.000Z I have been developing software systems for about 30 years, long enough to see plenty of data-access trends come and go: massive ORMs, custom repositories, code-generation tools, and advanced query builders. They all have their merits, but over time I found myself coming back to a simple truth: raw SQL is the most transparent, flexible, and performant way to interact with a database. This approach, which I call "zero-abstraction SQL", doesn't mean that you ignore helpful libraries or frameworks. It just means that you write your own queries, maintain direct control over them, and let your language's tools help you without adding heavy layers on top. This aligns with my broader philosophy of simplifying systems by reducing unnecessary abstractions, as I’ve discussed in my recent articles on functional cores and imperative shells. You can do this in many ecosystems; I happen to use Rust a lot these days because it pairs incredibly well with a library called *SQLx*. *SQLx* allows me to keep my queries in plain text while still benefiting from compile-time checks and async operations. But conceptually, writing raw SQL in any language that supports it can deliver similar clarity and control. For example, *JOOQ* remains the strongest equivalent to *SQLx* in Spring Boot. ## The Essence of Zero-Abstraction SQL ### Plain SQL Statements, Right in the Code Instead of burying your logic behind an ORM or elaborate DSL, you write each query yourself. This means no unexpected “lazy loading,” no mysterious joins, and no performance bottlenecks you can’t see. If something needs tuning, you can open up your code and modify that exact SQL statement. #### Optional Compile-Time Checks In Rust, SQLx gives me the ability to verify queries at compile time, catching mistakes like typos in column names or mismatched types. This ensures your SQL aligns with the database schema before you even run the application. You can think of this as a best-of-both-worlds scenario: raw SQL with an added safety net. Outside Rust, you might rely on tests or migrations, but the principle remains. Stay close to the database’s reality. #### Async I/O Without Reflection Many modern applications handle high concurrency. With *SQLx* in Rust, all calls are asynchronous, and there’s no hidden reflection that slows things down. You can adopt a similar mindset in other stacks, using raw SQL with non-blocking I/O frameworks. #### Consistent, Predictable Performance Because you control the queries, you avoid the guesswork of “What SQL is my ORM generating?” or “Why is my code scanning entire tables unexpectedly?” If performance problems do arise, you can pinpoint them quickly. I’ve seen huge teams benefit from having fewer layers in the debugging process. ## A Brief Look at the Code Below is a shortened Rust example using *SQLx* that illustrates how straightforward this can be. The function registers a new asset, first checking for a naming conflict, then inserting the new row. Even if you’re not a Rust user, the main idea applies in other languages: keep your raw SQL front and center, and [apply domain logic separately.](https://ricofritzsche.me/applying-functional-core-and-imperative-shell-in-practice/) ```Rust use sqlx::PgPool; use uuid::Uuid; pub async fn register_asset( pool: &PgPool, tenant_id: Uuid, name: &str, ) -> Result<(), sqlx::Error> { // Check if an asset with the same name exists for the tenant let exists = sqlx::query_scalar!( "SELECT 1 FROM assets WHERE tenant_id = $1 AND name = $2", tenant_id, name ) .fetch_optional(pool) .await?; if exists.is_some() { // Return an error or handle it in your own way return Err(sqlx::Error::RowNotFound); } // Insert the new asset sqlx::query!( r#" INSERT INTO assets (id, tenant_id, name) VALUES ($1, $2, $3) "#, Uuid::new_v4(), tenant_id, name ) .execute(pool) .await?; Ok(()) } ``` Notice how there’s no ORM “model” struct. The logic is straightforward, and the queries remain entirely in your control. If you want to change the indexing strategy or use a specific Postgres feature, you can do so by adjusting the SQL directly. Meanwhile, SQLx’s *query!* macros (like *query\_scalar!* above) will check the validity of columns and types at compile time, giving you confidence in your queries before your app even runs. ## But What About Migrations, Schema Changes, and Other Realities? I think these are natural questions: ### Do I lose out on auto-generated schemas or fancy migrations? Not necessarily. You can still use tools like *sqlx-cli* or other migration frameworks. The difference is that you remain in the driver’s seat, writing migration files by hand (or selectively using generation tools) rather than leaning on an ORM’s assumptions. ### Isn’t raw SQL more prone to errors? Potentially, yes! If you never validate your queries. But if your environment offers compile-time checks (like Rust+SQLx, or Spirng Boot+JOOQ), or if you maintain thorough tests, you’ll catch mistakes early. Some teams also run static analyzers or database-linting tools to ensure consistency. ### How does this scale for big applications? In large projects, people usually fear “too many raw queries.” But in practice, grouping queries by feature or module, naming them clearly, and establishing a consistent folder structure leads to manageable code. With an ORM, you might have hundreds of model classes or complicated DSL calls. With raw SQL, you have a set of statements that do what they say, in a form that every developer or DBA can read directly. ## Why I Stick to It After Many Years Having worked on all types of projects (monoliths, microservices, data pipelines, real-time systems), I’ve come to realize that massive abstractions often hide more than they help. This echoes my earlier arguments about replacing layered architectures with functional cores and imperative shells, where clarity and control take precedence. Writing raw SQL might look old-school at first, but it fosters genuine clarity and control. Once you get used to it, you’ll notice how much friction disappears. You stop wrestling with mysterious ORM quirks, and instead write exactly the query you need. Rust has been my go-to language for this pattern because SQLx’s macros provide that wonderful safety net at compile time, along with *async* performance. But the guiding idea isn’t Rust-specific: - keep logic pure, - keep the SQL visible, - rely on your language’s best tools to catch errors and maintain performance. I hope this gives a sense of why “zero-abstraction SQL” has become my preferred method. It’s not about reinventing the wheel; it’s about knowing precisely how that wheel turns and being able to optimize it when needed. If you have similar experiences, or if you’re on the fence about letting go of an ORM, give raw SQL a try. You might be surprised at how it simplifies everything in the long run. ## This Might Work for Small Projects, But Not for Large Ones… I can imagine that many developers assume raw SQL quickly becomes unmanageable at scale. My experience has been the opposite: I’ve worked on large, complex enterprise systems where layers of abstractions, frameworks, and generated code ended up obscuring performance pitfalls and increasing complexity. By keeping queries “simple and pure” and grouping them by feature , you actually reduce the cognitive load. This approach complements the vertical slicing I’ve advocated for in my articles on functional programming, where each feature owns its logic and data access. Instead of chasing down which part of the ORM’s auto-generated code is causing problems, you locate the relevant SQL statement in a well-organized feature slice. This level of clarity scales surprisingly well. A large project that’s split into manageable modules and features can maintain hundreds (or even thousands) of raw SQL statements without confusion. Each statement does what it says on the tin, and you can tweak it as your data patterns evolve. Rather than adding layers that mask what’s really happening in the database, zero-abstraction SQL keeps the entire team honest about schema design, index usage, and query efficiency. It’s not a shortcut or a hack; it’s simply a direct conversation with your database, making it easier to spot performance issues, maintain code, and onboard new team members who can jump right in without learning an intricate “ORM dialect.” *Cheers*! ### Immutability in Data Flows for Safer, Simpler Code URL: https://ricofritzsche.me/immutability-in-data-flows-for-safer-simpler-code/ Last updated: 2025-04-22T08:38:21.000Z In my previous article, I demonstrated the power of a functional core, imperative shell architecture in Rust to achieve testable logic and clear boundaries. A key principle underpinning this approach is immutable data flow, which means treating data as immutable values rather than mutable, stateful objects. In this follow-up, we will explore the core principles of immutability in data flows, and why immutable data objects (as used in a functional core) offer significant advantages over traditional mutable patterns (e.g., using setters to update state). We will explore how immutable state leads to better reasoning about code, fewer side effects, safer concurrency, and easier testing. Along the way, we'll use practical examples in JavaScript and Rust to compare mutable and immutable data handling. We'll pay particular attention to why Rust is particularly well suited to immutability; from its ownership and borrow checkers, which enforce safe mutations, to powerful features like pattern matching and enums, which naturally encourage an immutable, functional style. ## Immutable Data Flows A quick recap: the Functional Core, Imperative Shell paradigm divides an application into a pure, logic-focused core and an outer shell that handles side effects. The *functional core* deals with domain logic using pure functions and accepts inputs and produces outputs without altering external state. The *imperative shell* is in charge of real-world side effects (database I/O, HTTP calls, etc.), but it calls into the core for pure computations. This separation reduces complexity and improves testability by keeping side effects out of the core business logic. One crucial technique that makes a functional core possible is treating data as immutable as it flows through the system. In an immutable data flow, data is never modified in-place. Instead of objects that carry state which gets mutated over time, we use values that, once created, do not change. Any "change" to data is represented by producing a new data object and leaving the original untouched. This is in contrast to the typical object-oriented mutable pattern, where an object’s fields can be updated via setter methods or by direct assignment, meaning the same object’s state evolves over time. By embracing immutability in the core, we ensure that domain functions have no hidden side effects. Given the same input, an immutable pure function will always produce the same output without altering any external state. This makes the flow of data easier to follow and reason about. Data goes in, data comes out, and nothing else changes in between. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/04/Screenshot-2025-04-22-at-09.11.16.png) Given the same input, you'll get the same output, with no side effects occurring during processing. In practice, adopting immutable data flows means that when the imperative shell layer receives input (say, an HTTP request or a database record), it converts it into *immutable* domain objects. These are passed into the functional core for processing. The core functions return new immutable objects (or simple values) as results, which the shell then uses to produce outputs (sending a response, updating the database, etc.). At no point does the core mutate a shared state or global variable; any necessary state changes are handled by the shell in a controlled manner (e.g. saving a new record to the DB) rather than by the core logic itself. ## Mutable vs. Immutable State: Understanding the Difference Before diving into benefits, let's clarify the difference between mutable and immutable state in a program’s data flow: - Mutable state means that the state of an object can be changed after it's created. For example, you might have a User object and call *user.setName("Alice")* to change the name of the user, or change it by direct assignment (*user.name = "Alice"*). The same User instance now has new data. Mutable patterns often rely on setters or direct field mutations to update state. - Immutable state means that once an object (or value) is created, it cannot be changed. To 'change' an immutable object, you create a new object with the updated value. For example, instead of calling *setName*, you would create a new User object that is identical to the old one, but with the Name field set to "Alice", leaving the original user unchanged. In languages such as JavaScript or Python, this often means using techniques such as object spreading (*{ ...oldObj, changedProp: newValue }*) or library functions to create new copies. In purely functional languages (Haskell, Clojure, etc.), all data structures are immutable by default, so any update will return a new structure. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/04/Screenshot-2025-04-22-at-09.57.17.png) Mutable patterns edit objects in place, while immutable ones make new copies, keeping data safe. Mutable patterns with setters can make data flows harder to follow. If multiple parts of your code hold a reference to a mutable object, a change in one place will be visible everywhere that object is referenced, unfortunately sometimes unexpectedly. For example, if you pass an object to two functions and the second function mutates it, the first function’s view of that object is also affected, even though it had no idea about the change. This "*action at a distance"* can introduce bugs that are difficult to trace. You might find yourself wondering "who changed this value?" or "when did this object’s state flip from X to Y?" during debugging. By contrast, immutable data objects act like snapshots of state. If you pass an immutable object to two functions, and one of them creates a new object to represent a change, the other function still sees the original, unmodified snapshot. There is no surprise modification happening behind the scenes. **Any change is explicit:** a new value is returned and you can clearly see in the code where that new value came into existence. This leads to a more transparent data flow. In essence, immutability turns state changes into *value transformations.* Each function takes an input value and computes an output value, without ever hidden modifying the input. This aligns perfectly with the functional core idea of having clear inputs and outputs and no side-effecting operations in between. ## Benefits of Immutability Why go through the trouble of creating new objects for every change? It turns out that embracing immutability yields several major benefits. Immutable code is easier to understand because variables don't change unexpectedly. Once an object is created, its properties remain fixed, making it easy to predict its behavior. You don't have to mentally keep track of how a piece of data might evolve elsewhere in the program, because it won't. This often leads to improved readability of the code, as you can treat each value as a constant **fact**, and the traceability of changes is improved. By avoiding in-place mutations, we reduce side effects. A side-effect is any change in system state or observable interaction with the outside world that occurs during function execution (such as modifying a global variable, or updating an object that exists outside the function). Immutability naturally limits side effects, because functions can't change existing objects, they can only create new ones. This makes functions closer to pure, which means less chance of unintended interactions. Code that avoids side effects is less prone to bizarre bugs, where one part of the system inadvertently breaks something in another. For example, if an object is immutable, once you have verified that its state is correct, you can trust that it will remain valid throughout its lifetime; no background thread or other code will change it without your knowledge. This leads to improved safety in the code. In a functional core context, this is critical: domain logic can run without worrying that some external state will be changed midway. Any necessary external change is performed in the imperative shell *after* the pure logic computes the new desired state. Immutability greatly simplifies concurrent programming. If multiple threads or asynchronous tasks share access to some data, mutable state would require careful synchronization (locks, mutexes, etc.) to avoid race conditions (e.g. two threads updating a value at the same time). Immutable data, on the other hand, can be freely shared between threads because no thread can modify it. There is no risk of two threads racing to modify it. This eliminates the need for locks in many scenarios, which both avoids complexity and improves performance by avoiding conflicts. If all concurrently accessed data is immutable and all functions are pure, then dangerous concurrency hazards are avoided. If some data is mutable, then things get tricky and synchronization is needed to make accesses safe. In Rust. for instance, the compiler enforces at compile time that data races are impossible for this very reason. You cannot have an unsynchronized mutable reference to data shared between threads. Immutability thus enables what Rust calls[ fearless concurrency](https://doc.rust-lang.org/book/ch16-00-concurrency.html?ref=ricofritzsche.me), because you can confidently share and use data in multiple threads without fear of low-level race conditions. Code that avoids mutable state is generally easier to test. If the output of a function depends only on its input, and not on some hidden mutable state, then you can test it by simply calling it with a variety of inputs and asserting the outputs. There's no need to set up complex object graphs or reset global variables between tests. You also reduce the reliance on mocking in your tests. There is no need to mock an object just to intercept or check that a particular field has been set; you can check the value returned directly. Debugging is also simplified because state doesn't change unexpectedly, and bugs are easier to reproduce and reason about. Since the state of an immutable object doesn't change, it's much easier to test code and find bugs. If something does go wrong, you can pinpoint which transformation produced an incorrect value because each transformation is explicit and isolated. ## Mutable vs. Immutable Data Handling in Practice Let's go through a simple scenario in JavaScript to compare mutable and immutable data handling. I'll use a very simple example of manipulating a user's name data to demonstrate the concepts. ### Example: Updating an Object JavaScript (but also C#, Java, etc.) makes it very easy to use mutable state because any object can be changed at will. For example, consider an object representing a user: ```javascript // Mutable approach: modify the object in-place function lowerCaseNameInPlace(user) { user.firstName = user.firstName.toLowerCase(); user.lastName = user.lastName.toLowerCase(); } const userA = { firstName: "John", lastName: "DOE" }; lowerCaseNameInPlace(userA); console.log(userA); // Output: { firstName: "john", lastName: "doe" } (userA was mutated) ``` In the code above, *lowerCaseNameInPlace* takes a User object and mutates it by setting its *FirstName* and *LastName* to lowercase. After calling the function, userA itself has been changed. If there were other references to the same userA object, they would also see the updated name. This may be an unintended side effect if those references weren't expecting userA to change. Now contrast this with an immutable approach in JavaScript, where we create a new object for the modified data, rather than modifying the original: ```javascript // Immutable approach: return a new object instead of mutating function lowerCaseName(user) { return { firstName: user.firstName.toLowerCase(), lastName: user.lastName.toLowerCase() }; } const userB = { firstName: "John", lastName: "DOE" }; const userBLower = lowerCaseName(userB); console.log(userB); // Output: { firstName: "John", lastName: "DOE" } (userB remains unchanged) console.log(userBLower); // Output: { firstName: "john", lastName: "doe" } (new object with lowercased names) ``` Even in this simple example, the benefits of the immutable approach are obvious in terms of predictability: userB is guaranteed to remain the same after the function call, so if some other piece of code relies on userB remaining uppercase, it still does. In the mutable version, that other code could break because userA has been changed without its knowledge. When building applications, for example using frameworks like React/Redux in the JavaScript world, this immutable update pattern is extremely common to ensure that state changes are predictable and traceable (Redux even enforces that reducers must not mutate state). ## Pattern Matching and Enums for State Changes [Rust](https://doc.rust-lang.org/?ref=ricofritzsche.me) is a programming language with strong support for immutability. In Rust, values are immutable by default. You must explicitly choose mutability using the *mut* keyword. This design encourages you to think carefully about where mutation is really needed. Let's look at a more domain-oriented example, inspired by the earlier geofencing scenario from [my previous article.](https://ricofritzsche.me/applying-functional-core-and-imperative-shell-in-practice/) Rust's immutability strengths really shine when you use enums and pattern matching to manage state transitions or variations in state. Enums in Rust let you define a type by enumerating its possible variants (a kind of algebraic data type), and pattern matching lets you safely destructure and handle each variant. This leads to code that represents state cleanly, without mutable flags or complex logic. Suppose we have an enum representing whether an asset is inside or outside a geofence: ```Rust enum GeofenceStatus { Inside, Outside } ``` And another enum for the movement event that might occur when a new location update comes in: ```rust enum Movement { Entered, Exited, StayedInside, StayedOutside, Unknown } ``` We want a function that, given the previous status of an asset (if any) and the new status, determines the movement event (e.g. if it was outside and now inside, that's an "Entered" event, if it remained inside, that's "StayedInside", etc.). We can write this in Rust using pattern matching on a tuple of the old and new status: ```rust fn compare_status(old: Option, new: GeofenceStatus) -> Movement { match (old, new) { (Some(GeofenceStatus::Outside), GeofenceStatus::Inside) => Movement::Entered, (Some(GeofenceStatus::Inside), GeofenceStatus::Outside) => Movement::Exited, (Some(GeofenceStatus::Inside), GeofenceStatus::Inside) => Movement::StayedInside, (Some(GeofenceStatus::Outside), GeofenceStatus::Outside) => Movement::StayedOutside, _ => Movement::Unknown, } } ``` This compare\_status function is a pure, unchanging function that examines the inputs and returns a new movement value without modifying anything in place. The use of enums and pattern matching makes it very clear how the output is derived from the inputs. Each arm of the match covers a possible combination of previous and new states, yielding the corresponding motion. There is no need for a mutable object to track state internally; we don't do anything like *self.lastStatus = newStatus* or *self.lastMovement = ...* here. If we were writing this in a mutable OOP style, we might have a class like AssetTracker with a method that updates its internal state fields and returns nothing (or returns the event after side-effecting its fields). In this approach, we simply take the old state and the new state as values and calculate another value. The caller can decide what to do with this movement, e.g. log it or send a notification, and if necessary store the new state somewhere (probably in the imperative shell layer, e.g. update the database record for the asset's last known state). The core logic remains free of side effects. This example shows why Rust is great for designing immutable data flows: the language encourages you to use strong types (enums for states) and pattern matching to make transitions explicit. We didn't need a bunch of if/else statements with flags, nor did we mutate any objects; we considered all possible cases in a clear, declarative way. Rust's enums also ensure that if we add a new variant (say *GeofenceStatus::Unknown*) in the future, the compiler will force us to address it in the match (or use a catch-all \_ as we did for safety). This results in very robust logic that's easy to test extensively. We can feed in different combinations of old/new statuses and check the function's output without any setup or side effects. ## Conclusion Immutability in data flows isn't just a theoretical ideal from functional programming. It is a very practical tool for managing complexity in real-world software. By using immutable data objects instead of mutable ones with setters, we gain clarity (we know where and when data changes), safety (fewer side effects and no unintended interactions), concurrency without fear (immutable data can be shared freely, enabling parallelism), and ease of testing (pure functions are a tester's dream). These benefits address the very problems that plague large codebases: unpredictable behavior, difficult debugging, and code that's hard to extend or reuse. *Cheers*! ### Applying Functional Core and Imperative Shell in Practice URL: https://ricofritzsche.me/applying-functional-core-and-imperative-shell-in-practice/ Last updated: 2025-05-22T08:08:03.000Z In my [previous post](https://ricofritzsche.me/simplify-succeed-replacing-layered-architectures-with-an-imperative-shell-and-functional-core/), I introduced the concept of shifting from layered architectures to a functional core and imperative shell. My main goal was to reduce complexity, clarify boundaries, and improve testability. The idea itself is straightforward: the functional core handles all domain logic without side effects: accepting inputs, returning outputs, and never directly interacting with databases or external systems. Meanwhile, the imperative shell deals with real-world concerns like HTTP endpoints, persistence, and third-party integrations, orchestrating calls to the core without letting infrastructure concerns pollute domain logic. After sharing mostly theory before, I received plenty of questions about real code samples. So this time, I’ll walk through building an API for asset tracking and geofencing, structured as vertical slices. Specifically, I’ve taken the “Track Asset” feature from my Geofencing API and streamlined it for this example. I also want to restate the motivation behind these articles. After over 30 years in software development, I’ve seen too many teams drown in complexity, often due to applying patterns that look elegant on paper but fail in practice. My aim is always to solve real-world problems in a sustainable, maintainable way, not to show off with complicated solutions. A functional core and an imperative shell, in my experience, keeps the architecture lean, clear, and ready to handle future changes. ## Weekly Tips to Master Software Architecture Newsletter I provide lean, effective software-architecture strategies that cut through the noise. Subscribe Email sent! Check your inbox to complete your signup. Trusted by 2,500+ developers on Medium. ## The Use Case: Asset Tracking To illustrate, imagine a logistics scenario with multiple trucks or devices that periodically report their location. The service then checks whether these devices are within a defined geofence. A geofence is a geographical boundary within which you can track events such as entry, exit or presence. From a business standpoint, a project like this needs to: 1. **Track an asset’s movement** over time, storing the last known geofence status (inside or outside). 2. **Respond to new location updates** from an asset and determine if it has entered or exited the geofence. 3. **Persist data** so the system can handle real-time or historical queries. The domain logic is fairly straightforward: given a latitude and longitude, decide whether it falls within the geofence boundaries. Then compare that new status to the asset’s previous status and figure out whether it changed from outside to inside (an entry), from inside to outside (an exit), or remained the same. However, many code bases place this functionality deep inside an all-in-one “service” layer or scattered across multiple “manager” classes. That approach becomes messy fast, especially when test coverage is critical. By contrast, a functional core and feature-based slices keep these rules in pure logic modules, tested with no dependencies on web frameworks or databases. ## Why I use Feature Slices Vertical slicing moves away from the traditional code technical code structures of “controllers in one folder,” “services in another,” “models in a third,” etc. Instead, each feature is an end-to-end module that contains everything needed to do its job: domain, logic, and external adapters: - A domain/model file for input/output types and domain data structures. - A logic file for purely functional rules (e.g., checking geofence membership). - A handler file to accept requests and produce responses. Organizing it this way ensures that everything related to the “track asset” feature sits in one place. Anyone coming into the code base can see precisely how an asset-tracking call flows from HTTP all the way into the domain rules. There is no more hunting through layers. ## Project Structure Overview I have chosen Rust for this example. The functional core and imperative shell approach works just as well in C#, F#, Java, or any other language that can cleanly separate logic and infrastructure. I use Rust here because its strict compiler and memory safety rules inherently promote immutability and explicit state management, making it a perfect complement to the principles of the functional core. Also, Rust's performance and concurrency advantages scale well when you need to process tracking data in real time. Below is a simplified look at how I organize my code: ```yaml src/ ├── main.rs // Thin entrypoint ├── lib.rs // Wiring, DB setup, route config ├── shared/ // Shared infra: DB, errors, ... │ ├── db.rs │ ├── error.rs │ └── mod.rs └── features/ // Vertical slice directory ├── mod.rs // Summarizes feature slices └── track_asset/ ├── mod.rs ├── handler.rs ├── model.rs ├── logic.rs └── tests.rs // domain logic (core) tests ``` The shell is represented by main.rs, where the application starts, reads environment variables, and creates the HTTP server. The logic for hooking up routes (like /track) and establishing the database connection is done in lib.rs. All domain logic remains in small Rust files dedicated to the “track asset” feature. ## The Functional Core: Pure Domain Logic Within track\_asset/logic.rs the geofence check is a pure function: ```csharp pub fn check_geofence(loc: &Location) -> GeofenceStatus { if loc.lat >= 40.0 && loc.lat <= 42.0 && loc.lon >= -74.0 && loc.lon <= -72.0 { GeofenceStatus::Inside } else { GeofenceStatus::Outside } } pub fn compare_status(old: Option, new: GeofenceStatus) -> Movement { match (old, new) { (Some(GeofenceStatus::Outside), GeofenceStatus::Inside) => Movement::Entered, (Some(GeofenceStatus::Inside), GeofenceStatus::Outside) => Movement::Exited, (Some(GeofenceStatus::Inside), GeofenceStatus::Inside) => Movement::StayedInside, (Some(GeofenceStatus::Outside), GeofenceStatus::Outside) => Movement::StayedOutside, _ => Movement::Unknown, } } ``` There’s no database logic, no HTTP calls, no thread locks. It's just raw computation. This is the functional core. It’s deliberately cut off from any side effects so it can be tested in isolation. ### Domain Testing Because the domain doesn’t depend on Actix Web or SQLx, it’s trivial to write unit tests directly. For instance: ```csharp #[cfg(test)] mod tests { use super::*; #[test] fn test_geofence_inside() { let loc = Location { lat: 41.0, lon: -73.5 }; let status = check_geofence(&loc); assert_eq!(status, GeofenceStatus::Inside); } #[test] fn test_movement_entered() { let old = Some(GeofenceStatus::Outside); let new = GeofenceStatus::Inside; assert_eq!(compare_status(old, new), Movement::Entered); } } ``` This style of testing is fast, runs without spinning up servers, and clarifies that domain correctness stands independent of infrastructure. For me, that’s the biggest advantage of the functional core approach: no mocking frameworks, no boilerplate, just pure logic under test. ## The Imperative Shell: Handler & Database Integration Meanwhile, the shell code is where side effects happen. In handler.rs there are a function that Actix calls when a request comes in, retrieves the previous status from the database, compares it with the new geofence result, then persists the updated status. A snippet looks like this: ```rust pub async fn track_endpoint( input: web::Json, db: web::Data, ) -> Result { let asset_id = &input.asset_id; let location = Location { lat: input.lat, lon: input.lon }; let new_status = check_geofence(&location); let old_status: Option = sqlx::query_scalar( "SELECT last_status FROM asset_status WHERE asset_id = $1" ) .bind(asset_id) .fetch_optional(db.get_ref()) .await .map_err(AppError::from)?; let parsed_old = old_status.and_then(|s| match s.as_str() { "Inside" => Some(GeofenceStatus::Inside), "Outside" => Some(GeofenceStatus::Outside), _ => None, }); let movement = compare_status(parsed_old, new_status.clone()); // Upsert the new status sqlx::query( "INSERT INTO asset_status (asset_id, last_status, updated_at) VALUES ($1, $2, now()) ON CONFLICT (asset_id) DO UPDATE SET last_status = $2, updated_at = now()" ) .bind(asset_id) .bind(format!("{:?}", new_status)) .execute(db.get_ref()) .await .map_err(AppError::from)?; Ok(HttpResponse::Ok().json(TrackOutput { asset_id: asset_id.clone(), movement: format!("{:?}", movement), })) } ``` Here the code is more verbose because it has to deal with real world concerns: pulling data from PostgreSQL, handling potential errors, and sending an HTTP response. Note how the domain logic (*check\_geofence*, *compare\_status*) remains the same pure functions from earlier. ### Integration Test Testing the "whole system" with the real HTTP endpoint can be done using reqwest in a separate tests/ directory: ```rust #[tokio::test] async fn test_track_endpoint_end_to_end() { let client = reqwest::Client::new(); let base_url = "http://localhost:8080/track"; let payload = serde_json::json!({ "asset_id": "test-asset-1", "lat": 40.5, "lon": -73.9 }); let resp = client .post(base_url) .json(&payload) .send() .await .expect("Request failed"); assert!(resp.status().is_success()); let json: serde_json::Value = resp.json().await.expect("Invalid JSON"); assert_eq!(json["asset_id"], "test-asset-1"); // movement could be Entered, StayedInside, Exited, etc. } ``` By spinning up the Actix server on a known port, I can verify the entire flow: from request parsing, through the imperative shell, into the database, and back out to the client response. This ensures confidence that all external pieces work together. ## Putting It All Together Functional core and imperative shell solve some age-old problems in software architecture: **Cohesion:** Domain logic is in one place, side effects are in another. It's obvious where to add new business rules and where to integrate new data sources. **Easy testing:** The domain logic can be tested by itself, without external services, while the imperative shell can be tested end-to-end with real HTTP calls. **Minimal coupling:** Each vertical slice stands on its own, so an evolution in one feature rarely breaks another. In an asset tracking scenario, this clarity is invaluable, especially when working to deadlines or with large teams. There's no need to wade through 'service' classes that mix business rules and repository logic. Instead, the slices remain comprehensible. The Track Asset Slice is about receiving a location, updating the geofence status and returning the updated status. ## Next Steps Anyone wanting to adapt this approach to other feature, like order processing, user registration, or IoT sensor data, you name it, should find it straightforward: - Add a new feature slice folder (e.g. order\_processing). - Create model.rs, logic.rs, handler.rs, etc. within it. - Keep the domain pure, put side effects in the shell. - Register the route in features::init\_routes. It’s that simple. The approach scales well, and new developers generally appreciate that each folder is a small, self-contained part of the application. If the code grows beyond one domain service, multiple crates can share domain logic while each service has its own shell. For a fully working reference, check out the [GitHub repository](https://github.com/ricofritzsche/func-core-feature-example?ref=ricofritzsche.me). The code inside demonstrates how to stand up a vertical slice with a functional core (no side effects) and an imperative shell (all the external calls, framework integrations, database I/O). This example was deliberately small, but the pattern works for complex applications, too. It leads to clearer boundaries, simpler tests, and fewer headaches when you refactor or scale the application. That’s all for this follow-up of [Simplify & Succeed: Replacing Layered Architectures with an Imperative Shell and Functional Core](https://ricofritzsche.me/simplify-succeed-replacing-layered-architectures-with-an-imperative-shell-and-functional-core/). The next time a design challenge arises, consider whether your business logic can be made pure, and whether you can isolate side effects in feature slices. Rust’s powerful compiler helps reinforce immutability, and the result is often more robust, maintainable software. *Cheers*! ### Simplify & Succeed: Replacing Layered Architectures with an Imperative Shell and Functional Core URL: https://ricofritzsche.me/simplify-succeed-replacing-layered-architectures-with-an-imperative-shell-and-functional-core/ Last updated: 2025-05-22T08:07:26.000Z The choice of software architecture can have a profound effect on how maintainable, testable and traceable a system becomes. Two widely discussed patterns, Hexagonal Architecture (HA) and Clean Architecture (CA), are often combined with Domain-Driven Design (DDD) and Rich Domain Models to isolate business logic from technical details. However, my findings and practical experience over the last few decades in a wide variety of projects indicate that these combinations can unintentionally introduce functional dependencies into the domain, making testing and maintenance more difficult. An alternative, Pure Functions in a Functional Core, offers a simpler, more testable approach that still meets the core goals of DDD while avoiding many of the pitfalls of layered architectures. In this article, we’ll explore how HA/CA with DDD and rich domain models can create unwanted complexity, and why I believe using Pure Functions in a Functional Core can lead to cleaner, more testable code. ## Weekly Tips to Master Software Architecture Newsletter I provide lean, effective software-architecture strategies that cut through the noise. Subscribe Email sent! Check your inbox to complete your signup. Trusted by 2,500+ developers on Medium. ## Understanding the Comparison: HA/CA with DDD vs. Pure Functions ### Hexagonal Architecture & Clean Architecture Basics The hexagonal architecture (sometimes called ports and adapters) is designed to isolate the domain from external systems via ports (abstract interfaces) and adapters (concrete implementations). For example, in a user export feature, the domain might define an *ExportUserport*, and different adapters (such as *ExportUserToCSV* or *ExportUserToPDF*) implement the actual export details. Clean Architecture uses a similar concept, structuring software into concentric layers: the domain at the centre (entities and use cases), surrounded by the application layer, then the infrastructure. Both aim to keep the domain “clean”, free from infrastructure details. ![](https://cdn-images-1.medium.com/max/1600/1*Ge4pB0qko8qG9QUnlOGk3Q.png) Hexagonal Architecture Diagram: Ports and Adapters Isolating the Domain ### Domain-Driven Design & Rich Domain Models Domain-Driven Design (DDD) places the business domain at the centre of the software and models it using rich domain objects (entities, value objects, aggregates). Entities encapsulate both data and behaviour in a rich domain. For example, a user can contain the logic for *promoteToAdmin* or *calculateMembershipLevel*. This means that the business rules remain in the model itself and are not scattered across application services. ### Where Complexity Creeps In Despite the theoretical clarity, research and real-world experience indicate that when HA/CA is combined with DDD and rich domain models, the domain layer can become entangled with technical exceptions or error handling. For example: - **Functional dependencies:** When the domain layer calls a repository interface (UserRepository) or a port (ExportUser), it may be handling errors that are inherently technical (e.g., a database connection failure), leaking external concerns into core entities or services. - **Increased testing overhead:** Testing rich domain models that rely on external ports or repositories involves mocks and stubs. Although these interfaces are “abstract”, they still draw in logic related to side effects, making unit testing cumbersome. Over time, these hidden dependencies can make the domain harder to maintain and reason about. Instead of focusing on pure business logic, developers juggle partial technical concerns in the domain or application layer, moving away from the original promise of a “pure domain”. ## Functional Core/ Imperative Shell ![](https://cdn-images-1.medium.com/max/1600/1*lqRjTduUVgrpH_zpf7NC5w.png) Functional Core/Imperative Shell approach, highlighting pure functions at the center and I/O handling at the outer layer ### What Is a Functional Core? A functional core puts all business logic into pure functions. Each function is strictly dependent on its inputs and produces outputs with no side effects. This is in contrast to a rich domain object, which might call a repository or handle I/O directly. Pure functions, by definition: - Have no hidden dependencies on external systems. - Produce consistent outputs for given inputs. - Don’t mutate shared state or produce side effects. The complementary layer is the Imperative Shell, which handles I/O operations and orchestration. This shell reads data from a database, network or file, passes it to the Functional Core, and then takes the Core’s output and writes it back to the outside world. This design ensures that all domain logic remains “pure” and any side effects are safely outside the domain. Stay updated on the latest articles covering everything from programming to architecture. By subscribing, you’ll be the first to get new content, insights, and exclusive code samples. All completely free. [Subscribe Now ](#/portal/signup/free) ### Keeping the Domain Simple Instead of implementing a user entity with methods that call repositories or handle exceptions from the file system, you’d have a function like: ``` calculateMembershipLevel(userData) -> newMembershipLevel ``` It takes a plain data structure (userData), executes the logic, and returns a result (newMembershipLevel). If the logic requires more data (such as a historical purchase record), the shell is responsible for collecting it, but never the function itself. This way, testing the domain function is as simple as passing sample data and checking the output. ![](https://cdn-images-1.medium.com/max/1600/1*5LuDNws-zbc2UiceSYDgiw.png) User data flows into a pure function core, returning new membership levels without side effects. ### Imperative Shell The Imperative Shell manages all of the application’s side effects, such as database interactions, HTTP request handling, logging and external service calls. By placing these operations at the edge rather than in the core, the shell ensures that the pure logic in the functional core remains focused on the business rules. Functionally, the shell acts as a translator between the outside world and the functional core. It accepts incoming data from various sources, reformats it for the pure functions of the core, then processes the output of the core and applies any necessary side effects, such as updating a database or sending a response back to a client. This clear separation keeps external complexity out of the core, making the system easier to understand, test and maintain. ### Unidirectional Code/ Data Flow A key aspect of the Functional Core/Imperative Shell approach is the way in which application workflows are composed. Rather than relying on a complex web of method calls scattered throughout the code, the Functional Core idea advocates simple function composition: each function takes the output of the previous function as its input, creating a clear, unidirectional flow of data. This approach keeps the code coherent and maintainable, especially in larger systems. All side effects and I/O (such as database writes or network requests) live at the edge: **the shell**. The business logic remains in the core as **pure functions** that receive data, transform it and return new data, with no hidden dependencies or side effects. This separation makes the flow of data and where side effects occur crystal clear. Unidirectional flow and self-contained transitions flow in one direction: Data travels through the shell, is passed to a pure function in the core that produces a result, and then control returns to the shell. There are no back-and-forth calls or complicated callback patterns within the domain itself. Self-contained transitions: Each step in a workflow (pure function) performs exactly one task. Its output becomes the input to the next step, reducing dependencies and simplifying code reasoning. ### HTTP Web Server Example Imagine a straightforward HTTP server that handles incoming requests and generates responses: ![](https://cdn-images-1.medium.com/max/1600/1*a1ixTU_WVkZAPw8ppjw4hQ.png) Unidirectional flow chart showcasing how HTTP requests pass through an Imperative Shell to pure functions and back, ensuring minimal dependencies and better testability. The shell receives the HTTP request, parses the headers and extracts the query/body data. The functional core handles the business logic, such as the *calculateDiscount* pure function. The shell takes the result, serializes it (JSON or HTML) and sends it back over the network. The server reads the network, the pure function in the core processes the data with no side effects, and the server (shell) writes a response. Each part is well defined, making it easier to test and debug. The Functional Core/Imperative Shell model brings clarity and resilience to modern software projects, particularly in complex domains where controlling side effects and managing complexity can be an ongoing challenge. By focusing on unidirectional function composition, it becomes easier to track data transformations, limit technical debt, and ensure that critical business logic remains thoroughly testable and free from external concerns. ## Why Pure Functions? ### Fewer Hidden Dependencies In the rich domain model approach, even if you hide infrastructure details behind interfaces, the domain often deals with exceptions or state changes that point to external concerns. Pure functions eliminate these hidden dependencies entirely: **They only accept and return data.** In my experience, “technical impurities” (e.g. database or network error handling) often creep into a rich domain model, making the logic more complex. In the rich domain model approach, even if you hide infrastructure details behind interfaces, the domain often deals with exceptions or state changes that point to external concerns. Pure functions eliminate these hidden dependencies entirely: they only accept and return data. ### Easier Testing Without Mocks Pure functions require **no mocking** of external dependencies because there are none. You can test them by passing input structures and comparing the result with what you expect. Unlike the hexagonal/clean architecture approach, where testing an entity’s method might require mocking a repository or a port interface, pure functions drastically reduce overhead. Practical example: If you have a function like calculateDiscount(price, customerType) -> discount, just call it in a test. No need to set up a fake database or fake external services. ### Reduced Maintenance Overhead Rich domain models are layered with object-oriented features: inheritance, method overrides, repositories, ports, etc. Over time, these can become large and unwieldy if not meticulously managed. Pure functions remain small, composable, and stateless. When you must update or extend business logic, you modify a single function or create a new one, keeping changes isolated and explicit. DDD Alignment: DDD focuses on capturing business rules. Pure functions still do exactly that, but now the domain logic is “pure”, with side effects moved to a separate shell. ## Practical Benefits & Implementation Considerations ### Where This Fits with HA/CA As most of you will have noticed from my recent posts, I am no longer a big fan of HA/CA. This is only because in most practical implementations it is precisely these functional dependencies that are created by dependency inversion. But ironically, you can still use hexagonal or clean architecture as a *structural* blueprint while keeping the domain layer purely functional. The ports/adapters concept can remain at the boundary between the imperative shell and the domain. However, instead of having Rich domain objects that *implement* those ports or handle external exceptions, you place pure functions in the domain. The shell becomes the single place responsible for all side effects. **This approach addresses** my biggest critique: that HA/CA in practice introduces functional dependencies. By adopting pure functions, the domain is truly isolated, and your domain layer stops being burdened with handling technical exceptions or I/O logic. ### Testability in Practice Once you adopt a Functional Core, tests become more about verifying: 1. **Functional correctness** of each domain function (simple unit tests). 2. **Integration** in the shell, ensuring side effects are invoked properly (mocking external systems only in the shell tests). This separation often shortens feedback loops and increases developer confidence. These are two critical factors in large-scale systems. ### Handling Errors One concern with pure functions is handling complex errors. This is especially true if your domain needs to respond to certain external errors (such as “database unreachable”). In a Functional Core world, such a failure is not a domain concern but a shell concern. The shell might return an “error code” or an “option type” to the domain function if needed, but it’s still orchestrated outside the domain logic. As a result, business rules themselves remain focused on business data. ## Comparative Overview Below is a side-by-side look at how the HA/CA + Rich Domain Models compare to a Functional Core with a Pure Functions approach: ![](https://cdn-images-1.medium.com/max/1600/1*etmSxYIzSBSCAr8KNShyyg.png) Comparison table contrasting Hexagonal/Clean Architecture plus DDD against a Functional Core with Pure Functions, focusing on core dependencies, complexity, and testability. ## Final Thoughts Hexagonal Architecture and Clean Architecture, especially when coupled with DDD and rich domain models, were intended to isolate business logic from technical details. In theory, this is admirable. In practice, these designs can create functional dependencies (such as domain objects dealing with repositories or external ports) that make the domain more complicated and less testable. In contrast, Pure Functions, as the heart of a Functional Core, depend only on inputs, produce predictable outputs, and keep side effects in the imperative shell. This approach is proven: - Reduce hidden complexity - Simplify testing by eliminating mocks for domain logic - Align strongly with DDD’s emphasis on *core business rules* - Improve overall maintainability and adaptability If you’ve been struggling to keep your HA/CA or DDD layers free from external noise, consider moving to a functional core approach. Start with a small feature slice, refactor the logic into pure functions, and push the I/O into the shell. You may find that the benefits of clarity, testability and reduced overhead are well worth the change. *Cheers*! ### Beyond the Hype: Event Modeling, Event Sourcing, and Real Choices URL: https://ricofritzsche.me/beyond-the-hype-event-modeling-event-sourcing-and-real-choices/ Last updated: 2025-03-31T18:38:01.000Z Event modeling and event sourcing frequently appear together in blog posts, conference talks, and online discussions. This association might suggest that event modeling requires event sourcing but they're actually distinct concepts that can exist independently. Many software teams incorporate events into their design while using traditional databases, while others prefer recording every change in an append-only event store. This article will explore both concepts and help you determine whether you need event modeling, event sourcing, or neither. ## Event Modeling Event modeling is a design approach that helps teams describe system behavior through a sequence of business events. An "event" is simply something significant that happens in your system (like when someone places a new order or ships an item). If you're familiar with "event storming," you'll find it quite similar. The process involves mapping out events and understanding what happens before and after each one, along with how different parts of the system respond. The focus is on understanding **why** each event matters to the business and **how** it affects data and users. This approach typically leads to a clearer understanding of requirements and encourages domain expert participation since they can describe these events in everyday language. Event modeling doesn't require any changes to your data storage approach. It's simply a technique for designing software that better reflects business needs. You're free to use a relational database if that suits your needs after modeling. ## Event Sourcing Event sourcing is a data storage approach where you record every state change as a separate event, rather than just maintaining the current state. For example, instead of only storing a user's current account balance, you store each transaction that affected it. To determine the current state, you replay these events in order, as if you were reading a detailed log of everything that happened in the system. Consider a bank account example: you store events like "Deposited $100" and "Withdrew $30." The current balance is calculated by adding up all these past events. This approach is particularly valuable when you need a complete history or want to reconstruct past states. Many developers appreciate how event sourcing allows them to debug issues by examining the event log. However, event sourcing comes with added complexity. It requires managing numerous records (in production there are as many as most people have not yet seen) and often demands specialized event storage tools, along with a process to convert stored events into a usable "read model." When business rules evolve, you might need to migrate historical events or handle multiple event versions. Event sourcing is a storage pattern that provides comprehensive historical tracking, but it may be excessive if you don't need such detailed history or replay capabilities. Of course, there are frameworks and tools like Marten in .NET out there that magically take care of everything. But this makes the system dependent on an additional framework and simply shifts the complexity. ## Why People Conflate the Two Event modeling and event sourcing are frequently discussed together because many articles and talks cover them simultaneously. Proponents of event sourcing emphasize the importance of designing systems around events, which naturally leads to discussions of event modeling or event storming. So it might sound like **designing with events = storing all events**. I think that is not true. Especially when you have to solve real problems with the system you're building. You don't want to philosophize about it on a theoretical level and show how great it all is with a simple to-do list example. You can design a system around events (event modeling) and still decide to store only the final state in a well-designed relational database. While some materials promote event sourcing as a "future-proof" or "audit-friendly" solution, this can create a misconception that event-driven system design requires event log storage. However, there is a pragmatic middle ground: you can use events to structure your business logic while still storing data conventionally if it better serves your needs. ## Complexity vs. Business Need Some people think the main reason to avoid an event store is the extra complexity. That is partially true: managing a stream of events is more complex. But the bigger question is whether the business needs it. If your domain needs a thorough audit trail or a way to recreate past states, an event store might be worth it. If not, the added complexity might provide no real benefit. For example, if your business does not need to replay every action or track a detailed event history, a **relational database** can be a strong choice. Relational databases provide **data integrity** and **transactional consistency**. They also offer well-known tools for handling updates, queries, and backups. If you still want some event-like features, you can log specific actions in a queue or external store. Meanwhile, you keep the core entities in tables that enforce constraints and maintain consistency. This way, you meet your primary business needs without adding the complexity of storing every event. **“Do you need Event Sourcing? No, you don’t.”** says Oskar Dudycz in his article [CQRS facts and myths explained](https://event-driven.io/en/cqrs%5Ffacts%5Fand%5Fmyths%5Fexplained?ref=ricofritzsche.me). He points out that Event Sourcing is often misunderstood as being required for CQRS, when it's actually *"just one of many storage options available."* Dudycz emphasizes that you can implement CQRS or vertical slicing without Event Sourcing, supporting the fit-for-purpose principle of using the simplest effective solution. He considers the notion that *"You need to do Event Sourcing"* a common myth, showing that CQRS can work with any storage solution, including relational data models. ## Feature Slices Do Not Mean Event Sourcing I'm a strong advocate for vertical slices and have written extensively about them. While some people suggest implementing event sourcing from the start, I disagree. My philosophy is simple: build software based on actual requirements, avoiding unnecessary complexity or following trends for their own sake. In vertical slice architecture, you organize all aspects of a feature (logic, data handling, and UI components) in one cohesive unit. This approach naturally leads to cleaner code and improved team collaboration. But here's the key point: vertical slicing doesn't demand event sourcing. You're free to build your application in feature slices while using a traditional database. You can even selectively implement event sourcing for specific features where it makes sense. The core principle of vertical slices is keeping related logic together for better maintainability and clarity. The choice of data storage and retrieval methods remains flexible. Some slices may work better with events, while others won't need them at all. Jimmy Bogard's philosophy aligns perfectly with this incremental, needs-based adoption of patterns. When describing [Vertical Slice Architecture](https://www.jimmybogard.com/vertical-slice-architecture/?ref=ricofritzsche.me#:~:text=The%20old%20Domain%20Logic%20patterns,Very%20liberating), Bogard says he prefers to **start simple** with straightforward command/query handlers that act like transaction scripts, and only *refactor to \[more complex\] patterns* when code smells or requirements demand it. This approach means: Don't implement event sourcing or CQRS infrastructure up front for every feature. Instead, let each vertical slice determine its own path: most features can work fine with simple synchronous logic and common database inserts and updates, while select critical areas might benefit from an event-sourced model. Bogard's warning is clear: don't complicate things with unnecessary event buses and distributed logs. I know, it’s easy to get caught up in the excitement around certain patterns. Event sourcing is interesting, and it can solve tricky problems. But it also creates new problems. You need to version your events, you might have to handle replays carefully, and you must manage more data than ever. My recommendation is, look at the business requirements. If you do not see a clear reason to keep every historical detail, do not jump to event sourcing. If you are worried you might need that history in the future, but you are not sure, there are other ways to store data with some form of logging or archiving without going full event sourcing. For instance, you can store snapshots or keep backup logs that record changes in a more compact form. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/03/event-modeling-visual-1.svg) ## Real-World Examples Below are a few scenarios where some teams modeled events but did not store them all: ### E-Commerce Site with Audit on Orders Only A company wanted to know every change that happened to an order (status changes, payment updates, shipment info). They needed to be able to show an audit log. But they did not care about other data like user profiles or comments. So they modeled events across the whole system to clarify their business flows. Then, they chose to store a full event log only for the Order entity. Everything else remained in a normal CRUD database. That solved their main need without requiring them to event-source the entire platform. ### Small Startup with Tight Deadlines A small team wanted a quick launch. They mapped out their domain events to see how the app would work. They decided to store everything in PostgreSQL because they did not have time to learn or implement an event store. They still used the event models to guide how they structured their code and logic. Later, if they needed to add an event store for analytics or compliance, they could revisit the choice. ### Internal App for One Department This was a simple tool for a finance department to track budget approvals. The team did event modeling to understand what steps or events happened in a typical approval. But they realized they only needed the final state (approved or rejected). They chose to store that in a relational database. The design with events was still helpful for clarity, but the actual storage mechanism did not need to be event-based. In each of these cases, the team used events to guide design decisions but chose a simpler storage solution. This balanced approach gave them the best of both worlds. They kept the logic clear and tied to the real business events, but they did not add extra overhead by storing every event in an append-only log. ## A Note on Black-and-White Thinking The software world is full of strong opinions. Some people claim “Event sourcing is the only proper way to build enterprise systems.” Others say, “It’s always overkill and never worth it.” The truth is somewhere in between I guess. Sometimes event sourcing is the right answer. Sometimes it is not. To figure it out, consider these questions: - **Do we need to see the entire timeline of changes for each record?** - **Will we gain value from being able to replay events for new requirements, debugging, or analytics?** - **Is an audit trail required for legal or compliance reasons?** - **Are we prepared to handle the complexity of versioning events and maintaining read models?** If the answer to most of these is “yes,” then event sourcing might bring real value. If not, you might be better off with another design. Also note, you can still design your system in an event-focused way and stop short of full event sourcing. You can keep a record of the most important changes if you like. Or you can go all in and store every single event. That decision should come from the business needs, not from a blog post or conference talk pushing the latest trend, or selling the latest technology. ## Final Thoughts Event modeling helps you think about how your system behaves and how it should react to various domain events. It is a design practice that leads to better conversations with stakeholders and clearer logic. I do this since more than 10 years now. You can do it even if you store data in plain old tables. Event sourcing is a way to store every event that happens in the system. It can be a powerful pattern, but it also introduces overhead. You need a good reason to store everything in an event store, like the need for a full audit history or replay. If you are not sure you need that, it might not be worth the complexity. The best systems fit their actual business needs. If you only need certain features of event sourcing, consider a hybrid or partial approach. If you truly need a full append-only event store, go for it. Just be sure to weigh the trade-offs. By understanding the difference between event modeling and event sourcing, you can avoid the hype. Do not let anyone tell you that you must have an append-only store just because you are talking about “events” in your design. And do not feel locked into a single approach. Take a step back, look at what the business needs, and pick the simplest solution that works. In the end, we should appreciate having so many options for designing systems. It's curious that in software development, we often rush to embrace a single approach as the "ultimate solution" rather than exploring the full range of possibilities. *Cheers*! ### Slow Down, Understand the Domain, and Then Build URL: https://ricofritzsche.me/slow-down-understand-the-domain-and-then-build/ Last updated: 2025-05-03T10:02:41.000Z Many software projects begin with excitement. The team gets together, talks about new ideas, and wants to build something great. In these early meetings, a lot of technical terms are thrown around. Patterns like hexagonal architecture, clean architecture, clean code, DDD, event sourcing etc. are mentioned. They discuss which framework is best for the front end. They discuss which framework is best suited for the front end. They even talk about their preferences in database technologies: NoSQL vs SQL, or what's being hyped at the moment. But in all of this, they rarely talk about the core business problem or domain. It is common to see a strong focus on technology choices before anyone understands what needs to be built and why. I have seen this happen many times. It is not that these developers are doing anything bad. They want to make sure the project is set up for success. They want to avoid having to make big architectural changes in the future. They think, "Let's define the patterns now, so we don't have to refactor everything later. This impulse makes sense, but it often leads to a state of confusion. The team has a fancy design, but no one is quite sure what the real value of the software should be. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/03/DomainFirst.png) Technology First vs. Domain First ## Early Optimizations and Patterns If you have been part of such projects, you know the rush to define everything up front. There is a belief that we must have a perfect foundation. Teams say, “We need a hexagonal architecture because it will allow us to switch out external systems easily,” or “We should set up a microservices architecture so we can scale later.” These are not bad ideas by themselves. The problem is they take center stage before the real business needs are understood. In many projects, the team does not know how much data needs to be processed. They might not know how fast the system must run to meet user expectations. They have not spoken with actual users or stakeholders about their real problems. Without that information, choosing any design or architectural style is like guessing. It might work out, or it might turn into a structure that is too big or too small for the real project needs. ## The Risk of Losing Focus on the Domain When a software team jumps into patterns and technology first, they miss out on crucial understanding of the domain. The domain is the world in which your product will operate. It includes the users, business rules, regulations, and workflows. It is important to explore these details before deciding how to build the software. If you skip these conversations, you are left with a technical shell. (*That's what I observe most of the time.*) It may have the best code style, and it might be ready to integrate with any service you can think of. But you may end up with software that does not solve actual problems. You risk spending weeks or months on development, only to realize that what you built does not match the real business goals. At that point, changes can be costly, and the team morale might drop. Focusing on the domain first helps you see which features matter the most. It reveals which parts of the system are likely to change. It also uncovers critical details about data volumes and performance needs. With this knowledge, you can choose a suitable architecture. Maybe you do not need a complex structure for a small application. Or maybe you need something more robust. Unless you **learn the domain and become a domain expert** over time, you will never know. ## Why We Gravitate to Technology First Developers learn about new frameworks or architectures and want to use them right away. This is partly driven by curiosity and the desire to keep skills fresh. It is also fun to experiment with new techniques. Some teams might also fear that if they do not get the architecture right from the start, they will have to spend a lot of time refactoring. There is also an industry trend that pushes for best practices from day one. Many blogs and talks suggest certain patterns are “the only way” to do things. The problem with this industry is that it usually has no real connection to reality. It has often never done real projects or solved real problems. In reality, every project has different needs. You can follow a set of so called *best practices* and still fail if the product does not deliver what users want. On the flip side, many successful projects began with a simpler approach and adjusted over time, once they found their market or understood the domain better. Teams also feel pressure to show progress quickly. Stakeholders want to see something running. They might be less impressed by a series of conversations with users or subject matter experts. So the team jumps into coding. They set up a fancy architecture that looks nice in diagrams. This can impress some people initially, but if the final product is not aligned with real needs, it will not matter how pretty the code is. ### Balancing Technical and Domain Considerations It does not mean we ignore technology altogether. We need to pay attention to it at a certain point. The key is to balance the learning phase with the designing phase. Before locking in your framework or architecture, spend some time talking to the people who will use the software or benefit from it. Understand their pain points, how they do their jobs, and what they wish the new system could do for them. You can start by prototyping. Prototypes are small experiments that test assumptions. They are quick to build and throw away. During this stage, you might discover that some of your initial technical ideas are overkill. Or you might find that you actually do need certain optimizations because the data is bigger than you thought. Either way, your decisions will be driven by concrete findings, not just guesswork. In parallel, it is fine to keep patterns and practices in mind. You can set coding standards, choose a programming language, or decide on a framework that is common within your team. You might prefer certain database solutions if your team has expertise in them. The difference is you do not lock everything down before you understand the main problems. You stay open to changing the approach if new information comes in. ### Embracing Continuous Learning Software development is rarely a straight line. It is a path with twists and turns. Business requirements change. You learn new things about the domain that you did not know at the start. The teams should engage in this learning process and adapt their architecture and design accordingly. They do not see it as a failure to rework parts of the system. They see it as a natural part of building software that actually delivers value. If you pick a pattern like hexagonal architecture, that is fine. But do it because you have a specific reason that ties back to the domain or to the expected evolution of the system. For example, if you know that the application will need to integrate with several external services that might change over time, then hexaagonal architecture could be a good choice. But if you only have a single integration point that is unlikely to change, then a simpler approach will do the job. The same goes for performance optimizations. If you know the system will handle millions of records and require near real-time responses, then design for that. But if your data is much smaller, you can start simple and optimize later if you need to. Avoid the trap of adding complexity for problems **you do not actually have**. ### How to Shift Focus onto the Domain 1. **Ask questions early**: What problem are we solving? Who are the users? How will they benefit? Find out what kind of data you will be dealing with and how large it might be. If you do not have exact numbers, try to get a rough estimate. 2. **Talk to stakeholders**: This includes end users, managers, and anyone else who might be affected by the software. Ask about their workflows, their biggest hassles, and their wish list of features. 3. **Map out the workflow**: Draw a simple diagram of how the business process looks without the software. Then place the software in the process and see where it fits. This helps you understand the context before jumping into code. 4. **Start with small prototypes**: Build a quick version of a key feature to test an idea. Show it to real users. Gather feedback. This way you can confirm if you are on the right track before investing too heavily in architecture. 5. **Review and adapt**: As you learn more, adjust your design. If you realize a certain pattern is not needed, let it go. If you find out you need more flexibility, bring in a pattern that supports that. Keep an open mind. 6. **Make it simple when you can**: Complexity is not always a badge of honor. If a simple solution solves the problem, that is usually better. ### Conclusion When we focus on technology before the domain, we risk building something that does not solve real problems. We pour time into patterns and optimizations that might never be used. We run the danger of missing deadlines or wasting resources because the core business issues remain unclear. By taking the time to learn about the domain first, we create a clear path for our project. We see what features matter most. We gather real data about how much the system will grow or change. Then we choose technology patterns that fit those needs. This approach feels slower at the start, but it saves time and frustration in the long run. Remember that your goal is not just to write code. It is to solve a problem or deliver value. Knowing the domain and the willingness to become a domain expert is the best way to guide your technical decisions. This approach leads to software that people actually want to use. It also leads to more satisfied development teams, because their efforts are focused on building something that matters. By slowing down to understand the domain, you can move faster in the long run. *Cheers*! ### Why Code is the Wrong Layer for Rate Limiting URL: https://ricofritzsche.me/why-code-is-the-wrong-layer-for-rate-limiting/ Last updated: 2025-03-26T17:10:28.000Z Rate limiting controls how many requests your system will accept within a certain period. You might be familiar with “burst limits” or “throttling”. It’s all about protecting your app from overload. There are plenty of ways to do it, including in your code via middleware, libraries, or frameworks. But just because you *can* handle rate limiting in code doesn’t mean you *should*. ### The Appeal of Middleware-Based Rate Limiting In many frameworks (.NET Core is a good example) you can add a few lines of configuration in your Program or Startup file to activate a built-in rate-limiting feature. You define your policy: - PermitLimit (allowed requests), - Window (time period), - whether you queue or reject additional requests, - and which HTTP status code to send back when the limit is reached. It’s convenient, and it keeps everything in one place: your codebase. ## Why is this a bad thing? 1. **Operational Concerns Should Stay Out of Core Logic** Rate limiting isn’t really part of your business logic; it’s about overall system stability. When that logic lives in your service code, it risks cluttering your application’s responsibilities. If your system experiences heavy load, you want the infrastructure to handle the meltdown gracefully. It’s easier to reason about rate limiting when it’s separate from your application code. 2. **Unified Observability** Infrastructure-based rate limiting centralizes metrics and logs in one location. If each service has its own limiter, you’ll piece together scattered logs to understand how traffic flows and where bottlenecks form. A gateway or dedicated rate-limiting layer can give you a real-time snapshot of the entire system. 3. **Scalability and Reliability** At higher loads, in-process or “code-located” rate limiting solutions tend to suffer. Yes, you can distribute counters or use shared state, but that adds complexity. A dedicated layer or external tool can scale horizontally and is already optimized for concurrency. 4. **Consistency Across Tech Stacks** Odds are, .NET Core might not be the only platform in your environment. You might also run Node services, Java apps, or Azure functions. When you enforce rate limits at the infrastructure level, everything is governed by the same set of rules. That’s a huge plus for consistency. **Centralized Control Matters** If you manage rate limiting across multiple microservices or containers, doing so at the code level means repeating yourself or orchestrating lots of separate services. Changing or updating the rules requires new deployments in each service. That’s fragile and labor-intensive. Putting rate limits at the infrastructure level (e.g., API gateways, load balancers, or service meshes) allows you to update a single configuration and instantly apply it everywhere. ### Example: Fixed Window in ASP.NET Core Here’s an example of how you might enable a fixed window rate limiter in .NET Core: ```csharp services.AddRateLimiter(options => { options.AddFixedWindowLimiter("FixedPolicy", policy => { policy.PermitLimit = 10; // number of requests policy.Window = TimeSpan.FromSeconds(60); // time period policy.QueueLimit = 2; // how many to queue // etc. }); }); app.UseRateLimiter(); ``` It works fine on a single service basis, but once you have multiple instances or a cluster of services, you need to coordinate them with a shared backplane or more sophisticated distributed approach. And that’s exactly why external solutions often make more sense. ### When Code-Based Rate Limiting Might Be Okay - **Small-Scale Internal Tools:** If you run a simple app or internal tool with limited traffic, a quick in-code limiter might be enough. - **Prototyping/Demos:** If you’re just whipping up a proof of concept and need basic protection against bursts. - **Local Testing:** When you want to observe how your service reacts under load without setting up external rate limiting. Just understand that you’ll likely outgrow it as soon as your service evolves beyond trivial environments. ### The Right Way: Keep It Outside The way I recommend is that a single external mechanism such as an API gateway, a reverse proxy (e.g. Traefik, NGINX, Kong) or your orchestration layer handles all traffic and applies rate limits consistently. This means: 1. **One Central Configuration:** Update your rate-limiting rules in a single file or config store. 2. **Unified Monitoring:** Log and visualize everything from a central dashboard. 3. **Better Scalability:** Infrastructure solutions are built for concurrency. 4. **Less Code Bloat:** Keep your service focused on business logic. ### Final Thoughts Yes, you can do rate limiting in code, and yes, frameworks and libraries make it pretty easy. But that doesn’t change the fundamental mismatch: rate limiting is an **operational concern**, and code-based solutions create extra overhead when you really need a stable, scalable, and easily maintainable approach. If you’re small-scale or testing, then go for a built-in solution. But when your application is heading towards critical usage, it's better with your rate-limiting rules living outside the code. You'll avoid sleepless nights fighting distributed state, repetitive logic, and app-level logs that only tell half the story. Choose a gateway or orchestrator-level rate limiter and let your codebase do what it does best: deliver your business logic without getting bogged down in operational concerns. *Cheers*! ### Beyond the Hype: Rediscovering the Value of Relational Data Models URL: https://ricofritzsche.me/beyond-the-hype-rediscovering-the-value-of-relational-data-models/ Last updated: 2025-03-13T11:29:11.000Z The last few years have been busy scorning traditional relational databases and data models. But why, actually? Don't they have plenty of strengths? If you look at current trends, everyone seems to say, "*Use a single detached table, skip relations.*" Flat tables can be appealing because they're simple to read and fast to query. Command Query Responsibility Segregation (CQRS) is built on that idea too: the tradeoff is extra data copies for quick reads. I tried CQRS for the first time in 2013\. It felt like a superpower. But there was a price. All those event handlers and separate write models take time and energy to maintain. Of course, CQRS isn't the only pattern that nudges us away from a relational data model. People say, "Store data per use case." Sometimes that's a table in SQL, or maybe MongoDB, or even just a file in S3\. This is called polyglot persistence. But why do we jump in that direction? Because it's trendy? Then, before we notice, we've created a mess of mismatched data. Relational databases shine for a reason: consistency and integrity. We shouldn't ignore them. They bring real benefits. Interestingly, some large enterprises that initially embraced polyglot persistence or event sourcing have found themselves reverting to traditional relational models to reduce complexity and regain data consistency. For instance, The Guardian switched from MongoDB to PostgreSQL to simplify operations and cut costs, noting that managing Mongo clusters was more expensive and cumbersome than relying on a robust relational engine. Another example is Olery, a SaaS provider that retired MongoDB in favor of PostgreSQL after facing ongoing issues with performance and schema drift, discovering in the end that a relational schema with ACID transactions was far easier to maintain. Let’s make it more concrete. Imagine you manage an online store. You have a products table, a categories table, and maybe an orders table. If you're not careful with relations, you could delete a category but still have products pointing to it. You might miss an update to a product name in one place, yet keep the old name in another. That's inconsistent data. A proper relational model can stop that from happening by linking tables with foreign keys and enforcing referential integrity. It goes beyond just storing data. It protects the meaning of the data. Another reason consistency matters is when multiple processes try to change the same data at once. In a relational database, transactions help keep the system stable. They ensure you don't end up with half-updated rows or confusing race conditions. It sounds like a small detail until you realize that a mismatch can break your entire logic. You can avoid those headaches with solid constraints, properly designed relations, and the safety net of ACID transactions. ACID stands for Atomicity, Consistency, Isolation, Durability. These are key properties that ensure data integrity in relational systems. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/03/Screenshot-2025-03-12-at-17.33.31.png) Don't let the hype with all this buzzwords like polyglot persistence fool you into throwing out the strong foundation that relational databases provide. Consistency and integrity aren't buzzwords. They're insurance. They protect data from turning into chaos over time, and that adds real value. ## Referential Integrity Referential integrity is one of the key strengths of a relational database. By linking tables with foreign keys, we ensure that references between them always stay valid. Then come the cascades. They tell the database what to do if a parent record is changed or deleted. ON DELETE CASCADE might remove all child records automatically when the parent goes away, while ON DELETE RESTRICT stops you from removing a parent if children still exist. Sounds straightforward, but you have to set these rules with care. Imagine you have a users table linked to addresses. If you casually set DELETE CASCADE, you might wipe out addresses the second you remove a user. If that's exactly what you want, fine. But if it means losing valuable data in the process, it's a nightmare. On the flip side, RESTRICT could leave you stuck with a user you can’t delete because you forgot to handle old addresses. So it’s really about understanding your domain and choosing the appropriate option. Another point is that foreign keys are more than just references. They stop us from inserting nonsense, like an address that points to a user who doesn’t exist at all. That alone can save you a bunch of headaches when you realize how quickly data can spin out of control without these checks. The database effectively enforces your business rules for you, so you don’t need to write custom scripts or rely on application logic to keep everything in sync. That’s what makes relational databases so solid. They’re not just about storing rows and columns. They’re about ensuring consistency and integrity at the core. Modern solutions sometimes skip these constraints and move that logic elsewhere, but that often leads to confusion later. We should remember why foreign keys and cascades are there in the first place. They protect us from chaos and give us a stable platform for everything else we build. ## Consistency At its core, it means the data we store actually matches the data we expect. In a relational database, that’s usually guaranteed by ACID transactions. With them, you can start a transaction, make your changes, and either commit everything or roll it back if something goes wrong. No half-updated records. No race conditions that corrupt your data. That might sound obvious, but in systems without these safeguards, you can easily end up with incomplete or conflicting data. And if that happens, cleaning it up isn’t fun. Think of an online booking system. Two people can’t book the same seat on a plane. Without consistent transactions, it’s easy to imagine multiple processes updating the same seat at the same time. One might “win,” but then the other sneaks in a second later. Now you’ve got a seat double-booked, and a passenger who’s unhappy. With proper transaction isolation, one update waits for the other to finish. Either the seat is free, or it’s taken. Clear and consistent. It’s not just about concurrency, though. The database also helps you maintain consistent rules across the whole schema. You might forbid empty names or require certain fields to match patterns. These checks are part of consistency too. They ensure the data you store won’t break your application later. Yes, you can manage those rules on the application side, but having them baked into the database means fewer surprises if someone accidentally slips in a manual update or tries to bypass validations. All of that ties back to why a relational model is still so appealing. It’s not just about storing data. It’s about ensuring the data is **accurate and coherent,** even when lots of operations happen at once. That reliability is a big reason why so many core systems rely on relational databases, even when newer or more specialized tools appear. If we ignore consistency, we open the door to a world of confusing data states and manual fixes. A well-built relational schema provides a safety net that just makes sense. ## My Return to a Real Data Model I’ve decided to go back to using a relational data model in my large projects because storing and managing data may be technically separated from processes, but it’s still part of the domain. If I don’t treat that with respect, I’ll eventually pay for it when the data becomes inconsistent. Consistency, integrity, and solid performance are all benefits of a careful relational design. I prefer using CQS (Command Query Separation) as a more natural approach of splitting read and write operations without necessarily employing distinct data stores. Full-blown CQRS in contrast involves event sourcing and projections and separate databases (EventStore + Read Models). ```sql -- Create the `tenant` schema CREATE SCHEMA IF NOT EXISTS tenant; CREATE TABLE tenant.owners ( id UUID PRIMARY KEY NOT NULL, tenant_id UUID UNIQUE NOT NULL REFERENCES tenant.tenants(id) ON DELETE CASCADE, email VARCHAR(250) UNIQUE NOT NULL, external_user_id VARCHAR(250) NULL, created_at TIMESTAMP DEFAULT NOW() ); ALTER TABLE tenant.owners ENABLE ROW LEVEL SECURITY; ``` Practically speaking, I create SQL scripts and manage them from the application side using DbUp. It’s a simple, lean approach. No mystical layers that hide what’s going on. On top of that, I organize my code around self-contained features. I don’t rely on a central, massive data model that an ORM might impose. Instead, I use a SQL provider and Dapper for queries to keep things clear and efficient. Dapper helps turn raw SQL results into objects without too much hassle, but it doesn’t force a global model on me. ### Feature Slices and Clear Boundaries Each feature in my application has its own slice of the domain. That slice defines the data needed to handle a particular request or command. Maybe that’s a “request model” or a “command model.” Either way, the business rules live right inside the feature. This design keeps coherence high and dependencies low. I keep my SQL inside the command handler but separate out the actual database execution logic so I can test without tying everything to a live database. It’s straightforward, and it exposes the data model as part of the domain. Because let’s face it: without data, there wouldn’t be much of a system at all. SQL is expressive. What should be the reason to hide this? Here an example how a command handler can be easy to understand, lean and expressive. ```csharp public sealed class UpdateAssetCommandHandler(IDatabaseExecutor dbExecutor) : IRequestHandler> { public async Task> Handle(UpdateAssetCommand request, CancellationToken ct) { // Check if asset exists and belongs to tenant const string checkExistsSql = """ SELECT COUNT(1) FROM tracking.assets WHERE id = @Id AND tenant_id = @TenantId """; var existsResult = await dbExecutor.QueryAsync(checkExistsSql, new { request.Id, request.TenantId }); if (!existsResult.IsSuccess) return Result.Failure(existsResult.Errors); if (existsResult.Value == 0) return Result.Failure(["Asset not found"]); // Check name uniqueness const string checkNameSql = """ SELECT COUNT(1) FROM tracking.assets WHERE tenant_id = @TenantId AND name = @Name AND id != @Id AND is_retired = false """; var nameResult = await dbExecutor.QueryAsync(checkNameSql, new { request.TenantId, request.Name, request.Id }); if (!nameResult.IsSuccess) return Result.Failure(nameResult.Errors); if (nameResult.Value > 0) return Result.Failure(["Asset name already in use by another active asset"]); // Update asset const string updateSql = """ UPDATE tracking.assets SET name = @Name, description = @Description, attributes = @Attributes::jsonb, updated_at = NOW() WHERE id = @Id AND tenant_id = @TenantId RETURNING id, name, description, attributes """; var updateResult = await dbExecutor.QueryAsync( updateSql, new { request.Id, request.TenantId, request.Name, request.Description, Attributes = request.Attributes != null ? JsonSerializer.Serialize(request.Attributes) : null } ); return updateResult is { IsSuccess: true, Value: not null } ? Result.Success(updateResult.Value) : Result.Failure(updateResult.Errors); } } ``` On the query side, I do something similar. Each query feature has its own representation of the data. I don’t need a shared, global model. I only fetch what I require (maybe columnA and columnB from tableA) to build a User object that fits the current scenario. In another query, a User might look different because I only need some of its fields. That keeps each feature tight, expressive, and free of clutter. ```csharp public sealed record User(Guid Id, string Name); // ... string sql = "SELECT id, name FROM users"; var results = await conn.QueryAsync(sql); ``` This avoids things like DTO suffixes or strange artificial class names to distinguish different types in a large model we had in earlier years, like UserInfo to indicate its not the full user model and so on. ## When Polyglot Persistence Backfires: A Real-World Example A good example of why consistency matters comes from Olery, a hospitality analytics startup. They initially embraced polyglot persistence by mixing MySQL for transactional data with MongoDB for large, unstructured review data. Over time, however, they found themselves facing severe performance issues and unpredictable lock-ups. Schema drift (IMO a natural consequence of MongoDB’s flexibility) eventually forced them into constant schema checks within their application code, making data consistency fragile and difficult to manage. After significant struggles with debugging and operational complexity, they decided to consolidate everything into PostgreSQL. The transition was challenging, involving careful schema redesign and data migration, but the benefits were immediate: dramatically improved performance, reduced complexity, and consistent data enforced at the database level. Olery's experience is a strong reminder that trendy approaches might seem appealing initially, but over time, the clarity and stability of a well-structured relational model often proves more valuable. ## CQRS Isn't a Free Lunch CQRS isn’t inherently good or bad. It’s just a tool. Even Martin Fowler, who helped popularize it, warns against using CQRS everywhere. According to Fowler, introducing CQRS into a domain that doesn’t specifically require it "adds complexity, reducing productivity and increasing risk." That aligns perfectly with my own view: CQRS is helpful when you genuinely need separate read and write models for scale or complexity, but in most cases, I rely on relational databases for their consistency, integrity, and simplicity. I still enjoy CQRS for carefully chosen scenarios, but it’s always about keeping complexity manageable. My approach is straightforward: a well-designed relational model provides consistency, integrity, and clear data management, reducing the need for complex data synchronization or duplicated logic. ## Conclusion Relational databases sometimes get criticized for requiring upfront schema design, but that's also their strength. Polyglot persistence or complex event-driven architectures often introduce hidden complexity, something teams like The Guardian and Olery found out firsthand. Both companies initially adopted MongoDB for flexibility, but later reverted to relational databases like PostgreSQL due to operational simplicity and schema integrity. Similarly, DNA Technology stepped back from event sourcing to a relational database to significantly reduce complexity. This reminds us that while new techniques promise flexibility, traditional relational models often deliver greater simplicity and robustness over time, especially when maintaining the integrity and consistency of core business data. *Cheers*! ### Cutting Through the Noise: A Reflection on the True Essentials of Software Development URL: https://ricofritzsche.me/cutting-through-the-noise-a-reflection-on-the-true-essentials-of-software-development/ Last updated: 2025-03-07T18:30:23.000Z Over the years, I’ve seen countless frameworks, patterns, best practices, and methodologies trend, peak, and fade away. Each claimed to be the definitive solution to our biggest challenges. But the real needs of software development have remained constant. At its core, software development isn’t about chasing the latest “must-have” technology. It’s about understanding the domain, delivering business value, and modeling solutions clearly and simply. This article explores how our industry’s obsession with hype can distract us from the real goal of solving problems effectively, and how I’ve come to appreciate simpler, more domain-focused approaches. ## Separating Hype from Reality Despite the continuous influx of new frameworks, architectural styles, and “revolutionary” methodologies, the underlying mission of software development stays the same: solve real problems. While some new tools do deliver genuine efficiency or expressiveness, too many are driven by: **Buzzwords**: Vague or superficial terms promising silver bullets. **Book Sellers**: Experts, gurus, or influencers pushing a trademarked approach or methodology. Academies offering overpriced training courses and useless certificates. **Fear of Missing Out**: The nagging worry that if you aren’t using this or that, you’ll be left behind. This cycle can distract entire teams from focusing on value-driven and domain-oriented solutions. It’s essential to separate the genuinely useful from the merely fashionable. ## Bounded Contexts: Clarity through Limits I once went all in on Domain-Driven Design (DDD). I believed every project needed rich entities, value objects, and repositories. Over time, I realized that the real power of DDD is not in its tactical patterns. The strategic side is what keeps projects aligned with business goals. It sets the boundaries between services or modules and gives everyone a shared language. That shared understanding prevents confusion and wasted effort. That big-picture thinking keeps teams from mixing unrelated concerns and stops them from building “just-in-case” structures. Repositories, for instance, introduced extra layers for data access, so even simple queries involved multiple abstractions. Factories buried creation logic behind another layer that did not always match the real business needs. Instead of making the domain clearer, these patterns created a maze of indirection. The main goal is to stay true to the business problem and avoid turning every detail into a tactical exercise. ## Rethinking Rich Domain Models I used to hate anemic domain models. I wanted every entity to carry all possible business rules, believing that would ensure “smart” code. In practice, these over-stuffed models turned into a liability. One small change triggered a chain reaction across many classes. Instead of moving forward with new features, teams often found themselves untangling side effects rather than delivering value. What I learned is that adding business logic to domain objects can be useful, but only when it truly expresses the domain. If it becomes a dumping ground for every rule or function, it muddles the flow of data and makes updates painful. ### How Data Flows Through a System In my opinion, a healthier approach is to focus on how data moves through each step of the system rather than stuffing all business logic and data into a single, bloated domain entity. 1. **Request Data** – A client sends a message (command or a query). 2. **Processing** – The server applies business rules, reads or updates the database, and prepares the output. 3. **Response Data** – The server returns a response data to the client, this can be a representation of the data as a result of query or the processing status and minimal information in case of a command. Each step gets its own data object, tailored to what’s happening. No need to force a so called rich model to handle everything, or settle for anemic shells with logic scattered elsewhere. Instead, a flow or message-driven approach creates distinct data objects for each stage, which keeps code understandable and aligned to what the system is actually doing at each moment. Say you’re registering a user: - **Request Data**: A *RegistrationRequest* record with email and password. - **Processing**: Check the email’s unique, hash the password, save the user. - **Response Data**: A *RegistrationResponse* record with a success note and the user’s ID. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/03/Processing-Flow.png) Data Flow Example For instance, in C# or Java, I prefer using records instead of classes for these step-specific data structures. Records make immutability straightforward. Once they are instantiated, their values do not change. This immutability reduces side effects and bugs. It also clarifies how data flows: when a request comes in, you create a new record to capture the data; you process or transform that record (maybe returning a new record if something changes); you send back a response record when everything is done. #### Rich vs. Anemic vs. Flow-Driven - **Rich Domain Models** try to put most business rules and methods inside the same objects that store the data. This can work in some cases, but it risks over-complication if you cram every bit of logic into a single “catch-all” class. - **Anemic Domain Models** do the opposite. They store data in bare-bones classes or structs, with almost no domain logic. These are easy to read but scatter business rules across the codebase or push them into large service classes. Data classes have public setters so that everyone can change data from the outside without business rules etc. - **Flow-Driven Models** split data into focused objects or messages that reflect each step in the pipeline. Business logic and transformations still have their place, but they're attached at the relevant step, rather than bloating your domain objects. Each part of the system deals with data in the form that makes sense for that stage. It’s about clarity: a command kicks things off, data shifts as needed, and the user gets what they asked for. ### Event-Driven Doesn’t Mean Event-Sourcing When moving beyond simple request/response scenarios, an event-driven approach can help. You might model events that describe important state changes or activities, then publish those events to other parts of the system. This makes it easier to compose features from smaller, independent services or modules. But being event-driven does not mean you must use event sourcing. You can store state in a database with standard tables, enforce transactions for consistency, and still notify other parts of your system when changes occur. You do not have to jump on the append-only event store hype if it does not fit your needs. By the way, it's just a new form of just-in-case culture to argue that you should have all events because you might need them at some point. It's similar to arguing that you need repositories because you might need to replace the underlying database technology at some point. ### Balanced Design: Expressive, Not Excessive A rich domain model can help when it reflects genuine domain concepts, but many projects make these models far too big. Not every rule belongs inside an entity. Not every operation needs its own class or factory. If you treat data flow as a series of transformations, you keep each step clear: a command triggers logic, data is updated or retrieved, and the user gets a response that matches what they asked for. You can attach business logic where it is relevant in a dedicated building block rather than swelling your entities into monsters. In the end, a balanced approach is best. Keep your models expressive but not bloated. Represent data in the forms that make sense at each stage. Use events to notify or trigger additional processing if that adds clarity. Save your data in a consistent way that is easy to manage, without locking yourself into a pattern just because it sounds advanced. The point is to deliver value, not to build the most complicated domain model possible. If a smaller, more focused model solves the problem, that is the right choice. ## Stop Hiding SQL: Why Transparency Matters Hiding SQL behind Object-Relational Mappers (ORMs) has long been popular, and admittedly, there are scenarios where ORMs are beneficial. They can help teams quickly spin up small prototypes or simplify basic CRUD-heavy applications, especially where performance and query complexity are not primary concerns. However, in my experience, ORMs often cause more trouble than they're worth in larger or more performance-sensitive applications. Initially, ORMs seemed convenient, automating tedious tasks like mapping queries and managing object persistence. But hidden queries soon turned into debugging nightmares, obscuring performance issues behind layers of abstraction. SQL itself is a powerful, expressive, and universally understood language. Keeping queries visible fosters transparency, aiding optimization, collaboration, and troubleshooting. Direct SQL enables easier performance tuning, clearer debugging, and faster iteration. Yet, there are scenarios where ORMs remain practical: - **Rapid Prototyping:** Quickly bootstrapping applications when time-to-market matters more than long-term optimization. - **Simple CRUD Applications:** Straightforward apps with minimal business logic where ORM’s automation is genuinely helpful rather than hindering clarity. The key is mindful choice: use ORMs when their benefits clearly outweigh complexity, but embrace direct SQL to maintain control, transparency, and performance where it counts most. Here an example how it makes code clear, expressive and simple: ```csharp public sealed class ViewAssetsQueryHandler(IDatabaseExecutor dbExecutor) : IRequestHandler>> { public async Task>> Handle(ViewAssetsQuery request, CancellationToken ct) { const string sql = """ SELECT a.id, a.name, a.description, a.attributes, da.device_id, a.is_retired FROM tracking.assets AS a LEFT JOIN tracking.device_assignments AS da ON a.id = da.asset_id AND a.tenant_id = da.tenant_id AND da.end_time IS NULL WHERE a.tenant_id = @TenantId AND (@IncludeRetired OR is_retired = false) """; var result = await dbExecutor.QueryMultipleAsync( sql, new { request.TenantId, request.IncludeRetired } ); return result; } } ``` #### Balancing SQL and Business Logic Some teams push business logic into the database through stored procedures or triggers. Others try to keep everything in application code. A direct SQL approach does not force either extreme. You can store data with well-crafted queries while still keeping domain rules in code. If a rule involves complex calculations or cross-service interactions, it probably belongs in dedicated building block. If it is a matter of simple validations, you can enforce them in the database if that adds clarity or efficiency. #### Why SQL Beats ORM Complexity Embracing SQL fosters collaboration. Many developers, even those who do not specialize in databases, can read and understand SQL. Debugging a query is much easier when you see the actual statement. When the actual SQL is transparent, debugging is streamlined. You can directly execute queries, examine execution plans, optimize indexing strategies, and quickly identify performance bottlenecks. By keeping SQL visible and under your control, you avoid the black-box effect that heavy ORMs introduce. You gain confidence in how your system manages data, and you free yourself from the hidden complexities that lurk in generated queries. ## When Layers Become Obstacles Clean Architecture, Hexagonal Architecture, and Onion Architecture all promise clear separation of concerns. They aim to organize code into layers so that business logic stays independent from infrastructure details. That sounds good in theory, but I have seen these architectures create: - **Extra Layers of Indirection**: Every new layer requires its own interfaces, modules, or adapters. Simple features end up scattered across multiple files. - **Slower Onboarding**: New developers must learn the architecture’s rules before they can write a line of real code. This adds friction and can lead to confusion. - **Reduced Velocity**: Strict layering means each change can trigger edits in multiple layers or modules. Even small adjustments involve shuffling data across boundaries. The motivation behind these patterns is understandable. But in practice, the overhead can outweigh the benefits. Many teams apply these formal structures to every project big or small only to discover that they slow down delivery. They end up with complex abstractions that do not always map to real business needs. All those extra layers do not guarantee better code. In many real-world projects, I have seen them lead to more complexity, not less. ## The Trap of Over-Abstraction: Rethinking DRY “Don’t Repeat Yourself” is a longstanding principle that aims to reduce code duplication. I used to chase duplication everywhere. If I saw the same line of code in two places, I wanted to merge it. That seemed smart at first, but it introduced new problems: - **Tight Coupling**: A shared utility or class that lumps together logic from different contexts ties those parts of the codebase together. A small update in one part might break another. - **Reduced Clarity**: Sometimes repeating a few lines in separate modules is clearer. A generalized function might require mental gymnastics to figure out what it does in each scenario. - **False Economy**: DRY for the sake of DRY wastes time. A tiny update for one case might break another, forcing more fixes down the line. I learned to balance DRY with common sense. Duplication can be bad, but forced abstraction can be worse. If extracting common code makes a module harder to understand or maintain, it is not worth it. Repetition is not a crime if it keeps the system simpler and helps each part evolve on its own. ## Conclusion: Design for Clarity, Not Trends After three decades in software development, one truth stands clear: effective software solutions prioritize simplicity, clarity, and alignment with genuine business needs. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2025/03/Screenshot-2025-03-07-at-19.18.34.png) Trends and frameworks come and go, but fundamental principles remain unchanged: - **Favor explicitness over unnecessary abstraction.** Clarity in code and architecture reduces complexity and maintenance costs. - **Align architecture with actual business needs.** Strategic context trumps theoretical purity. - **Optimize for maintainability and readability first.** Elegant code is valuable only if it can be clearly understood and efficiently maintained. - **Embrace SQL for its transparency and power.** Visibility into your data operations enhances debugging, performance, and team collaboration. - **Use abstraction judiciously.** Simpler solutions that occasionally accept duplication can be superior to rigidly forced abstractions. Ultimately, staying grounded in practical realities, rather than chasing the latest buzzword or trendy methodology, ensures that software remains maintainable, adaptable, and truly valuable. *Cheers*! ### Scalability in High-Traffic Environments URL: https://ricofritzsche.me/scalability-in-high-traffic-environments/ Last updated: 2024-12-16T15:30:54.000Z This year I started building a [Geofencing API](https://www.inuveon.com/?ref=ricofritzsche.me) from scratch. Let me share the key insights from this journey. I've learned that scalability isn't just a technical requirement—it's the cornerstone of everything I'm working toward. My venture enables businesses to leverage location data effectively, whether they're tracking fleets, managing assets, or enhancing customer experiences. Every decision—from architecture to workflows to priorities—is shaped by the challenge of handling real-time location data at scale while maintaining reliability and cost-efficiency. Scalability is often dismissed as just another buzzword. But in real-time location-based intelligence, it's an absolute necessity. The challenge extends beyond handling more users or devices, it's about maintaining consistent, reliable performance while processing streams of location data, potentially millions of updates, without missing a beat. This post explores the lessons and ideas I've developed along the way. ## The Nature of the Challenge Consider what happens when hundreds of devices send location updates in bursts. This isn't a typical request-response scenario - it's a flood of small but continuous data points that need processing as events. Some updates trigger immediate actions, like geofence notifications, while others are logged for historical analysis. The system must handle this complexity gracefully, not just during steady traffic but also during sudden spikes like Black Friday for retail or a city marathon for urban planners. The biggest challenge isn't just the volume. It's the **unpredictability**. Peaks in location data don't follow a predictable curve. They can surge unexpectedly and often come with demanding latency requirements. ## Concepts for Scalable Solutions When approaching scalability for location-based intelligence, don't think of it as a "single system that scales," but rather as an **ecosystem of specialized components**, each handling a specific piece of the puzzle. Here are the key concepts: ### Separation of Concerns In high-traffic scenarios, you don't want a single system doing everything. Ingesting location updates, running geofence evaluations, and sending notifications should all be decoupled. This creates failure isolation—when one part is overwhelmed, it doesn't drag the entire system down. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2024/12/High-Level-Architecture-for-Scalability-1.png) This visualization shows how the system separates concerns for scalability. ### Event-Driven Thinking Location updates are events. Treating them as such simplifies the flow. The system doesn't process everything synchronously. Instead, updates are queued, prioritized, and distributed across workers that process them at their pace, ensuring consistent throughput. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2024/12/Event-Driven-Workflow.png) Illustrates how events flow asynchronously in a scalable, decoupled system. ### Temporary vs. Persistent Data Not every piece of location data needs permanent storage. Many updates are only relevant in real time. This means using fast, in-memory systems for transient data and offloading historical storage to slower, cost-effective options. ### Prioritization of Workloads Not all data is equal. A geofence trigger requiring notification is urgent. But logging a location update for analytics can wait. Prioritization ensures critical tasks happen regardless of traffic. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2024/12/Workload-Prioritization.png) Shows how different types of workloads are prioritized in the system. ## Balancing Cost with Performance Scaling isn't just a technical challenge - it's a cost challenge too. Throwing more resources at the problem can solve scalability temporarily, but it's rarely sustainable. Designing for cost-effectiveness becomes critical when building for high-traffic environments. For instance: - Using asynchronous processing wherever possible to avoid tying up resources unnecessarily. - Storing only what you need in high-performance systems while archiving less-critical data elsewhere. - Leveraging serverless architectures (Azure Functions, AWS Lambda) for unpredictable workloads, where you pay only for what you use. ### Closing Thoughts Scalability is not a destination; it's a constant process of refinement. As traffic grows, new bottlenecks emerge. The key is designing systems that are flexible enough to adapt, fail gracefully, and grow incrementally. In high-traffic location intelligence, it's not just about doing things fast—it's about doing the **right things fast**. This starts with asking the right questions: What's essential now? What can wait? What can break without everything falling apart? Scalability isn't about chasing the perfect system; it's about building one that works well today and can evolve to meet tomorrow's challenges. *Cheers*! ### My Journey to Simple, Lovable, Complete Products URL: https://ricofritzsche.me/my-journey-to-simple-lovable-complete-products/ Last updated: 2024-06-27T18:54:03.000Z Over the past few weeks, I have been studying and learning a lot about the Minimum Viable Product (MVP) approach. In my 30-year career as a software engineer, I have undoubtedly experienced a lot, and my own standard continues to be that I am always learning. Throughout those years, I often found myself in situations where I had to build the key features of a product, either to convince customers of my (or my company’s) capabilities, or to help founders with a minimal version of their product idea to find investors. It often didn’t matter what we called it, because there was usually a consensus that this first version of the product had to be compelling. ### **Rejecting the ‘Quick and Dirty’ Approach** As I began to research the topic of building MVPs as a product for entrepreneurs, founders, or even existing companies, I noticed that there are quite a few different views on what exactly an MVP should be. Of course, there are always people who think everything is clear, who don’t listen to what others have to say, and who consider their interpretation of the matter to be the only truth. But I am not one of those people. I need the exchange with others, and as a self-taught individual, I also need time for self-study to form an opinion and my definition. Sustainability and quality in the development of a software product are absolutely essential to me. That’s why the idea of “just throw something together quickly and dirty as long as it seems to work” doesn’t appeal to me. I am too much of an aesthete to just throw some input fields somewhere and let the user find the submit button. No, it doesn’t work that way. This was clear to me, and in all my years of software development, a good architecture and an appealing UI have always been part of the game. Although the common opinion seems to be that an MVP can be hastily assembled and is only meant to show that the product or use case works, I have a different view. To prove that something works technically, there are concepts like Proof of Concept (PoC) or prototypes. But we’re talking about MVPs that have to convince real users. ### **Low-Code and No-Code Solutions: A Critical View** I have noticed a large faction relying on no-code and low-code solutions. While they offer an easy entry point, I am convinced that they are neither sustainable nor solid. The most important reason why they cannot be a long-term solution is simply the dependency on a vendor. The product is not really your product but, to put it bluntly, part of a subscription system. In addition, all standardized modular solutions have inflexible and limited customization options. Even if the advantage is that no programming knowledge is required for the implementation and there are usually many tutorials and templates available, the disadvantages described above are too serious to speak of sustainability in terms of risks such as costs and scalability due to the hard dependency on the provider. I am convinced from experience that you need real substance to get a solid product or project off the ground. ### **Learning from ‘Lean Startup’** What exactly is an MVP? Looking at Eric Ries’ definition from “Lean Startup”, an MVP is “the version of a new product that enables a team to collect the maximum amount of validated knowledge about customers with the least amount of effort”. I think this definition is very generic. What it does not say is how exactly this works! The MVP concept usually involves creating a small product that can be tested easily and inexpensively. You bring the product to market quickly and then learn from customer experience and feedback. If the product is not successful, you abandon it, but if it has potential, you invest more. MVP development is iterative and relies on a stable team. ### **Defining ‘Minimum Viable’** “Minimum viable” can mean just about anything, depending on the perspective of those involved. Yes, it can be, as I prefer, the one core feature. But what does that look like? And when is a product viable? When it just works or is functional? In the manner shown below, a product is also minimum viable. ![](https://cdn-images-1.medium.com/max/1600/1*AD7qSVfU4zdsSIMSihYo-Q.png) A form of “minimum viable” That may be a bit of an exaggeration, I know, but it fits the definition. I hope there are no more users out there who would expect this. But who knows? In my experience, although the MVP concept offers a fast approach to product development and marketing, the “viability” aspect is often not given enough attention. The focus is very much on the “minimum”, while the “viability” of the product is overlooked, which can result in many start-ups losing the trust of their potential customers. ### Focus on Core Features This made me think about my approach and, in retrospect, about the values I have always held in developing software. The core of my thinking, whether at the product level or at the coding level, is to avoid unnecessary complexity whenever possible. Simply said: Keep it simple! Simplicity means not overloading and offering things with little or no value. I believe in keeping it simple, focusing on the essentials, and giving the user a great user experience. By that I mean a few features, but they should be finished and not half-baked. The application should not give the impression that the user is the alpha tester. The features that are there have to be finished, which does not mean that it has to be a complete application with all features, but a useful application with the critical feature or features of the highest quality. This includes a very good user experience. Earlier this year, I developed the MVP for a New York-based founder in the Web3 space. The core feature, the cross-chain swap from Bitcoin to Ethereum, was carefully worked out and turned into an iOS mobile app with a focus on the core idea. The app doesn’t look unloved and unfinished. It gives the user the feeling of a great product. ![](https://cdn-images-1.medium.com/max/1200/1*mU29YkpzjqxzX9tqnQPCEQ.png) ![](https://cdn-images-1.medium.com/max/1200/1*tzPjtT5ORLYzVqLVPDdOcg.png) Screenshots from mobile app MVP I built earlier this year. ### **The Evolution to SLC Products** Recently, a friend sent me a LinkedIn article about the SLC approach to product development that really caught my attention. **The next step in realizing MVPs is Simple, Lovable, Complete (SLC).** The “minimum” needs to be replaced with “simple”. Features should not be a matter of quantity, but should be a matter of value to the user. The product must be fun to use, even if it has only a few features to begin with. It is often said that a “viable” product must be functional, but not necessarily “lovable”. A “lovable” product is one that customers want to use. The “lovable” principle of the SLC approach requires a redefinition of purpose. While the “viable” principle of the MVP focuses on the needs of customers, the “lovable” principle of the SLC aims to develop what customers really want and to design it in such a way that they enjoy using it or are enthusiastic about it. Enthusiasm ultimately leads to commercial success. This automatically makes me think of Apple, which has always scored with simplicity. ### **SLC: A Step Beyond MVP** The difference between MVP and SLC lies mainly in the concepts of “complete” and “product”. While MVP aims to create a working product that solves basic customer problems and has the necessary features to survive and be accepted, the “complete” element in the SLC concept focuses on the product consistently performing a specific task without appearing to be missing anything. ![](https://cdn-images-1.medium.com/max/1600/1*APmM7eSxin8mzQEZI_jFdw.png) SLC: Value over Feature Quantity. I love this approach. It is exactly my idea of a good and useful product. It’s the way I think and work. Please don’t get me wrong, the MVP is not a bad method for product development and marketing, if applied correctly. As I understand it, the SLC approach aims in the same direction, and I see it as a new level, with a stronger focus on usefulness through simplicity. At the end of the day, it’s about companies creating products that deliver real value to customers in a way that they want to use and enjoy every time they use them. And this is the gap that the “ Simple, Lovable and Complete” approach fills. *Cheers*! This article was originally published on Medium on November 29, 2023: [https://levelup.gitconnected.com/my-journey-to-simple-lovable-complete-products-9583d35bc863](https://levelup.gitconnected.com/my-journey-to-simple-lovable-complete-products-9583d35bc863?ref=ricofritzsche.me) ### Lean Architecture with .NET Core and C# URL: https://ricofritzsche.me/lean-architecture-with-net-core-and-csharp/ Last updated: 2025-05-03T10:05:19.000Z In this article, I want to show how easy it is to keep the domain free of technical concerns. In my past articles, I've written about distinguishing between data models, persistence, and domain models. One reasonable question I often hear is whether this might lead to code duplications, having similar models in the data and the domain model. And that’s true. Often, there are no significant differences between the data structures of these two models in a system. In the past, I tended to have a pure domain model without any annotations or knowledge in the core of my application and a separate data model in the infrastructure layer, mapping the data in both directions. But to be honest, this is not lean and always feels over-engineered. When there is no strong justification for this, I think it’s best not to do it. There is a much better way to keep the code lean and clean while keeping the concerns separated. ## The Importance of Business Value While working on my new Location Services API [venture](https://inuveon.com/?ref=ricofritzsche.me) over the past few weeks, I recognized that it is crucial to create solutions that are robust, secure, and scalable, but foremost useful. Anything we do in code must have a business value. Otherwise, it’s wasted time. This is what I often see in development teams: there is no good balance between what is needed to create value for users and the company and what is done just to satisfy patterns and use the latest frameworks. We should find a good balance and not blindly follow highly promoted architecture approaches and patterns without fully understanding the value of the product and domain. ## My Approach to Lean Code Let me tell you more about my motivations and the approach I chose to produce value immediately while keeping the code lean and clean, maintainable, and easy to understand. In recent months, it has become more important to me to keep together things that are needed to fulfill a specific task. That’s why I prefer to think in feature slices, meaning to cut the code into small functional units vertically. For example, having a feature like *Register Tenant* is such a slice. Everything needed to register a new tenant must be located in this feature slice. It doesn’t matter if it’s just an API endpoint or a view or both; it is part of the interaction between the consumer and the machine. Another slice or task that the system must fulfill could be a feature slice like *Get Tenant By Id*, which is a read-only operation, while the first example, *Register Tenant*, is changing something. ### Command Query Separation (CQS) By doing this, we automatically keep commands separated from queries, meaning we apply CQS (Command Query Separation). Following the vertical slicing approach is a concrete method to apply CQS. This idea was first put forward by Bertrand Meyer in his 1988 book "Object-Oriented Software Construction". Meyer's big point was pretty straightforward: when you're coding, make sure a function either changes something or returns some information, but it shouldn't do both. CQS brings some clear benefits by introducing Separation of Concerns (SoC). This means that we keep the code that reads data and the code that changes data completely separate. Such a separation, right there in the code, naturally leads to a cleaner organization, whether we're talking about organizing methods or classes. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2024/04/CQS-Principle.png) Command Query Separation Principle When all the code is kept close together, we achieve high cohesion inside the slice but decoupled from each other. ### Benefits of Vertical Slicing This is a fantastic way to produce easy-to-extend code, where each feature encapsulates everything needed to fulfill the task instead of having the code spread over different layers like traditional horizontal layer approaches such as Clean Architecture and Hexagonal Architecture promote. I used these layered architecture approaches for many years, always starting with good intentions, but they often ended up too complex. Thinking in horizontal layers often leads to “god” service classes such as a typical *Tenant Service* that accidentally contain both commands and queries. ## Avoiding Duplicated Code and Maintaining Consistency The most interesting question with vertical slicing is how to avoid duplicated code and how to deal with domain consistency and data models without ending up in a layered architecture. We still need a reasonable separation of concerns but must find a way to keep the incredible benefits of the vertical slice approach, allowing us to include only what’s needed to fulfill the task within a slice. ### Practical Example I always try to think technology-agnostic. I have used different programming languages and technologies in my career and learned to have different views. What I like about *Spring Boot*, for example, is the idea of packages that ensure independent modules within the system when used correctly. A package can be a bounded context in terms of Domain-Driven Design, defining a clear boundary of a specific module of a particular domain. In .NET solutions, a bounded context is technically a class library, aka a .NET project. It’s an independent, viable package. Since this article is not about structuring different bounded contexts in a solution or making them deployable as (modular) monoliths or microservices, I won’t discuss this topic further here, but in one of my next articles. ### Structuring Code Inside a Package The core issue of this article is how the code could be structured inside a package, aka bounded context, avoiding unnecessary layers and abstractions while keeping clean concerns separated. In most applications, we need a place where the domain logic is located, meaning where we cluster the bounded context into aggregates as modules ensuring consistency. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2024/05/Screenshot-2024-05-28-at-15.23.45.png) The organization of a project that reflects the Bounded Context "Identity Access Management" In the figure above we see a typical structure that I prefer to organize the code in a very clear way. I will discuss the internal structure of the features in another episode of this series. The fact that the domain model contains the structure but has no knowledge of how it is persisted is important for today's discussion. Using an Event Store and Event Sourcing for write operations, along with projections for read operations, is also an option. This setup ensures that the read model event handler is notified about changes to an aggregate, keeping everything in sync. While this approach provides a clear separation of concerns and effectively implements Command Query Responsibility Segregation (CQRS), it also introduces additional complexity. Although I appreciate the benefits of CQRS and Event Sourcing, they do not add significant value to my API venture. ### Keeping It Simple Since I don't need history or to repeat events, many of my requirements are related to creating new data or updating existing data. My main goal is to keep the code simple, easy to understand, and separate concerns. I want to avoid duplicating data structures of my aggregates in data models, ending up in mappings. Some might suggest using mapper libraries, but for me, it’s just another dependency to manage. It’s better to avoid it when not needed. Separation of domain and persistence concerns is quite simple in .NET using the Entity Framework without using attributes as most commonly used. I have identified three technical concerns I want to separate: the domain itself, vertical sliced features, and underlying data structures. The domain should be clean from any technical aspects but contain logic and rules, clustered as aggregates. The vertical sliced features could be commands (changing things) or queries (reading), following the same interaction patterns: request => request processing => response as shown in the following example. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2024/05/Register-Tenant-Vertical-Slice.png) Register Tenant does exactly one thing. A slice should contain anything needed to process a request and give the response. So lets have a look in one of my aggregate root classes which contains the data structure of the Tenant but also validations, rules and logic. ```csharp public class Tenant : BaseEntity, IAggregateRoot { protected Tenant() { } public string CompanyName { get; init; } public string? Industry { get; init; } public string CompanyEmail { get; init; } private readonly List _apiKeys = new(); public IReadOnlyCollection ApiKeys => _apiKeys.AsReadOnly(); public void AddApiKey(TimeSpan expiration) { var apiKey = ApiKey.Create(this.Id, expiration); _apiKeys.Add(apiKey); } public static Tenant Create(string name, string email) { var tenant = new Tenant { Id = Guid.NewGuid(), CompanyName = name, CompanyEmail = email }; tenant.Validate(); return tenant; } } ``` As you can see, there is no trace of persistence. It is pure C# code that maps the domain concerns. Of course, we want to persist the state without enriching the domain model with knowledge about database issues. This is easily possible in .NET with the help of the EF Core Framework. But how to do it? To keep the domain model clean and free of persistence concerns, we use the *EntityTypeBuilder* to configure the mappings for our entities. This approach ensures that our domain models remain focused on the business logic, while the infrastructure layer handles the database-specific details. In the */Infrastructure/Persistence/Configuration* folder, I use the *EntityTypeBuilder* of the EF Core Framework to describe the persistence configuration. Here is an example: ```csharp public class TenantConfiguration : IEntityTypeConfiguration { public void Configure(EntityTypeBuilder builder) { builder.ToTable("tenants", "identity"); builder.HasKey(t => t.Id); builder.Property(t => t.Id) .HasColumnName("id") .IsRequired() .ValueGeneratedNever(); builder.Property(t => t.Industry) .HasColumnName("industry") .HasMaxLength(100); builder.Property(t => t.CompanyName) .HasColumnName("company_name") .IsRequired() .HasMaxLength(100); builder.Property(t => t.CompanyEmail) .HasColumnName("company_email") .IsRequired() .HasMaxLength(100); builder.Property(t => t.CreatedAt) .HasColumnName("created_at") .IsRequired(); builder.Property(t => t.UpdatedAt) .HasColumnName("updated_at"); } } ``` In this configuration, I define how the *Tenant* entity maps to the database schema. The *ToTable* method specifies the table name and schema. The *HasKey* method sets the primary key. Each *Property* method maps a domain property to a database column with its specific attributes, such as *IsRequired*, *HasColumnName*, and *HasMaxLength*. By using the *EntityTypeBuilder*, I keep the domain model free from persistence concerns, maintaining a clean separation of concerns. This way, my domain models remain pure, focusing solely on business logic while EF Core handles the database mapping in the infrastructure layer. This approach helps maintain a lean and clean codebase. ### Wiring Up EntityBuilder Configurations in the DbContext Here's how to wire up the entity configurations like *TenantConfiguration* in the *DbContext* implementation. We start by defining our *DbContext* class, where we specify the database sets and apply the configurations. Below is an example of how to implement this: ```csharp public class IdentityDbContext(DbContextOptions options) : DbContext(options) { public DbSet Tenants { get; set; } public DbSet ApiKeys { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder .ApplyConfiguration(new TenantConfiguration()) .ApplyConfiguration(new ApiKeyConfiguration()); } } ``` By applying these configurations in the *OnModelCreating* method, we ensure that the entity mappings are centralized and maintained in a single place, keeping the domain model free from persistence concerns. This approach adheres to the principle of separation of concerns, making the codebase easier to manage and extend. ## Conclusion In conclusion, achieving a lean architecture in .NET Core involves focusing on business value and maintaining a clear separation of concerns. By adopting vertical slicing, we can encapsulate features into small functional units, enhancing code maintainability and readability. CQS further helps in organizing the code by separating read and write operations, leading to a more structured and efficient application. We can avoid over-engineering by resisting the urge to blindly follow popular architecture patterns and frameworks without understanding their value to the product and domain. Instead, we should adopt approaches that simplify our code and focus on business logic, such as using the EntityTypeBuilder in EF Core to manage persistence concerns without polluting our domain models. By keeping the domain models pure and focusing on what truly matters—delivering value to users and the company—we can build robust, secure, and scalable .NET Core applications that are easy to understand and maintain. This approach reduces complexity and ensures that our efforts are aligned with business goals, ultimately leading to more successful and impactful software projects. *Cheers*! ### Understanding Event Stores: Benefits and Insights URL: https://ricofritzsche.me/understanding-event-stores-benefits-and-insights/ Last updated: 2025-05-03T10:06:58.000Z As I build a flexible and fast event store in .NET Core 8, I'm addressing the challenges and exploring the possibilities of NoSQL databases like CosmosDb. In this article, I'll discuss the nature of an event store, how it differs from traditional relational databases, and why it's critical for your event-driven applications. The approach is to offer a variety of database providers such as CosmosDb, DynamoDb, Firebase, and others that can be seamlessly integrated depending on your project's needs. This flexibility will be encapsulated in an open source project, ultimately resulting in a versatile NuGet package. Reflecting on my experience, I built my first event store 10 years ago using a simple SQL database table during my first foray into CQRS for a client project. Today, I'm developing this capability in the context of providing API solutions that are reliable, cost-effective, and exceptionally fast. This journey from a basic SQL implementation to leveraging advanced NoSQL technologies illustrates a decade of technology evolution and learning. As we move forward, I will share insights and practical knowledge that can help you configure and effectively use your own event store. ## What is an Event Store? An event store is essentially a database designed specifically to capture events. Instead of storing the current state of data, it records the sequence of events that affect system data. Each event represents a change in data, and the store acts as a ledger of all these changes. This approach has several distinct advantages. First, it provides a complete historical record of all changes, not just the most recent snapshot. This is invaluable for debugging, auditing, and any scenario where understanding the sequence of events is critical. Second, it naturally aligns with Domain-Driven Design practices, where changes within a domain are captured as a series of domain events, making the evolution of the system clear annd logical. Using an event store can dramatically improve the scalability and responsiveness of applications by decoupling data processing from data storage and large , inflexible data models. Each write to the store is sequential and additive, eliminating the complex update operations that can slow relational databases under heavy load. One of the aspects I appreciate most about using an event store is the shift in focus it brings. Instead of focusing on designing complex object graphs and data table relationships, which can add layers of complexity, the focus shifts to behavior, logic, and understanding the flow of data. This shift simplifies the architectural approach. It also improves the clarity and manageability of the system. ## The Difference Between an Event Store and a Relational Database Understanding the difference between an event store and a relational database is crucial for developers and architects. To preemptively address the common objection—"yes, but..."—I can confidently state after three decades in the field: no, it does not add extra complexity. Instead, it presents a distinct alternative to the all-too-common database model-driven approaches. This shift often results in significant benefits, particularly in terms of extensibility and adhering to the Open Closed Principle, enhancing the system's ability to grow and evolve without disrupting existing functionality. A relational database is structured around tables and the relationships between them. It is designed to store the current state of data, making it ideal for applications where the primary requirement is to perform CRUD (create, read, update, delete) operations efficiently. Relational databases are adept at handling complex queries involving multiple tables and relationships because of their normalized structure. In contrast, an event store is not concerned with the current state of data, but rather with the series of events that led to that state. It records each event as an immutable log entry, capturing every change that occurs over time. This model allows developers to reconstruct past states of the application by replaying events, which is inherently impossible in a traditional relational setup. Scalability in event stores is generally more straightforward. Since events are appended sequentially, scaling often involves partitioning or distributing the event log across multiple nodes. Relational databases can also be scaled, but this usually requires more complex procedures such as sharding or replication, which can introduce challenges in maintaining transaction consistency and data integrity. For me, the advantages of event stores and event sourcing extend beyond just scalability and performance enhancements. They also simplify the coding process. When expanding the functionality of an aggregate, it typically involves only designing and adding a new command and event, without altering existing logic or structures. Similarly, when behaviors need to change, it doesn't require modifying an existing event; instead, you simply add a new version. This approach offers two key benefits: 1. It keeps the [aggregate](https://ricofritzsche.me/ddd-modularization-concepts-aggregates-part-ii/) open to extensions but closed to modifications, aligning perfectly with the Open Closed Principle. 2. It minimizes the risk of failures and, undoubtedly, reduces complexity. ## Top 5 Advantages of Using Event Stores Implementing an event store in your event-driven applications offers numerous benefits that can transform the way you handle data and business logic. Here are the top five reasons you should consider using an event store: ### 1) Simplified Evolution of Business Logic With event sourcing, adding new functionality or changing business logic often means introducing new event types or handling logic without modifying existing code. This decouples new development from legacy code, reduces the risk of introducing bugs into functionality that is already working, and eases the burden of maintaining backward compatibility. ### 2) Facilitates Complex Business Processes By using an event store, systems can more naturally model event-driven business processes. Each step in a process can be captured as an event, allowing systems to respond in real time as each event occurs. This capability makes it easier to handle complex, state-dependent workflows that are more difficult to implement in traditional relational databases. ### 3) Enhanced System Resilience Because event stores record changes in state rather than the state itself, they inherently support event replay to recover or rebuild state after a system failure. This can dramatically improve the resiliency and reliability of your system by providing a robust method for data recovery and consistency checks. ### 4) Scalability and Performance Event stores are designed to handle high volumes of writes with low latency due to their append-only nature. This architecture allows for easier horizontal scaling as workloads grow, because new events are simply added to the end of the stream without the need for costly data modification or restructuring that relational databases require. ### 5) Complete Event History and Auditability An event store maintains a comprehensive log of all events, allowing you to see the full history of changes to each entity over time. This complete historical context is invaluable for audit trails, compliance, and forensic analysis, ensuring that you can always trace state changes back to their origins. This feature is especially critical in industries such as finance, science, or healthcare, where understanding the sequence of events can be as important as the events themselves. These benefits highlight why event stores are an excellent choice for developers who want to build robust, scalable, and maintainable systems. By focusing on extensible and clean code, event stores allow you to design your applications around behavior and logic rather than static data models. This approach is ideal for creating systems that are adaptable and easy to update, eliminating the need to arbitrarily manipulate logic as requirements evolve. ## Conclusion In conclusion, we've explored the benefits of event stores from a higher perspective, focusing on their strategic advantages over traditional data storage methods. While this discussion provides a broad overview, the practical application of these concepts can often provide even deeper insights. In the next article, I'll walk you through the specific steps and technical details of how I built my event store using .NET Core 8 and CosmosDb. This solution designed to be flexible, allowing for integration with other storage technologies. Stay tuned to see how you can implement and adapt these principles in your own projects to ensure that your data architecture is robust, scalable, future-proof, and adaptable to evolving technology landscapes. *Cheers*! To make sure you don't miss out on the practical insights into building a flexible event store with .NET Core 8 and CosmosDb, be sure to subscribe. You'll be notified of the next article where I'll dive into the technical details and show you how to adapt these strategies for different storage technologies. ## Subscribe now ...and stay one step ahead in building sophisticated, scalable applications! Subscribe Email sent! Check your inbox to complete your signup. No spam. Unsubscribe anytime. ### Bounded Contexts: Behavior Over Data Structures - Part II URL: https://ricofritzsche.me/ddd-modularization-concepts-aggregates-part-ii/ Last updated: 2024-04-11T16:19:00.000Z In [part one](https://ricofritzsche.me/bounded-contexts-behavior-over-data-structures/) of this series, I talked about the importance of understanding Bounded Context when modeling software systems. This is a big deal because it clusters and makes large domains easier to understand. To be clear, it means that you have to figure out the [subdomains](https://ricofritzsche.me/what-means-domain-in-the-context-of-domain-driven-design/) within a large business domain before you can identifying Bounded Contexts. They define clear boundaries around a subdomain, each with its own model, language, and rules. I really focused on "verbs," to make a point. I did this on purpose because I've seen too many systems built around static things, or "nouns," and then try to add behavior later. It's important to remember that you need both the "nouns" and the "verbs" they do to fully explain what's going on. Both are super important to writing good software. It's not black or white! We need a good balance. > ...both the nouns and verbs are required to fully describe behavior. Just as OOP is often misused, so is DDD. Both are important for building good software. Our profession seems to wander between extremes instead of seeking balance. - [Dick Dowdell on Medium](https://medium.com/@dick-dowdell?ref=ricofritzsche.me) Today I want to go deeper in another fantastic key concept for modularization in DDD: **Aggregates** So, after we've figured out the Bounded Contexts, we've basically pinpointed the modules we can use to break a big, bulky system into more manageable pieces. These could be the building blocks for turning a monolith into a set of modules, or even microservices or self-contained systems. But it's kind of a first guess - sometimes we might find that one of these contexts is actually too big and needs to be broken into smaller ones. Now let's dive into tactical design and get closer to the actual coding. Everything we're discussing will be turned into code. We're always working within a Bounded Context, which could be a module in a monolith or a microservice. DDD introduces several patterns for tactical design, such as factories, repositories, value objects, entities, and Aggregates. We'll focus on value objects, entities, and Aggregates because they are critical to making things modular. Entities stand out in DDD because they always retain their identity, behavior, and have their own life cycle. They are identified by their unique identity. It's this identity that distinguishes them from one another, even if they have identical attributes such as name, age, or job. Think of two people who may have the same name, age, and job. What really makes them different is their unique identity, not just the name or job title they may have. An important point here: Entities in DDD are not data models (aka representations of db tables), they are domain models. They represent business objects with behavior. We are modeling domain concerns, not database tables. Value objects are different. They don't have an identity or a life cycle. They're defined solely by their attributes and are always immutable. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2024/04/Big-Object-Graph.png) Big Object Graphs inside a Bounded Context. Aggregates are an excellent way to avoid large object graphs in a Bounded Context. This would happen if we were dealing only with value objects and entities. Aggregates are therefore useful for clustering value objects and entities and to define a boundary. Without Aggregates, our object setups would quickly get too complicated and hard to follow, making them more likely to mess up. We don't want a change in one place accidentally causing problems somewhere else. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2024/04/Aggregates.png) Entities and Value Objects clustered to different Aggregates. Like I mentioned before, we put entities and value objects together into what's called an Aggregate. Each Aggregate has a key component known as the root entity, or Aggregate root, which serves as the single entry point to the Aggregate. The root entity has a unique global identity and is responsible for checking invariants. Invariants refer to the rules or conditions that must always be true for the Aggregate to be considered valid. These can be anything from ensuring that an account balance never drops below zero to ensuring that an order has at least one item before it can be processed. The Aggregate root takes on the role of enforcing these invariants, ensuring the integrity and consistency of the Aggregate's state. This also means that whenever changes are made to any part within the boundaries of the Aggregate, all of the invariants of the Aggregate must be satisfied. In a sense, you can think of the Aggregate as a boundary for transactions. In the image below, I've taken an example from [Eric Evans' book "Domain-Driven Design"](https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215?ref=ricofritzsche.me) to illustrate how the Aggregate controls access to the clustered objects solely through the Aggregate root. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2024/04/Aggregate-Boundary.png) There is only one entry point to change the state of the Aggregate through the root entity. Other modules are not allowed to interact directly with the *Tire* entity. This rule is in place to maintain consistency throughout the system. It's important to understand that anything outside the boundary of the Aggregate cannot hold a reference to anything inside it, such as the *Tire*. The root can provide a copy of a value object to another object, but it doesn't keep track of what happens to that copy. Since it's just a value with no ongoing connection to the Aggregate, its fate outside the aggregate is inconsequential. Aggregates should remain unaware of each other; they are independent units. They serve as a facade to hide the internal workings and specific business logic from anything outside their boundaries. However, it's acceptable for any object inside the aggregate to hold a reference, specifically the unique ID, to the root of another aggregate. *Cheers*! PS: Join me on my [**brand new Discord server**](https://discord.gg/bCsrKeef?ref=ricofritzsche.me)if you want to discuss this article or other DDD-related topics in more detail. ### Domain-Driven Design: The Power of CQRS and Event Sourcing URL: https://ricofritzsche.me/cqrs-event-sourcing-projections/ Last updated: 2024-04-07T09:50:49.000Z ![](https://cdn-images-1.medium.com/max/1600/1*43on5LbhGBqc7O5WmQ94mQ.jpeg) Image licensed under the [Unsplash+ License](https://unsplash.com/plus/license?ref=ricofritzsche.me). Over the past 15 years, I’ve been deep diving into Domain-Driven Design (DDD), learning and improving every step of the way. I’ve always been curious, especially when simple client requests turn out to be way more complex once you dig a little deeper. I’m particularly thankful for tools like *EventStorming* that help uncover the real processes and events behind what seems like straightforward requirements, making everything clear not just to me but also to clients and domain experts. About 10 years ago, I first used Command Query Responsibility Segregation (CQRS) and Event Sourcing (ES) in a business application. This approach opened my eyes to many benefits, even though it definitely came with its challenges. But, one of the biggest game changers for me was realizing how CQRS naturally makes data flow in one direction. This single direction flow isn’t just about keeping things organized; it’s a game changer for how we build, maintain, and scale systems. It means we can separate the actions that change data from the actions that read data. This doesn’t just clear up a lot of potential mess; it also gives us a straightforward way to handle complex systems. While I’m a big fan of using CQRS/ES in my projects, it’s not the only way to successfully apply DDD. DDD is a big field with lots of tools and approaches, and the best one really depends on what you’re working on, and also the skill level of your team. CQRS has been a great fit for me, especially when dealing with complex systems where separating the read and write operations can make everything more manageable. Every project is unique, and there are plenty of situations where other strategies might be the better choice. ### The Benefits of CQRS *But what is CQRS?* It’s is a powerful concept. At its core, it involves separating the tasks of writing data (commands) and reading data (queries) into two distinct parts. This separation is intended to clarify the structure of the system, delineate responsibilities, and increase the speed at which data can be read from the system. I’ve noticed that CQRS is often seen as a performance optimization strategy due to the strict separation of read and write storage. While boosting performance is indeed a significant advantage, it barely scratches the surface of the benefits. CQRS offers much more. First off, this approach gives us a clean, organized structure. This organization extends into a fantastic vertical slice of domain concerns, meaning we can focus on specific areas without getting lost in a sea of complexity. Another huge plus is the enforced unidirectional flow of data. This isn’t just about keeping data movement tidy; it fundamentally changes how we design and think about our systems. It ensures that our data moves in a predictable pattern, reducing confusion and errors. CQRS also pushes us to focus more on behavior rather than just data. This means we’re designing our systems with real-world actions and consequences in mind, steering clear of anemic domain models that are all too common in software projects. An anemic domain model is like a skeleton without muscles – it might have all the parts, but it can’t do much on its own. CQRS helps us avoid that, ensuring our models are rich with behavior and functionality. The separation of concerns in CQRS allows us to enhance system views easily. Since read models are separate, we can tweak and improve them without touching the core logic of our application. This flexibility is invaluable for evolving systems to meet new needs or address new insights without a major overhaul. ### Mirrored Databases are not CQRS I’ve seen a trend where the read/write split in CQRS is interpreted as having two mirrored databases with identical data models, relying on replication for synchronization. However, this approach may miss the point of what CQRS is really about. ![](https://cdn-images-1.medium.com/max/1600/1*Ic2K-t6AUsuVctdyeJYmSQ.png) Anti-Pattern: Mirrored Databases are not CQRS The heart of the matter is the high coupling that comes from using the same data model for both writes and reads. CQRS shines when it allows independence, allowing the write side to develop a **rich, behavior-driven model** while the read side optimizes for queries. Mirroring data models across databases ties the two sides too tightly together, limiting the flexibility of the system and the potential for each side to evolve as needed. In addition, this setup runs the risk of turning the write model into a thin layer of logic around a database schema, rather than a robust domain model. It keeps us stuck in a data-centric mindset where the business logic isn’t where it should be — embedded in the domain itself. It is important to understand that the write model is very different from the read model. Otherwise, all flexibility and power is lost. ### Behavior Over Data with CQRS/ES For me, an important benefit of combining CQRS with Event Sourcing is that it shifts the focus to behavior rather than just data models, so to speak by design. This perspective is essential because it aligns our development efforts much more closely with the real-world processes and logic that our software is intended to represent. Traditional development practices often place too much emphasis on the structure of data — how it’s stored, retrieved, and modified. While this approach is important, it can also lead us to overlook the actual behaviors and interactions that generate and consume this data. And believe me, I’ve seen this happen more than once. CQRS, when coupled with ES, reverses this focus. It encourages us to model our systems around the business events and actions that drive change, not just the data that those changes affect. This means that our code reflects business logic in a much more expressive way. Instead of simply inserting, updating, or deleting rows in a table, we capture meaningful business events such as “order placed”, “user created”, or “inventory updated”. This makes our models richer and more meaningful. It improves our ability to rethink and evolve our systems over time. ![](https://cdn-images-1.medium.com/max/1600/1*6yv_VA13FGrgwfwVTqDiFg.png) CQRS/ES: Unidirectional flow, meaningful model. ### The Power Of Projections One of the most important aspects of the CQRS pattern is the concept of projections, which essentially serve as the basis for read models. This approach has profound implications for the way we deal with data in our applications because, most importantly, it frees us from the constraints associated with representing data on the write side. This separation ensures that the focus remains on accurately capturing behaviors and domain events, without the added complexity of how that data should be queried or displayed. ![](https://cdn-images-1.medium.com/max/1600/1*ljGpsOBVhZW-dqSPsLTMyQ.png) CQRS/ES Projections — and again unidirectional data flow. Read-side independence provides the flexibility to tailor data projections to a wide range of needs. Whether it’s structuring data for efficient querying or optimizing for specific views, projections can be designed to meet your needs. This capability is about meeting today’s needs and can also be easily adapted and extended to meet future requirements without touching existing code. For projections, there is no need to revise existing structures or change the logic on the write side. Instead, we can simply create a new, isolated module specifically designed to project and analyze data as needed. This modularity and extensibility is a huge advantage, allowing our systems to evolve organically without compromising the integrity of the core domain logic. ![](https://cdn-images-1.medium.com/max/1600/1*zxacZ81FBYXwKD281HJakg.png) CQRS/ES: Viewing Data from a projection ### Closing Thoughts CQRS combined with Event Sourcing changes the way we think about and build our software systems. By separating the writing and reading of data, we get a clean, organized structure that improves performance and scalability. More importantly, this approach shifts our focus to behavior and real-world actions, ensuring that our models are rich in functionality and aligned with the domain they represent. Projections also allow us to adapt and extend our systems without touching existing code, underscoring the flexibility and robustness of CQRS/ES. *Cheers*! ### The Role of a Unique Identity in Entities URL: https://ricofritzsche.me/building-domain-models-the-deadly-secret-of-integer-ids/ Last updated: 2024-04-03T12:56:03.000Z Database-generated integer IDs are ubiquitous in tutorials, auto-generated code (which I find creepy in and of itself), and numerous software projects. Despite technological and methodological advances, this practice is persistently part of the developer's toolkit, often without much thought to its implications. In this article, we will explore the significant impact this "small" detail can have on our modeling approach, and why it's time to rethink the way we think about and implement identifiers in our systems. It is somewhat understandable that integer IDs appear in certain scenarios. Consider a small, straightforward application whose single purpose is to input and display data via a user interface. In these cases, where complexity is minimal, no business logic is involved, only basic CRUD operations are performed, and nothing is distributed, the use of integer IDs may not attract attention. Especially when no business logic is being enforced behind the scenes, this approach may be sufficient. But to be realistic, after 30 years in the software industry, complexity is usually underestimated. Domain experts often think it's simple, and after a few weeks they realize how complex it really is. Or later, new complex requirements are added that require a domain model. However, if we venture into the area of complex process modeling, messaging, or distributed applications. In these contexts, relying on database-generated integer IDs is a bad practice and a fundamental misstep. And why is that? ## Integer IDs belong to databases, not to the domain itself! It's a common assertion among developers to claim their systems are built around domain models. However, a critical review often uncovers a reliance on auto-incremental integer IDs, sourced directly from databases, which is a clear deviation from the concept of a persistence-independent domain model. When we use database-generated integer IDs in the domain model, it shows we're mixing up concerns that should be kept apart. This mix-up goes against the Separation of Concerns (SoC) principle, revealing that what we have isn't truly a domain model but rather just a data model. This principle tells us to keep different parts of the system — like the domain logic and how data is stored — separate. This way, each part can do its job without getting tangled up with others. The following example shows a class that just represents a database table. **This is not a domain model. (** *Even though people have often tried to sell it to me as an domain model.* **)** ```csharp [Table("Products")] public class Product { [Key] [DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } [Required] [StringLength(100)] public string Name { get; set; } [Column(TypeName = "decimal(18,2)")] public decimal Price { get; set; } [StringLength(255)] public string Description { get; set; } } ``` Even if behavior were added to this class, it would remain closely linked to the database. This would mean that the object would first have to be persisted in order to obtain an identity. ## Entities and Identity in Domain-Driven Design DDD uses a set of patterns for effective tactical design, including entities, aggregates, value objects, factories, and repositories. Entities stand out as they form the backbone of our domain, representing core business objects within a [bounded context](https://ricofritzsche.me/bounded-contexts-behavior-over-data-structures/). Entities in DDD are distinguished by their **constant identity, behavior** and their **own lifecycle**. They are about domain behavior. Entities bring depth and meaning to the domain because they are uniquely identified by their identity. Their identity is what distinguishes them from each other, even if other attributes are the same. Consider two people with identical names, ages, and jobs. It's their unique identities - not just their names or job titles - that differentiate them. [Value objects](https://ricofritzsche.me/value-objects-implementing-domain-driven-design-by-example/), on the other hand, have no identity and no life cycle. They represent themselves only by their values. They are always immutable. But how do we assign these identities? Given our goal of avoiding tight coupling and enforcing SoC, it's clear that retrieving identities from the database is not the right approach. Instead, the entity itself should take care of generating identifiers, often during the creation of a new object via a factory method. A common strategy is to use a Global Unique Identifier (GUID) or Universal Unique Identifier (UUID) to ensure that each entity has a unique, domain-generated identity. Let's look at a simple example of what an entity might look like without having to depend on a database. ```csharp public class Product { public Guid Id { get; private set; } public string Name { get; private set; } public decimal Price { get; private set; } public string Description { get; private set; } // Private constructor used for creating new instances with a factory method private Product(string name, decimal price, string description) { Id = Guid.NewGuid(); // Assign a new unique identifier Name = name; Price = price; Description = description; } // Public constructor for reconstituting an existing instance from storage public Product(Guid id, string name, decimal price, string description) { Id = id; Name = name; Price = price; Description = description; } // Factory method for creating new Product instances public static Product CreateNew(string name, decimal price, string description) { if (string.IsNullOrWhiteSpace(name)) throw new ArgumentException("Product name cannot be empty.", nameof(name)); if (price <= 0) throw new ArgumentException("Price must be greater than zero.", nameof(price)); return new Product(name, price, description); } // Additional domain logic and methods here } ``` ## Specific Purpose and Meaning Introducing a special way of handling IDs in our applications increases the clarity and security of our domain models. To make it even more perfect, we encapsulate our very general IDs in a value object like ProductId, giving them a specific purpose and meaning that goes far beyond that of a mere identifier. This is where the ubiquitous language comes in. It gives each ID a role and ensures that it is recognized not just as any *Guid*, but as an integral part of the domain to which it belongs. This approach underscores the importance of treating IDs as a domain concept rather than just a database requirement. It's about reinforcing the idea that every element within our domain model, including IDs, should have a defined role and purpose that aligns with our understanding of the domain itself. Consider this C# example where we define a ProductId as a Value Object: ```csharp public readonly record struct ProductId(Guid Value) { public static implicit operator Guid(ProductId productId) => productId.Value; public static implicit operator ProductId(Guid value) => new ProductId(value); } ``` This encapsulation increases the expressiveness of our model and also strengthens the integrity of the domain by preventing misuse of IDs. ## Closing Thoughts Using unique identifiers such as GUIDs or UUIDs instead of traditional integer IDs better aligns with DDD principles. They also greatly enhance your system's ability to scale and integrate across distributed environments. Such identifiers ensure the uniqueness of each entity across contexts and systems, eliminating the risks associated with duplicate or conflicting IDs. By making this shift, developers can build models that are not only better aligned with the realities of their domain, but also more versatile and future-proof in the increasingly networked landscape of modern software development. Adopting this approach is a critical step toward creating systems that are truly robust, scalable, and distributable. *Cheers*! ### Bounded Contexts: Behavior Over Data Structures - Part I URL: https://ricofritzsche.me/bounded-contexts-behavior-over-data-structures/ Last updated: 2024-04-03T09:48:18.000Z ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2024/04/fabio-oyXis2kALVg-unsplash-1.jpg) Photo by [fabio](https://unsplash.com/@fabioha?utm%5Fcontent=creditCopyText&utm%5Fmedium=referral&utm%5Fsource=unsplash) on [Unsplash](https://unsplash.com/photos/geometric-shape-digital-wallpaper-oyXis2kALVg?utm%5Fcontent=creditCopyText&utm%5Fmedium=referral&utm%5Fsource=unsplash)Photo. You've probably heard discussions about modularization in software development, or even used it in your own projects. Concepts such as separation of concerns (SoC) are familiar territory. Modularity is a specialization of SoC, holds to principles such as information hiding, loose coupling, and high cohesion. Those familiar with my writings know that I've explored these essential principles in depth. However, a recurring question is how to effectively build modular systems in practice? This is where Domain-Driven Design (DDD) introduces two key concepts: Bounded Context and Aggregates, which provide an iterative approach to identifying modules. Unlike the more linear methodologies promoted by object-oriented analysis and design, which typically follow a waterfall-like process from requirements through design, implementation, and verification, DDD takes a distinctly iterative stance. Eric Evans' groundbreaking work, Domain-Driven Design, published over two decades ago, shed light on the modularization of complex systems at a vertical level. It's a common misconception that SoC is only about separating engineering concerns horizontally, through architectures such as Clean Architecture or Layered Architecture. However, the purpose of this article is to explore the vertical modularization of complex systems to ensure longevity, maintainability, and comprehensibility. I will focus on modularization through Bounded Contexts in this article. ## Shifting Perspectives: From Nouns to Verbs The traditional approach to object orientation in software development has been overwhelmingly noun-centric. This perspective focuses on identifying entities such as customers and orders, emphasizing private attributes, methods, and a plethora of getters and setters. The primary goal becomes designing data models to meet every conceivable requirement, often resulting in implementations that are confusing at best. Typically, behavior is relegated to external services tasked with manipulating these data models (they also call it entities), resulting in code that's not only difficult to read, but also hard to understand. This approach leads to what's known as anemic domain models, which are characterized by a lack of business logic. Despite its pitfalls, this methodology persists throughout the industry. DDD, however, takes a very different approach, prioritizing verbs over nouns. The focus shifts from data to behavior, focusing on what the software needs to do rather than what data it needs to store. In DDD, the key to identifying a Bounded Context lies in understanding the business rules, decisions, and policies - all articulated through a ubiquitous language. This language, defined within each Bounded Context, uses precise terminology that carries specific meaning, ensuring clarity and consistency in communication among all project stakeholders. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2024/04/Bounded-Context-Behavior.svg) The importance of this shift became clear during a discussion I had with a team that included traditional-minded requirements engineers. When I stressed the need for a shared, consistent language within a bounded context, one engineer dismissed the idea, arguing that the success of a software project doesn't depend on the choice of words. This response not only underscored a misunderstanding of the principles of DDD, but also highlighted a common oversight: the failure to adequately model behavior. The belief that semantics are secondary in software development overlooks the critical role that language plays in shaping our understanding of the domain and, by extension, the software we build to operate within it. In DDD, modeling behavior and defining a consistent language are essential to laying the foundation for a robust and coherent domain model. ## Understanding Context Through Example The concept of context is central to disciplines ranging from linguistics and philosophy to software development. To understand the importance of context, let's examine the avocado, a familiar object that undergoes a remarkable transformation when viewed through different lenses. In the culinary world, avocados are primarily considered a vegetable. They're the star of savory dishes, from guacamole to salads, where their creamy texture and rich flavor enhance the flavor profiles of these preparations. This classification is convenient and shapes the way avocados are marketed, sold and consumed. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2024/04/getty-images-OYtVf9Q1OTQ-unsplash.jpg) Image licensed under the [Unsplash+ License](https://unsplash.com/plus/license?ref=ricofritzsche.me). However, when we shift our perspective to botany, the classification of the avocado changes dramatically. Botanically speaking, an avocado is a fruit, or more specifically, a berry. This classification is based on specific criteria: it develops from the flower of the avocado tree, contains a seed, and has a pulp. This discrepancy in classification between culinary and botanical contexts clearly illustrates how the meaning and categorization of an object can change dramatically depending on the context. Just as the identity of the avocado changes from vegetable to fruit when it moves from the kitchen to the laboratory, so do concepts and models within different bounded contexts in software development. ## Bounded Contexts The avocado analogy sheds light on an essential aspect of DDD: the purpose and application of Bounded Contexts. Understanding that an avocado can be considered both a fruit and a vegetable, depending on the context, parallels the notion that a single entity can have different meanings and roles within different Bounded Contexts in domain modelling. This understanding challenges a common misconception in the field - that something like an entity should serve as a universal solution covering all possible interpretations as shown in the example below. ``` Entity: User Attributes: - UserID: INT, Primary Key - Username: VARCHAR - Password: VARCHAR - Email: VARCHAR - FirstName: VARCHAR - LastName: VARCHAR - DateOfBirth: DATE - RegistrationDate: DATETIME - LastLogin: DATETIME - PhoneNumber: VARCHAR - Address: VARCHAR - UserType: ENUM('customer', 'admin', 'moderator', 'vendor', 'guest') - PaymentInformation: VARCHAR - ShippingAddress: VARCHAR - BillingAddress: VARCHAR - Preferences: TEXT Relationships: - HasMany: Orders - BelongsTo: UserGroup - HasMany: Reviews - HasOne: ShoppingCart - HasMany: Tickets (for customer support) - HasMany: Posts (for forums or blogs) ``` The concept of a Bounded Context is purposefully bounded; it's not about for instance creating an all-encompassing, vague **User** entity that tries to be everything to everyone. Instead, it focuses on modeling behavior that is specific to the purpose of the context. A Bounded Context defines the boundaries of a model tailored to a specific application, ensuring that the language and interactions within it are precise and meaningful. In software development, however, problems arise when the focus shifts from behaviors (verbs) to entities (nouns). This shift often leads to the creation of a data model that acts as a structural boundary around which behavior is then defined and constructed. The problem with this approach is that when a data model - or object - becomes too broad, it loses its ability to effectively serve a specific purpose. As a result, the language used in this context becomes more general and less precise, reducing the value and usefulness of the model. This drift toward genericism not only obscures the clarity of the model, but also undermines the fundamental principles of DDD. By anchoring our models in behavior rather than structure, we maintain the focus on the specific actions and interactions that define a particular context. This approach not only ensures that our models remain purposeful and coherent, but also facilitates more effective communication and understanding within the development team and across the project as a whole. ## Conclusion As we conclude this first part of the modularization journey through Domain-Driven Design, we've highlighted the central role of Bounded Contexts. Shifting the perspective from static entities to dynamic behaviors within specific contexts clarifies the model. It also increases the flexibility and coherence of a systems. This exploration sets the stage for a deeper dive into DDD's capabilities in our subsequent discussion. My next focus will be on effectively modelling aggregates within these Bounded Contexts. Join me in the next part of this series as we continue to unpack the practical aspects of DDD, further empowering you to tackle the complexities of software development with confidence and precision. *Cheers!* ### Lifetime Entrepreneur - Episode #1 URL: https://ricofritzsche.me/lifetime-entrepreneur/ Last updated: 2024-04-01T18:39:42.000Z I share lots of good stuff about coding and tech for free. You can read and learn a lot without paying anything. There's just one special thing I keep just for my readers - my exclusive newsletter. It's also free, but you need to sign up to get it. _This post is for subscribers only._ ### Secure CI/CD with GitHub Actions: Deploying to AKS Using ACR and OIDC URL: https://ricofritzsche.me/secure-ci-cd-with-github-actions-deploying-to-aks-using-acr-and-oidc/ Last updated: 2024-04-01T09:03:42.000Z I'm currently developing a Geofencing API using a microservices architecture to ensure flexibility and maintainability through loosely coupled services. This architectural choice underscores my dedication to building an application that can easily adapt to changing needs and scale efficiently. However, I recognized the critical need for a robust infrastructure and seamless automation. The challenge wasn't just choosing the right tools; it was integrating them in a way that increased security without sacrificing efficiency. A common challenge is securely deploying containerized applications to Azure Kubernetes Service without embedding credentials like username and password for the registry login in the GitHub Actions workflow. This blog post walks you through setting up a secure pipeline that uses Azure Container Registry and OpenID Connect for authentication, effectively avoiding the need for hardcoded credentials. ## Prerequisites Before we begin, ensure you have the following: - An Azure account with an AKS cluster and an ACR instance configured. - A GitHub account with a repository for your project. - Azure CLI and kubectl installed on your local machine. ## Understanding the Components - Azure Kubernetes Service (AKS) provides a managed Kubernetes service that simplifies the deployment, management, and operation of Kubernetes clusters in Azure. - Azure Container Registry (ACR) is a managed Docker registry service based on the open source Docker Registry 2.0\. ACR allows you to store and manage container images across all types of Azure deployments. - OpenID Connect (OIDC) is an authentication protocol based on OAuth 2.0 that allows GitHub Actions to authenticate to Azure using short-lived tokens, eliminating the need to store and manage long-lived credentials. ## The Goal My goal is to avoid checking the "Admin User" checkbox in the ACR's access key settings, not having username/password credentials, and avoiding the following part of the GitHub actions. ```yaml - name: Log in to Azure Container Registry uses: azure/docker-login@v1 with: login-server: ${{ secrets.REGISTRY_LOGIN_SERVER }} username: ${{ secrets.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_PASSWORD }} ``` Let's get started on being more secure by integrating with a managed identity. ## Step 1: Configure AKS to Authenticate with ACR Using Managed Identity The first step is to ensure your AKS cluster can pull container images from your ACR instance using its managed identity, thus eliminating the need for Docker credentials. **Enable Managed Identity on AKS** (if not already enabled): ```bash az aks update -n $AKS_NAME -g $RESOURCE_GROUP_NAME --enable-managed-identity ``` **Grant AKS Access to ACR**: ```bash az aks update -n $AKS_NAME -g $RESOURCE_GROUP_NAME --attach-acr $ACR_NAME ``` This command allows the AKS cluster to authenticate to ACR directly using its managed identity. I already described this more in detail here: ## Step 2: Creating an App Registration for GitHub Actions in Azure App registrations in Azure are essentially creating a unique identity for your applications within the Azure ecosystem. This identity is crucial for enabling secure interactions between your application and Azure's services and resources. ### Setting Up Federated Access for GitHub Actions - Navigate to Microsoft Entra ID and look for "App registrations". - Create a new registration named *github-workflow*. If you choose a different name, make sure to note it down for future reference. - Once your app registration is ready, go to "Certificates & secrets". - You'll see a section titled "Federated credentials". Here, click on "+ Add Credential". ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2024/03/image.png) - This step is about linking your GitHub repository with Azure, allowing for a secure and direct integration between your GitHub Actions workflows and Azure services. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2024/03/Screenshot-2024-03-29-at-13.09.57.png) GitHub Actions can be authenticated using a token issued by GitHub's OIDC provider, thanks to OIDC federation with Entra ID (former Azure AD). ## Step 3: Adding the "ACR Push" Role to Your App Registration Once you've set up federated access to your GitHub repository with Azure through application registration, the next important step is to ensure that this application identity has the right permissions to interact with your ACR instance. Within the Azure portal, go to your Azure Container Registry resource and look for the "Access control (IAM)" option in the sidebar. This is where you can manage permissions for your Azure resources. Click on "Add a role assignment" to open the role assignment wizard. In the role selection step, choose the "ACR Push" role. This role allows the bearer to push new container images to your registry, which is precisely what your GitHub Actions workflow needs to do. When prompted to select the assignee, choose "Azure AD user, group, or service principal," and then search for the name of your app registration. We named it "github-workflow" earlier. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2024/03/image-1.png) By completing this step, you'll effectively be extending your GitHub Actions workflow with the ability to push updates to your container images directly to ACR. ## Step 4: Set Up GitHub Actions Workflow With OIDC configured, your GitHub Actions workflow can authenticate to Azure and deploy to AKS without hardcoded credentials. This can look as follows. ```yaml name: Deploy to Development AKS on: push: branches: - dev permissions: id-token: write contents: read jobs: build-and-push: runs-on: ubuntu-latest environment: dev steps: - uses: actions/checkout@v4 - name: Azure login uses: azure/login@v2 with: client-id: ${{ secrets.AZURE_CLIENT_ID }} tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - name: Set Git Commit SHA run: | echo "GIT_COMMIT_SHA=${GITHUB_SHA}" >> $GITHUB_ENV - name: Docker build and push ACR run: | az acr login --name ${{ secrets.REGISTRY_LOGIN_SERVER }} docker build -t ${{ secrets.REGISTRY_LOGIN_SERVER }}/geofencing-ui:${{ env.GIT_COMMIT_SHA }} . docker push ${{ secrets.REGISTRY_LOGIN_SERVER }}/geofencing-ui:${{ env.GIT_COMMIT_SHA }} ``` ## Conclusion As I conclude this journey of securing my geofencing API deployment, it's clear that integrating OIDC with AKS and ACR has been nothing short of transformative. By using OIDC, I've avoided the all-too-common pitfalls of credential management. There's a certain peace of mind that comes from knowing that I'm not juggling sensitive keys or tokens in my workflow, which significantly reduces the risk of a breach. What stands out is the simplicity of this setup. The managed identity bridge between AKS and ACR has streamlined my deployments; no more wrestling with Docker login commands or troubleshooting access rights. It's all about keeping things simple and focusing more on what's important - building and deploying, and less on the overhead of managing identities. ## Don't miss a thing If you've found these insights on securing your CI/CD pipelines and deploying with confidence using GitHub Actions, AKS, and ACR valuable, there's plenty more where that came from. Subscribe now for more valuable content like this. ## Sign up for Rico Fritzsche I'm Rico Fritzsche, a software architect, coder, and consultant. I help teams develop systems faster, more efficiently, and with greater maintainability. Subscribe Email sent! Check your inbox to complete your signup. No spam. Unsubscribe anytime. ## Need Help? If you're facing the complexities of building secure, efficient cloud deployments on Azure and could use a hand, I'm here to help. Contact me for targeted guidance, support tailored to your needs, and to learn more about the conditions. ### Value Objects: Implementing Domain-Driven Design by Example URL: https://ricofritzsche.me/value-objects-implementing-domain-driven-design-by-example/ Last updated: 2024-04-03T12:54:06.000Z Today, I’m going to jump right into a real-life example of what I just noticed while modeling the implementation of the core domain *GeofenceDetection* of my soon-to-be-released Real-Time Location Insights API. To give a short overview: *GeofenceDetection* is the core domain of my product because it contains the core logic with the highest business value. I have written several articles about it. If you are new to this topic, I recommend reading this article: [What Means Domain in the Context of Domain-Driven Design?](https://ricofritzsche.me/what-means-domain-in-the-context-of-domain-driven-design/) I have identified *BusinessLocation* as the root entity *aka* aggregate root. Aggregates are at the heart of your domain model. They encapsulate entities and value objects into a single entity with consistency and transactional integrity. An aggregate root, like *BusinessLocation*, acts as a central poinThe good thing is that it is easy to apply by following Domain-Driven Design principles. Value Objects give us the power to implement code that is secure by design. Let me explain this by entering my journey. When I started implementing my the BusinessLocation aggregate root I did the following.t in a domain, grouping together different elements (entities and value objects) to manage them as one unit, ensuring they stay consistent and correct through all operations. Entities have a unique identity so that they can be distinguished from one another, whereas value objects have no identity and are defined only by their attributes. In this article, my aim is to highlight the importance and effectiveness of value objects within Domain-Driven Design (DDD). These elements are fundamental to ensuring domain consistency and integrity. Furthermore, value objects serve as a central tool for constructing domain models that inherently embody secure+ity by design. I believe that security should be built in as a natural part of developing. I see security as a concern, not a feature. ```csharp public class BusinessLocation : IAggregateRoot { private IList _customers; private BusinessLocation(Guid id, Guid tenantId, double longitude, double latitude, double radius, IList? customers) { Id = id; TenantId = tenantId; Longitude = longitude; Latitude = latitude; Radius = radius; _customers = customers ?? new List(); } public Guid Id { get; init; } public Guid TenantId { get; init; } public double Longitude { get; init; } public double Latitude { get; init; } public double Radius { get; init; } // Factory method public static BusinessLocation CreateNew(Guid id, Guid tenantId, double longitude, double latitude, double radius, IList customers = null) { // Validate parameters return new BusinessLocation(id, tenantId, longitude, latitude, radius, customers); } // Other methods... } ``` While implementing a factory method to ensure consistent initialization of instances, I found myself pausing to carefully evaluate the validation of incoming parameters.In DDD, factories play a fundamental role in encapsulating the logic for creating complex objects and ensuring that they meet the strict requirements. This approach simplifies object creation and also strengthens the integrity and consistency of the domain model. Upon reflection, I recognized that embedding validation logic directly into the factory method could compromise security and lead to unnecessarily bloated code that ultimately obscures its purpose. Furthermore, from a domain perspective, longitude and latitude are fundamentally different and should not be treated as identical types. This distinction underscores the importance of designing our domain models with precision, ensuring that each element accurately reflects its real-world counterpart, and adhering to the principles of clarity and safety in DDD. By implementing separate value objects for **Latitude** and **Longitude**, specific validations and behaviors can be encapsulated, strengthening the model’s integrity and expressiveness. For general understanding, latitude and longitude are specific coordinate types that describe a location on the earth’s surface using the geographic coordinate system. Let’s define, from a domain point of view, what are the characteristics of these two types. - **Latitude**: A value object specifically for latitude, ensuring values are within the range of -90 to 90 degrees. - **Longitude**: A value object for longitude, ensuring values are within the range of -180 to 180 degrees. The implementations will look like the following: ```csharp public class Latitude : ValueObject { public double Value { get; init; } public Latitude(double value) { if (value is < -90 or > 90) throw new ArgumentOutOfRangeException(nameof(value), "Latitude must be between -90 and 90 degrees."); Value = value; } protected override IEnumerable GetEqualityComponents() { yield return Value; } } public class Longitude : ValueObject { public double Value { get; init; } public Longitude(double value) { if (value is < -180 or > 180) throw new ArgumentOutOfRangeException(nameof(value), "Longitude must be between -180 and 180 degrees."); Value = value; } protected override IEnumerable GetEqualityComponents() { yield return Value; } } ``` As you can see in the code provided above, I used a *ValueObject* base class because it facilitates the correct implementation of value objects within a domain model. Value objects are critical for capturing and encapsulating values along with their associated behavior without the need for a unique identity. This class ensures that value objects are compared based on their content and properties, not their memory references, in accordance with the DDD principle that value objects should be equal if all their attributes are equal. Value objects **must be immutable**; once created, their state cannot change. They do not have their own lifecycle. This immutability is critical because it ensures that the object’s hash code remains consistent, an essential aspect for collections. In addition, immutability makes the system more predictable and easier to reason about, since value objects can be safely shared between different parts of the application without the risk of unintended changes. ```csharp public abstract class ValueObject { public override bool Equals(object obj) { if (obj is not ValueObject other) { return false; } return GetEqualityComponents().SequenceEqual(other.GetEqualityComponents()); } public static bool operator ==(ValueObject a, ValueObject b) { if (ReferenceEquals(a, null) && ReferenceEquals(b, null)) return true; if (ReferenceEquals(a, null) || ReferenceEquals(b, null)) return false; return a.Equals(b); } public static bool operator !=(ValueObject a, ValueObject b) { return !(a == b); } public override int GetHashCode() { return GetEqualityComponents() .Select(x => x?.GetHashCode() ?? 0) .Aggregate((x, y) => x * 23 + y); } protected abstract IEnumerable GetEqualityComponents(); } ``` In this revised version, as shown below, *BusinessLocation* uses the specific value objects for Longitude and Latitude (and also for Radius), enhancing the expressiveness and integrity of the domain model. This change ensures that any instance of *BusinessLocation* will always have valid coordinates, using the encapsulated validation logic within the specific value objects. ```csharp public class BusinessLocation : IAggregateRoot { private IList _customers; private BusinessLocation(Guid id, Guid tenantId, Longitude longitude, Latitude latitude, Radius radius, IList? customers) { Id = id; TenantId = tenantId; Longitude = longitude; Latitude = latitude; Radius = radius; _customers = customers ?? new List(); } // Public properties public Guid Id { get; } public Guid TenantId { get; } public Longitude Longitude { get; } public Latitude Latitude { get; } public Radius Radius { get; private set; } // Factory method public static BusinessLocation CreateNew(Guid id, Guid tenantId, double longitude, double latitude, double radius, IList customers = null) { return new BusinessLocation(id, tenantId, new Longitude(longitude), new Latitude(latitude), new Radius(radius), customers); } // Other Methods... } ``` In refactoring the *BusinessLocation* aggregate root to use value objects instead of simple types, we took several key steps to improve the domain model in accordance with DDD principles, focusing in particular on encapsulation, immutability, and robustness of the domain model. Here’s a summary of the key actions and what they’re for: 1. **Introduced value objects for coordinates:** I have replaced simple double types for longitude and latitude with dedicated value objects. This change ensures that the coordinates are not just raw numbers, but have domain-specific meaning and behavior, including validation logic that ensures the coordinates are within valid ranges. 2. **Encapsulating validation logic:** By moving the validation logic into the value objects (latitude and longitude coordinates), we ensure that each instance of these objects is valid according to the domain rules. This approach prevents invalid data from being created or manipulated within our domain model, thereby enforcing business invariants. 3. **Immutability**: The value objects are designed to be immutable, meaning that once an instance is created, its state cannot be changed. This property is critical to maintaining consistency and predictability within the domain model, as it prevents side effects from unintended changes. 4. **Improved domain model expressiveness:** Using value objects instead of primitives makes the domain model more expressive. It becomes clearer to developers and domain experts alike what each part of the model represents and how it behaves. For example, a coordinate value object immediately conveys its purpose and constraints, unlike a simple double. 5. **Improved code structure and maintenance:** Refactoring results in a cleaner, more maintainable code base. Value objects can encapsulate complex behaviors and validations, reducing duplication and separating concerns within the domain model. This modular approach simplifies future changes and extensions to the domain logic. 6. **Ensure domain consistency:** By using value objects, we ensure that all instances of *BusinessLocation* and its components consistently adhere to domain rules and constraints throughout the application. This consistency is critical to maintaining the integrity of the domain model and the correctness of its operations. In summary, the refactoring process has significantly improved the design and implementation of the *BusinessLocation* aggregate root by introducing value objects. This enhancement not only strengthens the expressiveness and integrity of the domain model, but also aligns with DDD best practices, making the model more robust, maintainable, and aligned with the business domain. *Cheers*! ### Decoupling Software Components with Events URL: https://ricofritzsche.me/decoupling-components-with-events/ Last updated: 2024-04-08T18:16:00.000Z When we talk about building software that's divided into separate services, like microservices or service oriented architecture, there's a common way we set things up. It's pretty straightforward, much like a conversation. This is how we often write code: call some code, wait for it to finish, and then continue. This approach is perfect for many situations we face, especially websites that we interact with directly. You click a button and expect something to happen in response. However, as we start dealing with more and more separate services, things start to shift a bit. The more services there are, the more complex it gets to have all those services talking to each other directly and instantly. With this basic understanding of software communication, let's explore a principle that strongly influences how we design our systems for effective interaction. ## Command and Query Separation (CQS) The discussion in the article about the importance of having loosely coupled components in software systems brings us to a fundamental principle of system functionality. This principle was clearly articulated by Bertrand Meyer, who pointed out a simple yet profound rule about how methods should behave in programming. He argued that a method should focus on doing one of two things: performing an action, which we call a command, or providing information, known as a query. Importantly, it should avoid trying to do both. The significance of this is that if you're just getting information from a method, you shouldn't inadvertently cause changes in the system. In essence, queries should be just that - queries, without any side effects, ensuring that the system remains unaltered and predictable. Having laid the groundwork with CQS, we turn to the first pillar of this principle: Commands. ### Understanding Commands Commands in software are pretty much like giving a direct order. It's like asking your friend to turn off the lights; you expect him to do just that, to change the state of the room from light to dark. In a software, a command is a message we send to another part of our application, asking it to perform a certain task that will change the way things are. It's kind of like saying, "Hey, I need you to do this task," and then hanging around to see if the task is done. Here's a simple code example to illustrate commands: ```csharp public class LightSwitch { public void TurnOff() { // Code to turn off the light Console.WriteLine("The light has been turned off."); } } class Program { static void Main(string[] args) { LightSwitch lightSwitch = new LightSwitch(); // Sending a command to the LightSwitch lightSwitch.TurnOff(); } } ``` In this C# example, *LightSwitch* is a class that encapsulates the behavior of a light switch. The *TurnOff()* method is an action or command that changes the state, specifically turning off the light. We create an instance of *LightSwitch* and call the *TurnOf()* method on it, effectively sending a command to our "service" to change the state of the system by turning off the light. This is similar to giving a direct command in the real world and waiting for the action to be taken - here represented by the console output indicating that the light has been turned off. With a clear grasp of commands, it's time to explore the other side of the CQS coin: queries. ### The Role of Queries Queries are all about looking things up without making waves. Think of it as wanting to know if someone has registered as a user without affecting their registration status or any other part of the system. It's just gathering information, plain and simple. For example, sending a GET request to /users/{userID}, the API will simply return the details of the user associated with that ID. This operation doesn't change the user's data or affect any other part of the system. It's a straightforward, side-effect-free query of system state that perfectly embodies the concept of a query as outlined earlier in the CQS paradigm. Beyond commands and queries, there's a third critical component in our discussion: events. Let's take a closer look at how events further the goal of component decoupling. ## Using Events for Loose Coupling Events are both a fact and a notification at the same time. Events act as indicators of actions or changes that have occurred. They carry information about these occurrences without anticipating any specific follow-up actions. These events move in a one-way direction, meaning they're sent out without waiting for or expecting any kind of reply—this is often referred to as a "fire and forget" approach. However, a new event might be triggered as a reaction to them, creating a chain of actions initiated by the original event. This approach is powerful because it allows different parts of the system to listen for these announcements and decide independently if they need to respond. For example, if one service in our software system announces that a new user has signed up, other services can listen to that event. An email service might take action by sending a confirmation, while another service that tracks user statistics might update its records. Neither response is directly commanded by the service that announced the new user; they're optional, based on the needs and designs of the listening services. To see these concepts in action, let's examine how they play out in a microservices architecture, utilizing a practical example. ### Practical Example: Microservices and Event-Driven Communication To illustrate the architecture of three microservices with a message broker to facilitate event-driven communication, the following diagram shows the components and the message broker as an intermediary that handles the distribution of messages (events) between services. This setup is typical in a microservices architecture to achieve loose coupling and scalability. ![https://www.plantuml.com/plantuml/png/bP6nQiCm48PtFSMHp7c132KD3LtQ4cewb911Ldximx9axix9KFhkTHoemOL2j0Z1zt_IhzkeUR6-ZSvrFZI-0YrsPoIZ9-5XfyUka-n3lQ0lHyCwV2Za7HMhYJVEgO2SEU18gzK37LwXUfLi9JUG8wrftvUHjORI6ovJ68BJUD5edUTkibthEVxwrrJqfGRX4agMxzTTwSAy3JW7lTVxxLp8RPVJSFZd5vOlNDMsZJqcVDxanZmBa2Pe_huP_OaOysN2_tPSxZarpcsGJkCyVm00](https://www.plantuml.com/plantuml/png/bP6nQiCm48PtFSMHp7c132KD3LtQ4cewb911Ldximx9axix9KFhkTHoemOL2j0Z1zt_IhzkeUR6-ZSvrFZI-0YrsPoIZ9-5XfyUka-n3lQ0lHyCwV2Za7HMhYJVEgO2SEU18gzK37LwXUfLi9JUG8wrftvUHjORI6ovJ68BJUD5edUTkibthEVxwrrJqfGRX4agMxzTTwSAy3JW7lTVxxLp8RPVJSFZd5vOlNDMsZJqcVDxanZmBa2Pe_huP_OaOysN2_tPSxZarpcsGJkCyVm00) Loosely coupled components using events. This setup underlines the role of the message broker in decoupling microservices by mediating event communication. The services do not communicate directly with each other, but instead interact through the message broker using events. This approach increases the flexibility, scalability, and maintainability of the system. While our focus has been on microservices, it's important to note that the principles of commands, queries, and events apply equally to monolithic applications. ### Beyond Microservices: Events in Monolithic Applications It's worth noting that while our discussion has focused on microservices and the use of a message broker to facilitate event-driven communication, these concepts are not exclusive to microservices architectures. You can also apply these principles to decouple components within a monolithic application. In a monolith, different modules or components can still publish and subscribe to events, even if they're part of the same codebase or deployment unit. Using events to communicate between components helps maintain a level of separation and modularity within the monolith. This approach allows individual parts of the application to remain loosely coupled, making the system easier to understand, extend, and maintain. Here a simple example written in C#. ```csharp using System; public class NewUserEventArgs : EventArgs { public string Username { get; set; } } public class UserService { public event EventHandler NewUserRegistered; public void RegisterUser(string username) { Console.WriteLine($"{username} has been registered."); OnNewUserRegistered(new NewUserEventArgs { Username = username }); } protected virtual void OnNewUserRegistered(NewUserEventArgs e) { NewUserRegistered?.Invoke(this, e); } } public class WelcomeEmailService { public void OnNewUserRegistered(object sender, NewUserEventArgs e) { Console.WriteLine($"Sending welcome email to {e.Username}."); } } public class UserStatisticsService { public void OnNewUserRegistered(object sender, NewUserEventArgs e) { Console.WriteLine($"Updating statistics for new user {e.Username}."); } } class Program { static void Main(string[] args) { UserService userService = new UserService(); WelcomeEmailService welcomeEmailService = new WelcomeEmailService(); UserStatisticsService userStatisticsService = new UserStatisticsService(); // Subscribe to the NewUserRegistered event userService.NewUserRegistered += welcomeEmailService.OnNewUserRegistered; userService.NewUserRegistered += userStatisticsService.OnNewUserRegistered; // Register a new user to trigger the event userService.RegisterUser("JohnDoe"); } } ``` As we wrap up our exploration, let's reflect on how the integration of commands, queries, and events shapes the architecture of modern software systems. ## Conclusion Finally, the central role of events in achieving decoupling in software systems is the essence of our discussion. Events stand out as silent facilitators of communication, allowing different parts of a system to remain independent, yet informed about the occurrences that matter to them. This decoupling is crucial, not just for the system's scalability and flexibility but for its overall health and maintainability. The importance of events goes hand in hand with the principle of unidirectional data flow, a concept that ensures information moves in a single, clear direction. This approach simplifies understanding and managing state changes across the system, reducing the likelihood of complex dependencies and unpredictable behaviors. Together, these concepts form a blueprint for designing robust and reliable systems. Using events and one-way data flow changes the game. It makes us rethink how we build and update our software, making everything simpler and smarter. # ### ACR Integration with AKS for Simpler Authentication URL: https://ricofritzsche.me/acr-integration-with-aks-for-simpler-authentication/ Last updated: 2024-03-18T17:07:00.000Z I'm in the midst of fine-tuning my Location Insights/Geofencing API Service. This service is all about offering businesses smart, location-based solutions to boost their efficiency and connect more effectively with customers. With tools for live geofencing, tracking, and spatial analysis, our platform is designed to help companies make informed choices. Kicking things off with a .NET 8 Core app deployed on Azure App Service, I'm now on the path to getting everything production-ready. Part of this process involves weaving in a User Management microservice to streamline how user accounts are handled - making it easier than ever for businesses to sign up, manage accounts, and assign subscriptions without a hitch. With my history as a Software Architect tackling big projects, I've spent a good chunk of time in the world of DevOps. But even with all that experience, I keep running into fresh challenges and lessons. I recently decided to break my API service down into smaller, easier-to-manage pieces, a move that's right in line with the Domain-Driven Design (DDD) way of thinking. This led me to shift over to Kubernetes on Azure (AKS) to get more room to grow and more ways to adapt. Getting Azure set up exactly how I wanted was easy, thanks to some clever automated tricks. However, when it came time to launch the first service, I ran into an old enemy - the *imagePullSecret* issue. It's a problem I've struggled with from time to time in past Kubernetes deployments, and it's always a key test of whether everything is set up correctly. I remember I used the *imagePullSecret* argument in the deplyoment manifest very often, because in most cases the ACR and AKS were in different accounts, as follows. ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: useraccountservice-deployment namespace: ${NAMESPACE} labels: app: useraccountservice spec: replicas: 1 selector: matchLabels: app: useraccountservice template: metadata: labels: app: useraccountservice spec: containers: - name: useraccountservice image: ${ACR_NAME}/useraccountservice:latest ports: - containerPort: 80 resources: requests: memory: "128Mi" cpu: "250m" limits: memory: "256Mi" cpu: "500m" env: - name: ASPNETCORE_ENVIRONMENT value: "Production" imagePullSecrets: - name: acr-secret ``` Using the *imagePullSecret* method for Kubernetes deployments requires manual setup and management of secrets, which can add complexity and introduce security vulnerabilities if not handled properly. Additionally, updating secrets, such as for rotating Docker registry credentials, requires additional steps that can potentially disrupt services if not performed correctly. This approach, while functional, presents challenges from both a security and operational efficiency perspective. To switch to a more efficient method, when using Azure Container Registry (ACR) and Kubernetes (K8s) within the same account, I eliminate manual secrets management. By using the *\--attach-acr* command during or after AKS cluster creation, ACR is directly integrated, eliminating the need for *imagePullSecrets*. ```bash $ az aks update -n MyAKSCluster -g MyResourceGroup --attach-acr ``` This seamless connection simplifies deployments, increases security by automating authentication between AKS and ACR, and streamlines the process, allowing me to focus on improving service with the assurance of simplicity and increased security in deployments. In summary, the integration between Azure Container Registry (ACR) and Azure Kubernetes Service (AKS) simplifies the deployment of container images and reduces the operational and security complexities associated with manual secret management. This enhancement not only strengthens security, but also streamlines the deployment process, allowing developers to focus more on refining their services. If you need help with CI/CD pipelines with GitHub and Azure and are looking for guidance or support, feel free to reach out. My experience and insights may provide the help you need to navigate these processes more efficiently. You can reach me on LinkedIn: [https://www.linkedin.com/in/ricofritzsche/](https://www.linkedin.com/in/ricofritzsche/?ref=ricofritzsche.me) ### Why Ignoring Domain-Driven Design Is Planning to Fail URL: https://ricofritzsche.me/why-ignoring-domain-driven-design-is-planning-to-fail/ Last updated: 2024-03-11T17:33:58.000Z In my three decades in software development, I have seen countless technologies come and go, development paradigms change, and the complexity of business applications increase exponentially. With this in mind, I would like to share my belief that Domain-Driven Design (DDD) is not just an option, but a necessity to properly implement modern business applications. The most common misconception I have come across is the underestimation of the inherent complexity of software projects. This miscalculation leads to an exponential growth of complexity that eventually becomes unmanageable. I have often been called into projects that were already stumbling, recognizable by noticeably slow progress and a resulting inflexibility in making necessary changes. The first question managers often ask in such situations is whether adding more developers will solve the problem — a question I usually answer in the negative. The real problem lies not in the amount of resources, but in the clarity of communication between all parties involved, and in the understanding of the development process itself. This insight is particularly relevant to the development of critical business applications, as opposed to simpler projects such as websites or pre-built CMS systems. A recurring problem in many organizations is focusing on the wrong things: Data, structures, and, unfortunately, often with too much confidence, specific technologies instead of behavior. This focus leads to a distorted vision and too much ambiguity. People often know how they want to solve a problem, but not exactly what the problem is. This dilemma costs companies and organizations a lot of money without ever achieving any real goals. It’s 2024, and it seems that principles like continuous delivery are still not fully practiced. Many are chasing the “big bang,” the final completion, rather than maintaining a continuous delivery process. At the heart of the problem is a lack of commitment to the core domain. This leads to time, energy, and money being wasted on peripheral issues instead of focusing on the valuable aspects of the software, which in turn prevents continuous delivery. This is where strategic design in the context of Domain-Driven Design comes into play. DDD forces us to dive deep into the matter and understand what really happens in a business application. An essential tool in this process is Event Storming. This method brings together all relevant stakeholders to discuss the relevant domain events, far away from any technology. All that is needed is a (virtual) room, enough sticky notes in different colors and the right people. Event Storming is a cost-effective method for recognizing complexity at an early stage and identifying all relevant events in the business process. ![](https://cdn-images-1.medium.com/max/1600/1*5rEEy_uMZVpt4J9E9vGL9w.png) Simple Event Storming Example from a Multi-Tenant SaaS project. This deep engagement with the domain allows the system to be understood as a dynamic process, rather than being reduced to a stupid, static repository of data around which logic is woven in a makeshift fashion. Involving all stakeholders, including the developers, in this process is essential to the success of a project. The results of this effort are far-reaching: a clear understanding of the [core domain and subdomains](https://medium.com/gitconnected/what-means-domain-in-the-context-of-domain-driven-design-6d604685f5ca?ref=ricofritzsche.me) within the project, identification of the areas that require the most attention and investment, and efficient interface design based on behavior and process rather than individual preferences or technology considerations. This approach allows us to unlock the true value of a software solution and ensure that we focus resources on the aspects that have the greatest impact on business objectives. Another key benefit of Domain-Driven Design is that it fosters collaboration between technical teams and business experts. Using a common language, known as a [Ubiquitous Language](https://medium.com/gitconnected/domain-driven-design-the-critical-need-for-ubiquitous-language-fef301272d48?ref=ricofritzsche.me), simplifies communication across the project team. This concept ensures that everyone involved — from developers to product managers — uses the same terminology, which reduces misunderstandings and increases efficiency. Establishing the ubiquitous language is a critical step in ensuring that technical solutions accurately reflect the domain model. DDD also places a strong focus on modeling and designing software that is closely aligned with business requirements. The concept of bounded contexts clearly defines the scope of the application, which helps manage complexity and enables the development of focused and modular systems. This modularity is critical to the maintainability and scalability of software projects and allows teams to work more independently, increasing the speed and agility of software development. It is important to note that using DDD does not mean that every problem should be solved using a DDD methodology. Rather, it is about developing the awareness and skills to recognize when this approach makes sense. In my experience, the projects that benefit most from DDD are those that benefit from a deep understanding of the business domain. The challenges of modern software development are not trivial. Technologies are constantly changing, user requirements are becoming more complex, and the pressure to deliver quickly has never let up. A clear, structured approach that focuses on how a system behaves rather than how data is structured becomes essential. Domain-Driven Design provides such a framework and enables us to create robust, flexible and business-oriented software solutions. Finally, I would like to emphasize that my commitment to DDD and Event Storming does not come from a preference for specific technologies or methodologies, but from the practical experience that these approaches lead to better, more sustainable results. The true power of software development lies not in mastering the latest frameworks or programming languages, but in understanding the business processes we are trying to digitize. DDD is a tool in our arsenal that helps us achieve this goal. It allows us to bridge the gap between the technical and business worlds to create solutions that are not only technically excellent, but also deeply rooted in the core business. *Cheers*! ### Software Development: What on Earth Are We Doing? URL: https://ricofritzsche.me/software-development-what-on-earth-are-we-doing/ Last updated: 2024-03-10T16:03:06.000Z Recently, my projects and discussions have brought me face to face with the question: What is software development? This topic is currently occupying my thoughts. Many people think they understand what it is, but in reality the perception is often biased. I find Alberto Brandolini's quote particularly applicable: > Few people have done more harm to software development as a profession than those advocating that “software development is like building a house”. I think this quote highlights the detrimental effects of oversimplifying the software development process by comparing it to building a house. First and foremost, the comparison of software development to building a house is misleading and damaging. Unlike construction, which often follows a linear and predictable path, software development is inherently iterative and adaptive. This misconception stems from a fundamental misunderstanding of the creative and exploratory nature of software development. It's about solving problems in unique contexts, not just assembling predefined components from a static blueprint. Unlike building a house, which usually follows a linear, well-defined plan with a definite end, software development involves continuous adaptation, evolution, and iteration based on changing requirements, technological advances, and user feedback. The quote underscores the importance of recognizing and appreciating the unique challenges and methods of software development in order to avoid underestimating its requirements and intricacies. But it seems that developing software is only equated with writing code. However, the understanding of what goes into developing a software product is often lacking. It's not uncommon for people or organizations to decide up front for instance that they need a mobile app, when a quick review might reveal that using services with a small web app would suffice. I have observed that developers or offshore providers are often hired to implement a product without having any connection to the domain. The requirements are unclear and require intensive involvement. The consequences of implementing without considering operations, environment and costs, or availability and performance requirements are often overlooked. Nasty surprises arise when a cool product idea is crammed into a CMS like WordPress due to ignorance, lack of communication or not asking the right questions. On the other hand, large enterprises see developers as individuals who are familiar with frameworks, know a programming language, and can poorly translate defined requirements into technology. They seek resources for their projects under this guise, believing that they need a Java developer with experience in their industry. To this, I say absolutely no! Why? Because they need someone who understands coding as a craft. That's only the foundation, but what they really need is someone who is willing to engage with the domain, ask critical questions, and develop a shared understanding of the domain and the product. Writing code is actually the smallest part. Software development is about continuous learning, a fact that is unfortunately often forgotten. When Domain-Driven Design is applied, or at least attempted, it often boils down to mere technical concepts. I believe that strategic design is far more important. For example, event storming is great for understanding your domain and defining bounded contexts and subdomains. This makes it easier for everyone to communicate, and you can tell early on if developers are willing to engage with the domain, rather than just mechanically process tasks. If you're considering using microservices, event storming can help you define the boundaries of your services. I believe that the role of the "unthinking programmer" who simply translates business requirements into code will struggle to compete with new technologies like AI. As we look to the future, the role of the developer is undoubtedly changing. The rise of AI and other technologies is challenging the traditional view of the developer as a mere programmer. Instead, the ability to think critically, learn continuously, and collaborate effectively is becoming increasingly important. These skills enable developers to make informed decisions, rethink and revise their approaches, and navigate the complexities of modern software development. Software development is about making decisions and being prepared to take a step back and revise. It's a process, a learning process. And above all, it's about communication and collaboration with stakeholders. In conclusion, my experiences and reflections underscore a fundamental truth: software development is much more than writing code. It's a complex, nuanced process that requires critical thinking, continuous learning, and collaboration. As we move forward, embracing these aspects will be critical to creating software that truly meets user needs and stands the test of time. ### Vertical Slices in a Nutshell URL: https://ricofritzsche.me/applying-the-vertical-slice-architectural-pattern-in-a-modern-net-8-web-api/ Last updated: 2024-03-04T22:50:42.000Z Over the past few months, I've taken a new direction in how I build my applications. I decided to try something called the Vertical Slice Architectural pattern. At first it was just an experiment, something new to learn. But now, after using it for a while, I've found it to be incredibly efficient. In this blog post, I want to share my journey with this approach, what it is, and why it might just change the way you think about building applications, too. ## Think in Simple Interactions At the heart of every human-machine interaction is a simple yet varied pattern: a request is made, it's processed in some way, and then a response is given. While this may seem like a broad description, it actually gets to the core of how we design our software systems. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2024/03/1-ABsu0HdwAin7EcxwcjH0Kg.webp) Think of this whole process -from receiving a request to providing a response- as a single task. Looking at it this way not only helps us stay organized, it also makes complex operations easier to understand. In each of these transactions or pieces of work, there's an input (the request), something that handles the processing (the workhorse of the operation), and an output (the response). This structure helps us keep different aspects of our systems well-defined and ensures that they are both flexible and easy to adapt. The idea is simple: organize the code into separated feature slices, with each slice containing a query or command, its handler, and the resulting response. The main objective is having high cohesion within each slice while minimizing coupling between slices. This approach proved quite effective for several aspects of the application architecture. By taking a vertical view of the system, we increase its adaptability and minimize inter-dependencies. This approach allows us to select the most appropriate strategy or pattern for each specific scenario without affecting the implementation of existing scenarios. ## High Cohesion within Feature Slices Let's take a closer look at how I implemented the vertical slice architecture in my Location Services API as an example. The goal was to achieve a high level of cohesion within the slice, but to look at the slice in a very isolated way, which means that I packed all aspects of a feature, e.g. *Register Customer*, into a slice, as well as the API endpoint itself. So I considered the endpoints to be integral parts of their slices and made sure that they were placed within those slices to maintain coherence. I've leveraged the powerful capabilities of .NET API controllers to reduce unnecessary code. I avoid traditional, overly complex controller classes - the kind that turn a "thing" like a fat, central *CustomerController* into a massive beast. ```csharp [ApiController] [Route("api/v1/tenants/")] public class RegisterCustomerEndpoint( ISender mediator, ILogger logger) : ControllerBase { [HttpPost("{tenantId}/customers")] public async Task Post([FromBody] CustomerRegistrationModel model, Guid tenantId) { try { var response = await mediator.Send(new RegisterCustomerCommand( tenantId, model.CustomerId, model.DeviceId, model.Name)); return CreatedAtAction(nameof(Post), response); } catch (CustomerAlreadyExistsException alreadyExistsException) { return Conflict(alreadyExistsException.Message); } catch (Exception exception) { logger. LogError(exception.Message); return Problem(statusCode: 500); } } } ``` So I decided that the controller should be placed directly in the appropriate feature slice folder and represent exactly one specific API endpoint. The *RegisterCustomerEndpoint* class, derived from *ControllerBase*, contains all the necessary annotations and routing information. The endpoint is part of the slice, reinforcing the concept of cohesion. Integrating endpoints directly into slices simplifies how to manage API endpoints. Now, each slice is like a little package that includes everything it needs - its endpoint, the logic to make it work, and how it handles data. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/2024/03/1-LVq4hv5AHu1Hht67wXHJ_A.webp) This approach also makes the code more cohesive. It's much easier to deal with and understand because all the parts that work together for a specific feature are kept together. Using annotations in C# cuts down a lot of repetitive code. The code will be cleaner and simpler because you no longer need to manually add each endpoint to a router class. I hope my insights inspire you to experiment with the Vertical Slice approach. ### What Are Problem Space and Solution Space in Domain-Driven Design? URL: https://ricofritzsche.me/what-are-problem-space-and-solution-space-in-domain-driven-design/ Last updated: 2024-03-04T11:57:24.000Z In my previous article on DDD titled [“What Means Domain in the Context of Domain-Driven Design?”](https://ricofritzsche.me/what-means-domain-in-the-context-of-domain-driven-design/), we explored the essential elements of DDD, specifically focusing on the importance of understanding domains, core and subdomains, and bounded contexts. This foundational knowledge is critical when applying what we call Strategic Design in DDD, a method that helps to streamline how we approach complex systems. Strategic Design in the context of Domain-Driven Design is a high-level approach that guides the organization and structure of a software system. Rather than diving straight into coding and implementation details, Strategic Design encourages you to first understand the larger business domain. It helps you identify the various subdomains and bounded contexts, how they interact, and what is core to the business. By doing so, Strategic Design enables you to make informed decisions about where to focus your efforts, ensuring that the software aligns well with the business needs and can evolve more easily over time. Today, we’re taking the next logical step. We’re going to talk about problem and solution spaces within DDD and delve into the topic of context maps. These elements are crucial for the practical application of Domain-Driven Design and will help you navigate the complexities of real-world projects more effectively. So, let’s get to it. ### Problem Space and Solution Space When learning Domain-Driven Design, it’s crucial to distinguish between two key areas: the problem space and the solution space. The problem space deals with identifying what business challenges you’re trying to solve and why they matter. The solution space focuses on how you’re going to solve those challenges through software implementation. #### The Problem Space In DDD, the problem space is essentially the landscape of business issues that you aim to tackle. Think of it as the terrain you need to navigate to deliver a new core domain effectively. In our “sell books online” example from the previous article, the problem space might involve challenges like optimizing inventory management, streamlining the user interface for better customer experience, or enhancing the recommendation engine for increased sales. Evaluating the problem space means looking at existing subdomains as well as identifying new ones that need to be developed. Here, subdomains act as building blocks for your core domain. For instance, in our online bookstore, the subdomains could be Catalog, Orders, and Invoicing, as already discussed in my previous DDD article. The problem space is a combination of the Core Domain and Subdomains. That means that your central business objective (selling books online, in this case) can’t exist in isolation. It’s inherently tied to various sub-aspects like Invoicing, Shipping, and Orders, each a subdomain in its own right. Furthermore, subdomains often differ from one project to another because they serve to address current strategic business problems. In a different project, for example, the focus might shift from inventory management to global expansion and currency handling. #### The Solution Space This is where Bounded Contexts come into play. Once we have a clear understanding of the problem space, we move on to the solution space. This is where bounded contexts become essential. Each bounded context is a specific software model tailored to address elements within your problem space. In the context of our online bookstore, one bounded context might deal exclusively with inventory management, ensuring that books are in stock and properly cataloged. Another might focus solely on the user interface, working to provide the customers with an intuitive and engaging shopping experience. Each of these bounded contexts is a specialized solution aimed at solving a specific problem within the larger domain. #### Understanding the Vision Understanding the vision and goals for your core domain is paramount. If you don’t fully grasp these, along with the subdomains that support them, you’re missing a crucial piece of the puzzle. Without this understanding, it’s challenging to strategically leverage these elements to your advantage or steer clear of potential pitfalls. Therefore, while your initial assessment of the problem space should remain **high-level**, it should also be comprehensive. This means you don’t need to dive into nitty-gritty details just yet, but you do need a broad understanding that covers all the bases. Ensure that all stakeholders — whether they’re on the business side or the technical side — are on the same page. Alignment and commitment across the board are key to successfully realizing the vision you’ve set for your domain. ### Intersections Between Subdomains and Bounded Contexts One of the most critical aspects to grasp in DDD is the intersection between subdomains in the problem space and bounded contexts in the solution space. The reason this is crucial is because it gives us the lens through which we can translate business challenges into actionable software solutions. #### The Ideal Scenario: One-to-One Alignment In a perfect world, each subdomain would align perfectly with a single bounded context. Such a one-to-one alignment elegantly compartmentalizes your domain models into well-defined areas, based on business objectives. This makes it easier to manage, both from a business and technical perspective. In our “sell books online” example, let’s say our core domain is the Book Catalog. Ideally, the Book Catalog subdomain would align with a single bounded context dedicated solely to cataloging books — everything from managing inventory to categorizing genres to recommending reads. In the same vein, the Orders subdomain would align with a bounded context that takes care of everything order-related, like creation, tracking, and fulfillment. ![](https://cdn-images-1.medium.com/max/1600/1*R8CGhSU_kn8s4DbxdFdwtQ.png) Ideal One-to-One Alignment of Subdomains and Bounded Contexts in an Online Bookstore #### Reality Check: Intersections and Overlaps However, it’s worth acknowledging that we don’t always have the luxury of operating in ideal scenarios, particularly when dealing with legacy systems or when your architecture has grown into a “Big Ball of Mud”. In such cases, one subdomain might intersect multiple bounded contexts, or multiple bounded contexts might serve a single subdomain. The term “Big Ball of Mud” refers to a software system that lacks a discernible architecture and has grown in complexity over time, making it challenging to manage or modify. It often results from organic growth and the absence of best practices in software development. For instance, consider a complex legacy system in our online bookstore. The Orders subdomain might be splintered across multiple bounded contexts due to years of organic growth and patches. One bounded context might handle order creation, another order tracking, and yet another might manage returns and refunds. These bounded contexts might not be neatly contained but may intersect in various ways. ![](https://cdn-images-1.medium.com/max/1600/1*y78_JT9AEizxMLqF160YMQ.png) Complex Scenario of Subdomains and Bounded Contexts in an Online Bookstore Conversely, our invoicing subdomain might consist of two bounded contexts: one for creating invoices and one for tax calculations. Both are part of the invoicing subdomain, but complex enough to merit their own bounded contexts. #### The Process of Evolution DDD is not a “do-it-once-and-forget-it” methodology. As your business grows and evolves, so will your subdomains and bounded contexts. What was once a one-to-one relationship may turn into an intricate web of intersecting bounded contexts and subdomains. This is perfectly normal and should be managed rather than avoided. #### Brief Summary Understanding the intersections between subdomains and bounded contexts is vital for successful DDD implementation. While a one-to-one alignment is the ideal, real-world scenarios often present us with complexities that require careful thought and flexible design. No matter how carefully we stake out our contexts or define our sub-domains, it is the intersection of these two areas that really determines how well your software meets your business goals. ### Wrapping Up In today’s discussion, we demystified this complexity by deeply diving into the mechanics of subdomains and bounded contexts. We used the online bookstore as a vivid example to explore what problem space and solution space mean. This breakdown not only helps you understand the broader structure of your domain, but also helps you manage its individual components more effectively. As you tackle your own domain-driven projects, take the time to identify your problem space rigorously. Use that knowledge as a cornerstone to carve out your bounded contexts in the solution space, keeping in mind that real-world challenges might not always allow for a textbook-perfect mapping between subdomains and bounded contexts. ### What Means Domain in the Context of Domain-Driven Design? URL: https://ricofritzsche.me/what-means-domain-in-the-context-of-domain-driven-design/ Last updated: 2024-04-03T09:48:32.000Z Over a decade ago, I embarked on a journey into the exciting world of Domain-Driven Design (DDD). In the past, I even had the opportunity to write a column about it for VSOne magazine. I shared my hands-on experiences and insights into developing complex business applications with C# .NET. Today, I revisit that passion by focusing on the core principles of DDD, starting with the most basic but often misunderstood concept: the “domain.” In this article, you’ll learn what a “domain” means in the context of DDD, how it is structured, and why understanding it is critical to the success of any DDD project. We will explore core, supporting, and generic subdomains using a simple online bookstore as an illustrative example. Once you understand these basics, you will be better equipped to tackle complex business problems in an organized and effective manner. ## What Does “Domain” Really Mean? When we talk about Domain-Driven Design, or DDD, the term “domain” is often thrown around a lot. But what does it actually mean? If you’ve been developing software for any length of time, you’ve probably heard people talk about “the domain” as if it were some mystical entity that everyone should know about. Let’s clear the air and get to the heart of what “domain” really means, especially in the context of DDD. ### The Bird’s-Eye View of a Domain At a high level, a domain is essentially the sphere in which an organization operates. It’s what the business does and the environment it does it in. Think of it like this: if you run an online bookstore, your domain involves selling books over the internet. Simple, right? But here’s where we need to pause and make sure we don’t get carried away. Often, people hear the term “Domain Model” and assume that we’re supposed to create a grand, unified model that encapsulates every single aspect of the organization. That’s a misunderstanding. In fact, DDD advises against it. ### Domains Are Complex Beasts Here’s the truth: an organization’s domain isn’t a monolithic thing; it’s more like a complex ecosystem made up of various subdomains. These subdomains are smaller, specialized areas within the larger domain that the organization operates in. So, in our online bookstore example, while the overarching domain is “selling books online,” the subdomains might include Book Catalog, Orders, Invoicing, and Shipping. ![](https://cdn-images-1.medium.com/max/800/1*dJGeYoD3HHDNjWGr2VDYTw.png) “Selling books online” Domain with Subdomains ### Why Subdomains Matter in DDD DDD isn’t about creating one massive model that tries to encapsulate this entire ecosystem. That would be overwhelming and, frankly, not very useful. Instead, DDD encourages us to focus on these individual subdomains and create bounded contexts around them. In layman’s terms, a bounded context is a boundary within which a particular model is defined and applicable. You wouldn’t use the same rules or logic for shipping as you would for product management, would you? They are different sub-areas, each with their own concerns and complexities. For those who want to go deeper, I’ve already written an in-depth article on bounded contexts, which you can read [here](https://medium.com/gitconnected/bounded-context-in-domain-driven-design-a-practical-guide-c1f9192ac93d?ref=ricofritzsche.me). ### The Size of the Organization Doesn’t Matter You might be wondering, “What if my company is just a startup? Do I still need to think in terms of subdomains and bounded contexts?” The answer is a resounding yes. Whether your software serves five people or five million, breaking down the domain into manageable subdomains is critical for tackling complexity and delivering real business value. > So, when you hear the term “domain,” don’t get lost in the abstraction. Think of it as the overarching sphere of what a business does, but remember that this sphere is composed of many smaller, specialized circles — subdomains. By understanding this, you set the stage for effective Domain-Driven Design, focusing on what really matters to deliver solutions that work in the real world. ## The Importance of the Core Domain When working with Domain-Driven Design (DDD), understanding your Core Domain is essential — it’s the main area that your business needs to get right to succeed. So, what makes a Core Domain so crucial, and how does it differ from other subdomains? The Core Domain is the most important part of your business; it’s where you must excel to achieve your goals. When you’re investing your time and resources into a DDD project, the Core Domain is what you’re focusing on. ### Revisiting the Bookstore Example Let’s go back to our online bookstore example from Chapter 1\. In that scenario, the Core Domain could be the Book Catalog subdomain. Why? Because having an easy-to-use and comprehensive catalog is essential for attracting and retaining customers. If the catalog isn’t user-friendly, people won’t stay on your site, and they certainly won’t buy anything. ### The Role of Supporting Subdomains Supporting Subdomains are also critical, but they’re not the main focus. These subdomains add specific features or qualities to your business that set it apart but aren’t the primary reason the business exists. In our bookstore case, the Order subdomain might be considered a Supporting Subdomain. A streamlined and efficient ordering process adds value to the customer experience, encouraging repeat visits. ### Understanding Generic Subdomains Finally, there are Generic Subdomains. These are elements of your business that are needed for operations but aren’t unique to what you do. They are functionalities that any similar business would require. In the context of our online bookstore example, Invoicing and Shipping could be Generic Subdomains. These operations are essential, but they’re not where you’re going to differentiate yourself from the competition. ![](https://cdn-images-1.medium.com/max/800/1*R8CGhSU_kn8s4DbxdFdwtQ.png) “Selling books online” business Domain with Subdomains and Bounded Contexts ### All Subdomains Have Their Place Even though we categorize subdomains as Core, Supporting, or Generic, it doesn’t mean any are unimportant. Each has a specific role to play in the overall success of the business. While you’ll invest most of your innovative energy in the Core Domain, the Supporting and Generic Subdomains still need to function well to keep everything running smoothly. > In essence, your Core Domain is where you focus most of your efforts and creativity. Supporting Subdomains add unique features that help differentiate your business, while Generic Subdomains provide the essential functionalities that keep your business operational. By understanding the role of each, you can allocate resources more effectively and focus on what truly matters for your business. ## Summary We’ve navigated the multifaceted concept of “domain” and its subdomains — Core, Supporting, and Generic — in the context of Domain-Driven Design. I hope this discussion has provided clarity and reinforced the importance of pinpointing where your organization should focus its efforts. Remember, understanding your domain isn’t just an academic exercise; it has real-world implications for resource allocation, innovation, and ultimately, your business’s success. In the next episode of this DDD series, we’re going to take these principles and apply them to real-world scenarios. We’ll go into the problem space and solution space, dissecting how they interact and influence your domain and subdomains. It promises to be an engaging discussion that will further enrich your DDD toolkit. Thank you for joining me on this journey into the essence of Domain-Driven Design. *Cheers*! ### RESTful Principles in the Age of Microservices URL: https://ricofritzsche.me/decoding-the-synergy-how-microservices-and-restful-apis-power-modern-software-development/ Last updated: 2024-10-12T16:51:17.000Z ### Back in 2017, I was on stage at the code.talks conference in Hamburg and shared my experiences of implementing microservices with Nodejs in real-world customer projects. The interest in this topic and the many questions at the end of the session were overwhelming. Today, the landscape has evolved significantly. Microservices are not a one-size-fits-all solution, but their impact is undeniable. They have introduced monitoring tools and patterns for debugging in a distributed environment, improved our knowledge of APIs, and paved the way for the rise of containers and serverless computing. Gone are the days when a tiny code change could jeopardize the entire application. Remember the lengthy and tedious deployments of even the smallest changes? Such challenges have driven our pursuit of excellence. Throughout my time in software development, topics like automation in deployment, readable and maintainable code, but also reduction of complexity were my main topics that drove me to constant development and improvement. Today, I’ll take an in-depth look at microservices and RESTful APIs. These vanguards of modern development promise scalability, agility, and faster deployments. Let’s go! ![](https://cdn-images-1.medium.com/max/800/1*cg5xLl1q51rF7VKo9o52pA.jpeg) ### What are Microservices? Saying it simple, microservices are about decomposing applications into small services that run independently. But what exactly does it mean? Sam Newman, a leading voice in this space, beautifully summarizes it as: > Small autonomous services that work together, modeled around a business domain. ![](https://cdn-images-1.medium.com/max/800/1*MpkRivLDQHEE4nBYD4IsHw.png) Rico Fritzsche — code.talks Hamburg 2017 Breaking this down: - **Small & Autonomous:** Microservices are designed to be concise. Each service does one thing and does it well. Being autonomous means they operate independently, reducing dependencies and ensuring that a hiccup in one service doesn’t bring the whole system down. - **Work Together:** While each service stands alone, they aren’t isolated. They collaboratively achieve larger goals by communicating with each other. Think of them as players in an orchestra, each playing their part, but collectively creating a symphony. - **Modeled Around a Business Domain:** This is the essence of microservices. Instead of structuring around technical capabilities (like databases or frontend), they’re structured around business functionality — like billing, user management, or inventory. This makes them aligned closely with business needs, ensuring that changes in business strategy can be quickly reflected in the software. **In a nutshell, microservices take the complexities of modern software requirements and break them into manageable, independent units that collaboratively deliver value.** ### What are RESTful APIs? If you've ventured into web development, you've likely encountered RESTful APIs. But what exactly are they? REST stands for Representational State Transfer, while API means Application Programming Interface. In essence, a RESTful API is a method for software systems to communicate using REST principles. Developed by Roy Fielding, this concept has become a popular approach for designing web interactions. RESTful APIs act as interpreters, enabling various technological components to understand each other, thus enhancing our online experiences. Let's delve into the guiding principles of RESTful Web Services' architectural style. Read also [What is an API](https://www.bluvolve.com/what-is-an-api?ref=ricofritzsche.me)? ### Architectural Constraints of RESTful API When we talk about RESTful APIs, it’s essential to recognize the foundation upon which they’re built: the five architectural constraints. These aren’t just arbitrary rules; they’re pivotal guidelines that make a web service “RESTful.” Let’s break each one down: #### Uniform Interface Having a consistent interface is like having a universal remote for all your devices. It simplifies interactions for both the client and server, ensuring that no matter which resource is being accessed, the method of interaction remains the same. This predictability fosters stability and streamlines the integration of different services. #### Stateless In RESTful services, each request from a client contains all the information the server needs to understand and process that request. Think of it as sending a letter with all details every time, rather than assuming the recipient remembers past letters. This approach ensures that no client information is stored between requests, making interactions independent and straightforward. #### Cacheable Speed and efficiency are crucial in the digital world. When data is labeled as “cacheable,” it means the client can store that response for a set period. By doing this, not every request needs to go all the way to the server. Some can be handled locally from the cache, speeding up response times and reducing server load. #### Client-Server The essence of this constraint is the separation of concerns. The client handles the user interface and user experience, while the server manages the data. This separation allows both the client and server to develop and scale independently. It’s like having a cook specialized in preparing food and a waiter specialized in serving it, each honing their particular skill. #### Layered System Imagine a multi-layered cake. You see the top layer, but beneath are multiple layers, each with its role. In REST, a client doesn’t need to know about the intricate layers of an API — it interacts with the top layer. This abstraction promotes security, as internal layers remain hidden, and also facilitates modularity and scalability. In essence, these five constraints aren’t just checkboxes for an API to be deemed “RESTful.” They’re vital principles shaping the efficiency, scalability, and flexibility of RESTful services, ensuring they’re robust and future-ready. ### Microservices in the Context of RESTful APIs Microservices, by nature, align seamlessly with RESTful APIs. Both emphasize decoupled, scalable, and flexible systems. But when combined, they create a formidable partnership, addressing challenges in modern software development. Let’s discuss how microservices derive value when integrated with RESTful principles. #### Decoupled Design Microservices’ core principle is creating small, autonomous services that center around specific business functionalities. RESTful APIs promote a similar mindset with its client-server constraint: the client focuses on the user interface, and the server deals with data processing. When we combine these approaches, we get systems where changes in one service (or its interface) don’t ripple and cause disruptions across the board. The result? A more agile and resilient application. #### Scalability Both RESTful APIs and microservices address scalability but from slightly different angles. While microservices can be independently scaled based on their specific loads, RESTful principles like statelessness ensure that every request is treated as a new one, without relying on stored sessions. This makes scaling out more manageable, as incoming requests can be directed to any available instance of a service. #### Easier Troubleshooting RESTful APIs emphasize a consistent and uniform interface. When paired with microservices, this means each service offers a predictable interaction model. When issues arise, developers don’t have to grapple with disparate communication styles across services. They know what to expect, simplifying debugging and issue resolution. #### Improved User Experience Combining the modular nature of microservices with the caching constraint of RESTful APIs can significantly improve the user experience. Data that is accessed frequently can be cached on the client side, making applications faster and more responsive. At the same time, the modular architecture ensures that only the needed services are invoked, conserving resources. #### Flexibility in Development RESTful principles aren't tied to specific technologies. When combined with microservices, you have a system where each service can be written in the language best suited for its functionality. Need a service for heavy data processing? C#, Spring Boot, or Rust might be the right choice. Is another service more CRUD-oriented? Python or Node.js could be better suited. This combination encourages using the right tool for the job. ### Conclusion Microservices and RESTful APIs aren't just buzzwords—they complement each other perfectly. Together, they create a system that's scalable, maintainable, and efficient. For developers and enterprises aiming to stay agile and responsive in a dynamic technology landscape, this partnership adds significant value. *Cheers*! ### ### How to Implement Domain-Driven Design: Common Mistakes You Should Avoid. URL: https://ricofritzsche.me/how-to-implement-domain-driven-design-common-mistakes-you-should-avoid/ Last updated: 2024-03-04T13:13:52.000Z In today’s chapter of my Domain-Driven Design journey, I want to dive into how we structure our code. This topic holds a special place for me. You see, when code grows, it can easily become a jumbled mess, making it hard to read, tricky to manage, and tough for anyone to get their head around. And if there’s one thing I’ve learned, it’s that if our code doesn’t mirror the real world, it’s missing the mark. After all, a big part of DDD is about creating models that everyone can understand, from developers to stakeholders. Over the past 15 years, I have been able to apply DDD to a variety of projects and try different approaches. Some were, well, not the best, while others hit the nail on the head. If you’ve been following my writings, you’ll know I’m big on clean code. For me, messy code filled with confusing abbreviations and insider jargon is a total nightmare. It’s something that drives me crazy. ## The Struggle with Technical Structuring: Where We Often Go Wrong To kick things off, let’s delve into a recurring question: Why do developers frequently lean towards structuring their code around technical aspects rather than focusing on the domain? I believe this inclination primarily stems from the lens of an engineer. It’s the way they’ve been trained to view systems. Yet, at its core, Domain-Driven Design seeks to rectify this mismatch. DDD encourages everyone involved — not just developers but all stakeholders — to adopt a shared language and vision for the system. This shared understanding should ideally be mirrored in the way we structure our code, aligning it closely with the domain model. Adding fuel to the fire, numerous tutorials and even some prominent frameworks advocate this technical perspective. A notable example is Microsoft’s ASP.NET MVC framework. For context, here’s a typical ASP.NET MVC folder structure: ```Markup - Controllers - HomeController.cs - AccountController.cs - Models - HomeModel.cs - AccountModel.cs - Views - Home - Index.cshtml - Account - Login.cshtml - Scripts - Content ``` To put it simply: The way we’ve been taught to organize our code often doesn’t make sense when we stop and think about it in practical, everyday terms. ## The Drawbacks of Technical Code Structuring At first glance, slotting aggregates, entities, and value objects into their respective folders might feel clean and organized. It’s like neatly arranging your socks, shirts, and trousers in separate drawers. But let’s peer a little closer and uncover the potential pitfalls. ### The Absence of Real-world Analogies Consider an art museum. If the curators decided to sort paintings not by artist or era, but by the type of paint or canvas used, the experience would be, to put it mildly, jarring. Visitors come to see Van Gogh or Renaissance art, not “oil on canvas” or “watercolors”. The same logic applies to code. Grouping by technical categories often doesn’t align with the intent or the real-world usage of those seeking to understand or modify the system. ### Navigation Becomes Cumbersome Let’s picture our codebase like a library. Over time, as the collection grows, if you had to dig through an “Entities” section hoping to find a single book related to “Banking”, you’d probably wish there was a dedicated “Banking” section instead. ### Issues of Cohesion and Coupling Fragmenting related domain ideas across multiple folders leads to a spiderweb of dependencies and references. This distribution not only makes code navigation a challenge but can also amplify maintenance troubles. Cohesive items that belong together are scattered, and unrelated items might end up too tightly bound. In .NET namespaces, for example, entities that have nothing to do with each other from a technical point of view would then be located in the same namespace. For a clearer picture, let’s look at the conventional structure: ```Markup - Aggregates - BankAccount.cs - UserProfile.cs - Entities - Transaction.cs - UserHistory.cs - ValueObjects - BankAccountNumber.cs - UserPreferences.cs ``` Having understood the limitations of traditional structuring, it’s time to pivot our gaze towards an approach that’s not just technically sound but also intuitively aligned with how we think and operate in the real world. ## Prefer a Feature-based Structure Consider the following folder structure: ```Markup - BankAccount - Account.cs - Transaction.cs - BankAccountNumber.cs - Profile - UserProfile.cs - UserHistory.cs - UserPreferences.cs ``` Now, this isn’t just a different way to arrange your code — it’s a paradigm shift that brings with it a slew of advantages: ### Natural Cohesion With this structure, everything related to a particular concept, say ‘BankAccount’, sits together. Just like chapters in a book, you get a comprehensive, start-to-finish view of the domain. All intricacies of the BankAccount domain — its entities, value objects, and other nuances — are nestled together, making comprehension a smoother experience. ### True Encapsulation One of DDD’s pillars is the emphasis on aggregates maintaining consistency. By viewing the aggregate as a container of sorts and aligning closely associated entities within the same folder, this vision of encapsulation gets amplified. It’s akin to viewing a family portrait where every member, despite their distinct roles, is seen as part of a single unit. ### Swift Navigation Imagine trying to put together a puzzle with all the pieces that belong together lined up. That’s exactly how this structure feels. Want to learn everything about “Profile”? Just switch to the appropriate folder. No more digging through separate entity or value object folders. ### Flexibility with Evolving Features As your application evolves, its functions change. With a function-based structure, extending or optimizing these functions is simplified. You want to introduce a new aspect in “Profile”? Just add it to the “Profile” folder. This approach ensures that the encapsulation and cohesion of your features is maintained as they evolve. Take a closer look and you’ll see that the feature-based approach isn’t just about cleanliness. It’s about synchronizing your code with real-world logic, making maintenance, extension, and collaboration more intuitive and less cumbersome. ## Wrapping it up Finally, let’s take a step back and think about our daily lives. If you separated all the square from all the round, and stored all the things in your home by color but not function, that would be pretty weird, right? Just as we organize our homes to make our daily routines run more smoothly, our code deserves the same thoughtful arrangement. Choosing a feature-based structure is similar to having a well-organized closet where everything you need for a particular occasion is together. It’s about making our lives (and the lives of those who work on the code after us) a little easier, more intuitive, and, dare I say it, more enjoyable. Remember, programming isn’t just about making sure machines understand us. It’s also about making sure that those around us can still follow our thought processes months or even years later. So let’s say goodbye to labyrinthine, technical structures and turn to a more natural, domain-specific approach. *Cheers*! ### Integrating OpenAI's GPT with .NET Core: Building AI-Powered C# Applications URL: https://ricofritzsche.me/integrating-openai-gpt-with-net-core-building-ai-powered-c-applications/ Last updated: 2024-03-04T13:27:13.000Z ### Harness the Power of Artificial Intelligence in .NET Core with OpenAI's Language Models. Do you recall the amazement we felt hearing the modem's dial-up sounds, attempting our first connection to the Internet? Now, two decades later, we're not just talking about faster speeds, but an entirely new realm of possibilities with Artificial Intelligence. Let's explore how to integrate OpenAI into .NET Core apps. In this post, we’ll delve into one of the most compelling offerings in the Artificial Intelligence realm: OpenAI’s *gpt-3.5-turbo*, a highly advanced language model that's making substantial strides in natural language understanding and generation. While the fourth iteration, GPT-4, offers even more capabilities, it requires a paid account with OpenAI. As such, we'll focus on *gpt-3.5-turbo* to ensure that all developers, regardless of their OpenAI account status, can follow along and integrate this language model into their applications. Despite the model's complexity, integrating it into .NET Core using C# is straightforward, especially via the OpenAI REST API. While there are numerous packages available that simplify the integration of the OpenAI API into .NET Core applications, it’s crucial to first understand the underlying principles of how this interaction works. This blog post intentionally avoids the use of these pre-packaged solutions. By building from the ground up, you'll grasp the dotnetcore integration intricacies with the OpenAI API, understanding request construction and response processing. This hands-on approach will not only help you appreciate the intricacies involved but also equip you with the knowledge to troubleshoot issues, optimize performance, and adapt to changes in the API over time. ### Set up the .NET Core console app We begin our journey to integrate artificial intelligence into C# applications using the OpenAI API. It's a snap to access the OpenAI API with .NET Core 7, and today I'll walk you through this straightforward process in detail. If you're new to .NET and need some guidance on setting it up, check out my beginner-friendly guide [here](https://ricofritzsche.de/blog/setup-secure-dotnet-core-console-app-api?ref=ricofritzsche.me). To prepare your console app for the following OpenAI API integration example, execute the following commands. ##### Create new console app ```Bash $ dotnet new console -o openai-csharp-example $ cd openai-csharp-example ``` ##### Add required dependencies ```Bash $ dotnet add package Newtonsoft.Json $ dotnet add package Microsoft.Extensions.Configuration $ dotnet add package Microsoft.Extensions.Configuration.UserSecrets ``` ## Setting up the OpenAI API To interact with the OpenAI API, you’ll need an API key. This key is a unique identifier that grants your application permission to access the API services. You can acquire this key by registering an account on the OpenAI platform and navigating to the ‘API Keys’ section in your account settings. Once you have your API key, it’s time to integrate it into your application. The key is typically added as a field in a class specifically built for interacting with the OpenAI API, often referred to as the service class. This class not only holds the API key but also encompasses the methods for sending requests and handling responses from the API. Then initialize the Secret Manager and add your OpenAI API key to the Secret Manager as follows: ```bash $ dotnet user-secrets init $ dotnet user-secrets set "OpenAI:ApiKey" "your_api_key" ``` A detailed explaination how the Secret Manager works you find [here](https://ricofritzsche.de/blog/setup-secure-dotnet-core-console-app-api?ref=ricofritzsche.me) in detail. ### Create the OpenAI API Service Building a separate service to interact with the OpenAI API allows us to encapsulate the logic related to API requests and responses within a dedicated service class, thereby promoting better code organization, maintainability, and scalability. Let’s create a new class named *OpenAIService* in the root directory of our project. This service class acts as a gateway between our application and the OpenAI API, isolating the specifics of the API interaction such as request formatting and response parsing. It aligns with the principles of good object-oriented design and encapsulation by hiding the details of the API interaction and exposing a method, *SendPromptAndGetResponse()*, which serves as a clean interface to the rest of our application. This method takes a prompt and returns the corresponding response from the API, abstracting away the complexities involved in the process. ```Javascript public class OpenAIService { private readonly HttpClient _httpClient; private readonly string _apiKey; public OpenAIService(HttpClient httpClient, string apiKey) { _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient)); _apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey)); } public async Task SendPromptAndGetResponse(string prompt) { const string requestUri = "https://api.openai.com/v1/chat/completions"; var requestBody = new { temperature = 0.2, model="gpt-3.5-turbo", messages= new [] { new { role = "system", content = "You are a helpful assistant." }, new { role = "user", content = prompt } } }; _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _apiKey); var response = await _httpClient.PostAsync( requestUri, new StringContent(JsonConvert.SerializeObject(requestBody), Encoding.UTF8, "application/json")); response.EnsureSuccessStatusCode(); var responseBody = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); return responseBody.Choices[0].Message.Content.Trim(); } } ``` The constructor of *OpenAIService* takes *HttpClient* and *apiKey* as parameters. This usage of dependency injection allows us to supply dependencies from outside the class, increasing the flexibility and testability of our code. The *SendPromptAndGetResponse()* method is where the magic happens. It constructs a request, sends it to the OpenAI API, and processes the response. The method encapsulates these steps and presents a straightforward interface to the rest of our application: give it a prompt, and it will return the response. In essence, *OpenAIService* is an example of the Facade pattern, which provides a simplified interface to a complex subsystem. Here, the subsystem is the interaction with the OpenAI API, and *OpenAIService* is the facade that simplifies that interaction for the rest of our application. Next, we’ll look at how to use *OpenAIService* in the context of a chatbot application. ### Understanding the OpenAI Chat Completion API Before we dive further into the coding, let’s take a pause and better understand the core concepts that make the OpenAI Chat Completion API work. Getting these fundamentals right will go a long way in efficiently interacting with the API. **Interpreting API Responses:** When we send a chat completion request to OpenAI, the response comes in a JSON format, which includes an array of ‘choices’. To convert this JSON data into a usable format in our C# application, we use a process called deserialization. For this purpose, we’ve implemented the *ResponseBody* class: ```Csharp public class Message { public string Content { get; set; } } public class Choice { public Message Message { get; set; } } public class ResponseBody { public List Choices { get; set; } } ``` In the above classes, *Choice* corresponds to the 'choices' we get from the API, and *Message* represents individual messages. The *ResponseBody* class holds a list of *Choice* objects, which effectively forms a roadmap to traverse the JSON response. **Tokens Demystified:** Think of tokens as building blocks of conversation. In OpenAI’s language model, a token can range from a single character to a whole word. Understanding tokens is crucial because they influence both the cost and the maximum limit of your API usage. Every message to and from the API consumes tokens, affecting how much you pay and how long your conversations can be. **The Role of Choices:** When we send a prompt to the API, it can generate multiple completions or ‘choices’ based on the prompt. Each of these completions provides a unique direction for the conversation to proceed. By manipulating these choices, we can engineer our chatbot’s responses to provide a dynamic user experience. Recognizing these key concepts of the OpenAI Chat Completion API is paramount to leverage its full potential efficiently. It’s all about striking a balance between providing a rich conversational experience and managing the cost associated with token usage. ### Building a Chat Session to Maintain Context When designing our chat application, it’s essential to consider how we handle the conversational context. As humans, our understanding of a conversation depends on the messages that have been previously exchanged — we remember previous statements and use them to inform our responses. In similar fashion, for our AI to generate meaningful responses, it needs access to previous prompts and replies. This is what we refer to as maintaining the conversation context. To handle this in our console chatbot, we introduce a new class *ChatSession*. ```csharp public class ChatSession { private readonly List _messages; public ChatSession() { _messages = new List { new { role = "system", content = "You are a helpful assistant." } }; } public void AddMessage(string role, string content) { _messages.Add(new { role, content }); } public object[] GetMessages() => _messages.ToArray(); } ``` This class plays a crucial role in preserving the conversational context throughout the user's interaction with the AI. The *ChatSession* object stores every message exchanged during the conversation, ensuring that each new message can be understood in the full context of what has been said before. When a *ChatSession* is first initialized, it's provided with a system message that sets the tone for the AI's responses. As the conversation progresses, each message – both from the user and the AI – is added to the *\_messages* list within *ChatSession*. The *AddMessage()* method helps to encapsulate this operation, taking as parameters the *role* ("user" or "assistant") and the *content* of the message. Now, let’s modify our main chat loop to make use of *ChatSession.* For that create or change the *Program.cs* as follows: ```Csharp internal class Program { private static async Task Main(string[] args) { var builder = new ConfigurationBuilder() .AddUserSecrets(); var configuration = builder.Build(); var apiKey = configuration["OpenAI:ApiKey"]; using var httpClient = new HttpClient(); var openAIService = new OpenAIService(httpClient, apiKey); var chatSession = new ChatSession(); while (true) { Console.Write("You: "); var userInput = Console.ReadLine(); if (string.IsNullOrWhiteSpace(userInput)) { Console.WriteLine("Input can't be empty. Please try again."); continue; } chatSession.AddMessage("user", userInput); try { var response = await openAIService.SendPromptAndGetResponse(chatSession.GetMessages()); Console.WriteLine($"OpenAI: {response}"); chatSession.AddMessage("assistant", response); } catch (Exception ex) { Console.WriteLine($"An error occurred: {ex.Message}"); break; } } } } ``` After the user's input is received, it's added to the *ChatSession* object as a user message. When a response is obtained from the AI, this too is added to the *ChatSession* as an assistant message. In line with our newly established *ChatSession* class, it’s essential we adapt the *SendPromptAndGetResponse()* method in our *OpenAIService* class. This adaptation is crucial for maintaining conversation context and feeding it to our OpenAI model. Our revised method now takes an *IEnumerable* parameter, which is our list of messages. The list comprises both the system, user, and assistant messages which encapsulate the complete conversation history. This list forms the content of the *messages* field in the request body for the OpenAI API. The *SendPromptAndGetResponse()* looks like this now: ```Csharp public async Task SendPromptAndGetResponse(IEnumerable messages) { const string requestUri = "https://api.openai.com/v1/chat/completions"; var requestBody = new { temperature = 0.2, model="gpt-3.5-turbo", messages= messages.ToList() }; _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", _apiKey); var response = await _httpClient.PostAsync( requestUri, new StringContent(JsonConvert.SerializeObject(requestBody), Encoding.UTF8, "application/json")); response.EnsureSuccessStatusCode(); var responseBody = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync()); return responseBody.Choices[0].Message.Content.Trim(); } ``` With this implementation, our chatbot becomes more conversational, understanding and responding appropriately to user inputs in the full context of the conversation. By encapsulating the context management within the *ChatSession* class, our main application flow remains clean, focused, and maintainable, further underscoring the importance and benefits of good object-oriented design. ### Running and Testing Your OpenAI-Powered Chatbot With the chatbot implemented and the OpenAI API service integrated, it’s time to put our console application to the test. Remember, you can interact with your chatbot directly through the console, making it easy to provide inputs and see the AI’s responses. 1. Open a terminal window. 2. Navigate to your project’s directory. 3. To run the application, use the *dotnet run* command. This command builds and runs your application in one step. Your chatbot is now waiting for your input. Type in a question or statement and hit `Enter` to see how the AI responds. For example, you might ask: *Translate 'Today is beautiful weather.' into German.* You can now talk to the AI. As you can see in the following example output, the model recognizes the context of your input as in a human-to-human dialog. ```Bash You: Translate 'Today is beautiful weather.' into German. OpenAI: Heute ist schönes Wetter. You: Now to French. OpenAI: Aujourd'hui, il fait beau. You: Say it in Spanish. OpenAI: Hoy hace buen tiempo. ``` Experiment with different kinds of prompts to see how the AI responds. This is a great opportunity to gauge its capabilities, understand its limitations, and get a sense of how it might be used in a more complex application. Keep in mind that although this tutorial focuses on creating a simple chatbot, the principles and methods used here are applicable to a wide range of applications. Whether you’re building an intelligent virtual assistant for an app, an automated content generator, or a tool for answering customer inquiries, OpenAI’s GPT-4 can provide a significant boost in functionality and user experience. ### Wrapping up We’ve just built a console application in .NET Core that interacts with the OpenAI API using the GPT-3.5-Turbo model (or if you have a paid account and mininum one payment done you can also use GPT-4). This application serves as a basic chatbot that can receive prompts from a user and generate intelligent responses. In this journey, we have explored the foundational concepts of the OpenAI API, from tokens and choices to the structure of the chat completion API. This knowledge, combined with hands-on experience, will serve as a solid base as you venture further into the world of AI with OpenAI. If you would like to reference the complete code, you can access it on my GitHub repository: [openai-csharp-example](https://github.com/ricofritzsche/openai-csharp-example.git?ref=ricofritzsche.me). Bear in mind, Artificial Intelligence, especially in dotnetcore**,** is constantly evolving, and it’s an exciting field to be a part of. Don’t stop exploring, and don’t stop learning. ### Crafting Code the Right Way: Tips from My Decades in the Field URL: https://ricofritzsche.me/crafting-code-the-right-way-tips/ Last updated: 2024-03-04T13:37:03.000Z When I cast my mind back to 1993, the world of coding was a different playground. There were no YouTube tutorials, no massive online courses; it was me, problems to solve, and code to write. This journey, spanning decades, has ingrained in me a few invaluable lessons that I believe every coder, be it a newbie or a pro, should have in their toolkit. 1. **Dive In and Code**: The very genesis of my journey started not with formal classes but with real problems that required solutions. The hands-on approach can't be emphasized enough. Dive deep, write code, make mistakes, correct them, and then code some more. Just like a sport, you won't get better by merely watching or reading about it - you must practice. 2. **Understand the Patterns**: Patterns are your compass. These aren't tied to any specific language but are universal guidelines that streamline the coding process. Principles like 'Separation of Concerns', 'KISS (Keep It Simple, Stupid)', and 'DRY (Don't Repeat Yourself)' are timeless. They're the language every coder speaks, and understanding them makes transitioning between different programming languages smoother. By understanding approaches like Domain-Driven Design, you also get to see coding from a business perspective, which is essential for building software that truly serves its users. 3. **Be Language Agnostic**: It's easy to get cozy with one programming language, especially if it's the first one you learn. However, being fixated means you might miss out on the richness that others offer. While the syntax may change, the foundational concepts often remain similar. The magic lies in how you adapt and use these languages as tools to solve different problems. 4. **Simplicity is Key**: I've seen codes that seem more like intricate mazes than solutions. A growing list of dependencies, a complicated series of libraries for a simple task - these are red flags. Every time your code starts looking complex, it's time to take a step back and reflect. Remember, the beauty of a code lies not in its complexity but in its simplicity and readability. Wrapping it up, while the coding world has seen a paradigm shift since the time I began, some things remain eternal. It's not just about understanding a programming language but about understanding the problem you're trying to solve, and then choosing the best tools and patterns to solve it. These principles have been my companions throughout, and I hope they serve you as well as they've served me. If you're keen to dive deeper and unlock all the insights, head over to [Medium](https://medium.com/@rico-fritzsche/the-core-principles-to-become-a-great-coder-1f2c8fc44166?ref=ricofritzsche.me) to read the full article. Don't miss out! ### Decoding Clean Code URL: https://ricofritzsche.me/decoding-clean-code-its-impact-on/ Last updated: 2024-03-04T13:29:13.000Z ### Why Talk About Clean Code Now? You may ask yourself, "Why does everyone keep talking about clean code? Especially those who have been in software development for a long time" The answer is simple yet profound: Times have changed. Regardless of the industry you're in - automotive, healthcare, banking, or any other sector - you're basically in the software business today. Over the last few decades, as I've worked my way through more than 80 projects, one insight has stood out to me: Companies that put clean code first are the ones that thrive. This series is intended to provide some insight into the world of clean code. ### A Brief Stroll Down Memory Lane Let's travel back in time. When I started my career as a programmer in the early nineties, software was often a side issue, a tool that facilitated the main business processes. In the early years, I often sat in a room with the experts and learned directly what problems they were having in the analog world. A lot of things happened on cue, were implemented immediately. And the code was completely designed around "it has to work." The boss of one of my first customers was always of the opinion that once it works, we just don't touch it. His credo was "never touch a running system". Those were the days when code became more untenable with each new requirement. I think that was the birth of the expression "historically grown". Today, software not only supports a company's core processes, but often drives them. With such a central role, the importance of writing "clean" software cannot be overstated. ### Unpacking the Clean Code Philosophy At the heart of the clean code movement lies Robert C. Martin's book, "Clean Code: A Handbook of Agile Software Craftsmanship." For the uninitiated, you might assume clean code is just about neatness or aesthetics. But it's so much deeper than that. #### 1\. **Readability**: Your code isn't just for machines—it's for humans, too. Whether it's your colleague, a new team member, or even you revisiting your code after some time, it should be easily decipherable. Think of it like writing a book; if readers can't understand your story, they'll close the book. #### 2\. **Maintainability**: Business needs aren't static—they evolve. Your code should be agile enough to accommodate these shifts without going through a complete overhaul. It's like building with LEGO; you should be able to add, remove, or modify without tearing the entire structure down. #### 3\. **Efficiency**: While we've made leaps in hardware capabilities, code efficiency remains paramount. Efficient code ensures your application runs smoothly, offering users an uninterrupted experience. It's akin to a well-oiled machine that delivers optimal performance without guzzling resources. #### 4\. **Consistency**: Consistency in code is about following established patterns and styles. It ensures that when someone dives into the code, they don't encounter surprises. Think of it as following traffic rules; it keeps things predictable and avoids chaos. #### 5\. **Simplicity**: The allure to add 'bells and whistles' is real, but often, less is more. Avoid over-engineering. The goal is to design solutions that are straightforward and easy to grasp. Like a minimalist painting, sometimes the simplest designs are the most impactful. ### Clean Code: Beyond the Basics At first glance, one might assume that clean code principles apply strictly to software-centric industries like tech startups or IT service providers. Yet, the tenets of clean code – clarity, efficiency, and adaptability – find resonance across varied sectors, be it automotive, healthcare, finance, or even agriculture. While the implementation might differ, the core principles remain consistent. #### **Clarity: The Cornerstone of Communication** **Tech Startup**: In a rapidly growing tech startup, where features are frequently added or pivoted, clarity ensures that every developer, irrespective of when they joined, can grasp the code's purpose quickly. Imagine a code that manages user authentication. If not clear, security loopholes might arise, leading to potential data breaches. **Automotive Industry**: Consider the software behind a car's anti-lock braking system. It's crucial for the engineers, irrespective of their tenure or domain expertise, to understand the code. A lack of clarity can jeopardize lives. #### Efficiency: Doing More with Less **E-commerce**: Efficiency in an e-commerce platform's code could mean faster load times, leading to improved user retention and increased sales. **Agriculture**: In precision farming, efficient code in soil monitoring systems ensures timely data on moisture levels, enabling optimal irrigation. Even a slight delay due to inefficient code could lead to water wastage or suboptimal crop yields. #### Adaptability: Preparing for the Future **Healthcare**: In a domain like healthcare, where regulations and research frequently lead to changes, software used for patient management or diagnostics must be adaptable. As new discoveries emerge or when new regulations are introducede, the software should accommodate new modules or functionalities with ease. **Finance**: Think of a stock trading application. In view of the constantly changing market dynamics, the algorithms must be regularly adapted. If the code isn't adaptable, integrating these changes could be time-consuming, leading to potential financial setbacks. ### Wrapping Up: A Note from My Business Lens From boardroom discussions to software development sprints, the importance of clarity and efficiency remains unchanged. Clean code is not just a software principle, it's a business imperative. In the rest of this series, we'll explore how clean code principles are shaping businesses, influencing decisions, and driving growth in the 21st century. ### Mastering OpenAI in C#: A Guide to AI-Powered Apps URL: https://ricofritzsche.me/mastering-openai-in-c-a-guide-to/ Last updated: 2024-03-04T13:38:56.000Z OpenAI, a leading AI research lab, offers a wealth of possibilities with its powerful GPT-3.5-turbo and GPT-4 language models. These models are at the forefront of natural language understanding and generation, enabling developers to create more interactive and intuitive applications. As a software developer, I've always found immense satisfaction in exploring and experimenting with the latest advancements in technology. Recently, my curiosity led me towards OpenAI's API. I was fascinated by its capabilities and intrigued by the challenges it presented in the context of integration with existing applications. Why this fascination, you may ask? It's because OpenAI's API opens up a world of possibilities. The ability to have a machine understand and generate human-like text is a game-changer, and I was keen on understanding how I could integrate this into my own software. In this context, I decided to use C#, a language known for its versatility and robustness. In this post, I am sharing a overview of my journey to integrate OpenAI's API using C#. I try answering the question why using OpenAI API can be beneficial for developers, discuss the models that OpenAI provides, and explain how you can combine them to create exciting applications. I also touch upon the concept of chat completions in OpenAI, which is a fundamental principle in understanding how OpenAI's API functions. For a deep dive, with code examples and step-by-step instructions, I would encourage you to check out the full article on my Medium blog [here](https://blog.ricofritzsche.de/ai-powered-net-core-apps-a-comprehensive-guide-to-openai-integration-f24ae7ae55f0?sk=57f7780d138e8686c48da740e22a8060&ref=ricofritzsche.me). But for now, let's embark on this exciting exploration of OpenAI API and C#. ### Why OpenAI? Leveraging OpenAI's API brings in a host of benefits. It provides a convenient and effective way to generate human-like text, offering a valuable resource for a multitude of applications, from content generation and text translation to complex coding assistance. It is designed to understand context, making it a robust tool to interact with in various scenarios. A special note on the choice of OpenAI models: In my guide, I opted for the gpt-3.5-turbo model over the newer GPT-4\. The reason for this choice is based on the current accessibility of these models. As of now, GPT-4 is only available to users who have made at least one payment on their OpenAI account, while gpt-3.5-turbo is accessible to all users. This is an important consideration to ensure the practical applicability of our guide for a wider audience. That said, gpt-3.5-turbo still offers impressive performance, delivering quality responses and supporting the advanced chat format for a more interactive and engaging AI experience. ### Models and Their Role When working with OpenAI, understanding the concept of models is essential. Models like GPT-3.5-turbo are trained to understand and generate human-like text based on the input they receive. They operate on tokens, a basic unit of understanding, which could range from a character to a whole word in some languages. By managing tokens effectively, you can control the model's output to a significant extent. ### C# and OpenAI: A Powerful Combination So, why consider C# in the realm of AI? The answer lies in its versatility. C#, a multi-paradigm programming language developed by [Microsoft](https://dotnet.microsoft.com/en-us/?ref=ricofritzsche.me), is widely used for creating web applications and services, and more. When combined with OpenAI's capabilities, C# allows developers to create more dynamic and responsive applications. C# enhances OpenAI API's integration into your applications, making it a straightforward process. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https-3a-2f-2fsubstack-post-media.s3.amazonaws.com-2fpublic-2fimages-2fe96425ca-8225-46cd-bdc6-afd59771b6a7_2194x1130.png) ### OpenAI's Chat Completions: A Vital Concept OpenAI also introduces the concept of "chat completions". It allows your application to conduct a more coherent and meaningful conversation by maintaining a list of messages, each with a role (like 'system', 'user', or 'assistant') and content. It significantly aids the model in understanding the context better, leading to improved results. In essence, integrating OpenAI API into C# applications ushers in a realm of possibilities for developers. It offers the potential to create more dynamic, responsive, and user-friendly applications by leveraging the AI's understanding and generation of human-like text. This is just the tip of the iceberg! For an in-depth understanding and practical coding examples on integrating OpenAI into your C# applications, dive into this comprehensive guide [here](https://blog.ricofritzsche.de/ai-powered-net-core-apps-a-comprehensive-guide-to-openai-integration-f24ae7ae55f0?sk=57f7780d138e8686c48da740e22a8060&ref=ricofritzsche.me). ### Successfully Deploying a .NET Core App on Heroku from a Macbook with M1 Chip URL: https://ricofritzsche.me/successfully-deploying-a-net-core-app-heroku/ Last updated: 2024-03-04T13:35:09.000Z Ever hit a roadblock deploying your .NET Core application on Heroku, especially from a MacBook with an M1 chip? We've got you covered in my latest Medium post! In this post, I show you how to create a .NET Core application using the CLI, dockerize it, and tackle the tricky part - deploying it from a MacBook M1\. I explain why you may encounter an 'Exec format error' during deployment and provide an efficient workaround using Github workflows. ![](https://storage.ghost.io/c/5b/3f/5b3fa9cb-95b3-4667-98c5-ca3c2c9a3233/content/images/image/fetch/w_2000,h_2000,c_fill,f_jpg,q_auto:good,fl_progressive:steep,g_auto/https-3a-2f-2fsubstack-post-media.s3.amazonaws.com-2fpublic-2fimages-2f6d4b1e9b-0667-41b4-9f01-acca34d21da8_2206x800.jpg) Here are the basics: **Set Up the .NET Core Application:** Use the CLI to create a minimalistic REST API, ready for deployment. ``` dotnet new web -o MyAPI ``` **Dockerize the Application:** With my detailed guide, learn to create an optimized Dockerfile that suits .NET Core 7.0. **Deploy the Application:** On an Intel architecture Mac, deployment is as simple as pushing to Heroku using Heroku CLI commands. But if you're on a MacBook M1, you'll need to use a Github workflow to build, push, and release the Docker container to Heroku. ``` name: API Deployment on: push: branches: [main] jobs: build: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v2 - name: Build, Push and Release Docker container to Heroku uses: gonuit/heroku-docker-deploy@v1.3.3 with: email: ${{ secrets.HEROKU_EMAIL }} heroku_api_key: ${{ secrets.HEROKU_API_KEY }} heroku_app_name: ${{ secrets.HEROKU_APP_NAME }} dockerfile_directory: ./ dockerfile_name: Dockerfile docker_options: "--no-cache" process_type: web ``` In just a few steps, your .NET Core application is up and running on Heroku, even from a MacBook M1\. Now, isn't that smooth? For a detailed walkthrough, visit the full post [here](https://medium.com/@rico-fritzsche/how-to-configure-dockerize-and-deploy-a-net-core-application-on-heroku-from-a-mac-m1-machine-d09173560a4?ref=ricofritzsche.me). Stay tuned for more tech tips! ### Unlock the Full Potential of the Strategy Pattern URL: https://ricofritzsche.me/unlock-the-full-potential-of-the/ Last updated: 2024-03-04T13:40:22.000Z Software development is full of many interesting design patterns, each bringing their unique solutions to common problems we face as developers. Among them, the Strategy Pattern stands out. It embodies an elegance and efficiency that can revolutionize your codebase, pushing the boundaries of readability, maintainability, and organization. Now, how does the Strategy Pattern look in practice? And more importantly, how can you leverage it in TypeScript for instance to make your software design more robust? Today, I'm thrilled to share my new blog post: [**Unleashing the Power of the Strategy Pattern: A Real-World Guide with Examples in TypeScript**](https://blog.ricofritzsche.de/unleashing-the-power-of-the-strategy-pattern-a-real-world-guide-with-examples-in-typescript-1661e2b29d52?sk=f79fd15fefdc420f28e57f2aadbff342&ref=ricofritzsche.me). In this article, we'll delve into the Strategy Pattern's mechanics, explore why it's a valuable addition to your coding arsenal, and walk through clear, practical examples to illustrate its real-world application. Through this journey, you'll learn how to: 1. Identify when to use the Strategy Pattern. 2. Implement the Strategy Pattern effectively in TypeScript. 3. Increase code readability and maintainability by separating concerns using the Strategy Pattern. No matter where you stand on your coding journey, a deep understanding of design patterns can significantly upgrade your skills and open up new possibilities. If you have been following my writings on Medium, you'll know that we have discussed related topics in the past, such as Factory Pattern, Repositories, Domain-Driven Design, Separation of Concerns and many more. Each of these concepts adds a piece to the puzzle, leading us to a broader understanding of clean coding and effective software design. But today, it's all about the Strategy Pattern. So, are you ready to harness the power of this dynamic design pattern and bring your TypeScript coding skills to the next level? [Dive into the article](https://blog.ricofritzsche.de/unleashing-the-power-of-the-strategy-pattern-a-real-world-guide-with-examples-in-typescript-1661e2b29d52?sk=f79fd15fefdc420f28e57f2aadbff342&ref=ricofritzsche.me) and let's explore together. Rico ### Diving into Domain-Driven Design: Repositories, Factories, & Bounded Contexts Explained URL: https://ricofritzsche.me/diving-into-domain-driven-design/ Last updated: 2024-03-04T13:18:54.000Z In the world of software development, Domain-Driven Design (DDD) is a game-changer. It's more than just another design approach—it's a framework for decoding and addressing complex business problems. Today, we'll tackle the bedrock of DDD, namely Repositories, Factories, and Bounded Contexts. ### The Power Pair: Repositories & Factories Let's start by cracking open the twin pillars of DDD - Repositories and Factories. These are the architects of a formidable software structure. Repositories serve as an abstraction layer, offering methods to pull out domain objects, while neatly tucking away the underlying infrastructure. Conversely, Factories shoulder the responsibility of crafting complex domain objects and aggregates. They package the entire process of object creation and initialization, endorsing code reusability and a clean separation of concerns. Together, they simplify complex operations while upholding the model's integrity. Ready to dive deeper? [Check out this detailed piece on Repositories and Factories](https://levelup.gitconnected.com/repositories-and-factories-in-domain-driven-design-the-twin-pillars-of-robust-software-3c89760c932e?ref=ricofritzsche.me). ### Flexibility Unleashed: The Essence of DDD What sets DDD apart is the unmatched flexibility it bestows on the software design process. By putting the spotlight on core business concepts rather than infrastructure or specific technologies, DDD empowers developers to build software that's agile and ready for evolving business needs. The secret ingredient? The Ubiquitous Language—a universally understood language that's built around the domain model. It boosts communication efficacy, minimizes misunderstandings, and aligns the software closely with business needs. [Discover more about the essence and flexibility of DDD here](https://levelup.gitconnected.com/mastering-flexibility-understanding-the-essence-of-domain-driven-design-4a03fb5b56f6?ref=ricofritzsche.me). ### Bounded Context: Defining Clear Boundaries A crucial part of the DDD universe is the Bounded Context. It defines the boundary within which a specific model operates, preventing overlaps and clashes with other models. It’s like having a clear roadmap of what the model does (and does not), simplifying system management and comprehension. [This practical guide will walk you through the nuances of Bounded Context in DDD](https://levelup.gitconnected.com/bounded-context-in-domain-driven-design-a-practical-guide-c1f9192ac93d?ref=ricofritzsche.me). In a nutshell, Domain-Driven Design is a holistic and adaptable approach to software development. By zeroing in on the core business domain, leveraging Repositories and Factories, and establishing clear boundaries with Bounded Contexts, DDD equips teams to build robust, agile, and business-centric software. [Subscribe](#/portal/signup) ### Mastering URL Manipulation in JavaScript: A Guide to Extracting Protocol and Domain URL: https://ricofritzsche.me/mastering-url-manipulation-in-javascript/ Last updated: 2024-03-04T13:42:17.000Z In this blog post, we will explore how to extract the protocol and domain from a URL string using JavaScript. This is a common task that may be needed in a variety of applications, especially those dealing with web resources. Let's consider the following URL string as an example: `https://www.example.com/pathname?search=test#hash`. ## The JavaScript Way JavaScript provides the built-in `URL` API which allows us to easily manipulate and retrieve information from URLs. The `URL` constructor creates a new URL object from a URL string: ``` const url = new URL("https://www.example.com/pathname?search=test#hash"); ``` From this `url` object, we can access several properties which provide information about the URL. In this case, we are interested in the `protocol` and `hostname` properties: ``` const protocol = url.protocol; // "https:" const domain = url.hostname; // "www.example.com" ``` And if we want both the protocol and domain together: ``` const protocolAndDomain = `${url.protocol}//${url.hostname}`; // "https://www.example.com" ``` ## The Don'ts While it may be tempting to try and extract the protocol and domain using string operations, such as `split`, this approach is generally not recommended. URL formatting can be complex and varies greatly, and a naive string operation may not correctly handle all cases. In addition, the `URL` API provides a more robust and readable solution. Consider this example: ``` const urlString = "https://www.example.com/pathname?search=test#hash"; const protocolAndDomain = urlString.split('/')[2]; ``` This will work for this particular URL, but may fail for others. For instance, if the URL does not contain a path (e.g., https://www.example.com), the `split` operation will not work as expected. ## Conclusion When dealing with URLs in JavaScript, it's best to use the built-in `URL` API to manipulate and retrieve information. This will ensure that your code is robust, readable, and can correctly handle a wide range of URLs. Avoid using simple string operations, as URLs can be complex and vary greatly in format. Stick to the provided APIs for the best results. Next time you're dealing with URLs in your JavaScript code, keep these tips in mind! *Cheers*! ### Mastering TypeScript: A Guide to Choosing Between ‘type’ and ‘interface’ URL: https://ricofritzsche.me/mastering-typescript-a-guide-to-choosing-between-type-and-interface/ Last updated: 2024-03-04T13:40:51.000Z TypeScript has become a reliable tool for catching errors and improving the stability of code. However, when working with TypeScript, I’ve often wondered about the differences between the `type` and `interface` keywords and when to use each one. In this blog post, I want to share what I’ve learned about the differences between `type` and `interface` and when to use each one. I'll provide clear examples and explanations to help you better understand how to use these two powerful tools in your TypeScript projects. By the end of this post, you’ll have a better understanding of the differences between `type` and `interface` and how to use them effectively in your TypeScript code. I hope this post will be helpful to other web developers who are looking to improve their TypeScript skills and write more reliable code. ## Type vs Interface In TypeScript, both `type` and `interface` can be used to define object types, but they have some differences in syntax and functionality. `interface` is a keyword used to define object types and has the syntax. ```TypeScript interface IUser { name: string; age: number; ... } ``` On the other hand, `type` is a keyword used to define object types and has the syntax: ```TypeScript type User = { name: string; age: number; ... }; ``` The syntax difference is minor, but there are other differences in functionality. `interface` can be extended to create new types that inherit the properties of the parent interface: ```TypeScript type Union = Type1 | Type2; type Intersection = Type1 & Type2; ``` These types allow you to define more complex relationships between types and can help you model data more accurately. ## Example — Building a Messaging App Let’s say you’re building a messaging app, and you want to define a type for a message that can contain either text or an image. You can use a union type to define this: ```TypeScript type Message = { type: 'text' | 'image'; content: string | File; } ``` In this example, we use `type` to define a union type that can only be one of two string values: `'text'` or `'image'`. We also use a union type to define the `content` property, which can be either a string (for text messages) or a `File` object (for image messages). Now let’s say you want to define a type for a group of users who have different roles in your app, such as admin, moderator, and regular user. You can use an intersection type to define this: ```TypeScript type User = { id: number; name: string; } type Admin = User & { role: 'admin'; permissions: string[]; } type Moderator = User & { role: 'moderator'; canDelete: boolean; } type RegularUser = User & { role: 'user'; isPremium: boolean; } ``` In this example, we define a `User` type with two properties: `id` and `name`. We then define three other types (`Admin`, `Moderator`, and `RegularUser`) that extend the `User` type using an intersection type (`&`). Each of these types has a different `role` property and additional properties specific to their role. Using union and intersection types with `type` can help you define more complex types in your TypeScript code and make it easier to model real-world data. ## Declaration Merging `interface` also supports declaration merging, which allows you to define multiple interfaces with the same name and merge their properties into a single interface: ```TypeScript interface MyObject { property1: Type1; } interface MyObject { property2: Type2; } const myObject: MyObject = { property1: 'value1', property2: 'value2' }; ``` This can be useful when you’re working with third-party libraries or systems that define their interfaces separately and need to merge them into a single interface. ## Example — Unleashing the Power of Declaration Merging Let’s say you’re building an app that requires a user to sign in and you want to define an interface for the user object returned by your authentication API. You might define the interface like this: ```TypeScript interface User { username: string; email: string; } ``` Later, you find out that the authentication API also returns a `userId` property for each user, but you don't want to modify the original `User` interface. Instead, you can define a new interface with the same name and it will automatically merge the properties with the original `User` interface: ```TypeScript interface User { userId: number; } // Now the User interface has three properties: username, email, and userId const user: User = { username: 'john.doe', email: 'john.doe@example.com', userId: 123 }; ``` In this example, we define the `User` interface with two properties: `username` and `email`. We then define a new `User` interface with the `userId` property. When we use the `User` interface to define our `user` object, the properties from both interfaces are merged together to create an object with three properties: `username`, `email`, and `userId`. Declaration merging is a useful feature of `interface` that allows you to extend existing interfaces without modifying their original definition. ## Let’s talk about the Open/Closed Principle Ah, the Open/Closed Principle (OCP). One of the five SOLID principles of object-oriented programming design. It’s like the holy grail of software development. Everyone talks about it, but do they really understand it? Or are they just pretending? I hope you remember: SOLID is an acronym that stands for five principles of object-oriented programming design that were introduced by Robert C. Martin (also known as Uncle Bob). These principles are intended to make software development more scalable, maintainable, and extensible. The previous example is a good illustration of the Open/Closed Principle because it shows how you can extend an interface without modifying its original definition. By defining a new interface with the `userId` property, rather than modifying the original `User` interface, you're keeping the original interface closed for modification while still extending its functionality. This approach makes it easier to maintain and evolve your code over time without introducing unexpected issues or breaking changes. But wait, there’s more! If you use TypeScript or another language with declaration merging, you can have your cake and eat it too. You can define the `User` interface with just two properties, and then define a new `User` interface with the `userId` property. When you use the `User` interface to define your user object, the properties from both interfaces magically merge together to create an object with three properties. It's like magic, but with less rabbits and more TypeScript. But let’s be real, who has time for all this OCP nonsense? Sometimes you just need to get stuff done, and modifying existing code is the fastest way to do it. Who cares about the OCP anyway? It’s just a fancy principle that software developers like to throw around to make themselves sound smart. So, go ahead and violate the OCP if you must. Just make sure you have a good reason for doing so. And if anyone asks, just tell them you’re following the KCP (Keep it Complacent Principle). That’s a principle we can all get behind. ## React Component Props So you’re wondering whether to use types or interfaces to define your React component props? Well, let me tell you, you’ve come to the right place. I mean, who needs a life when you can spend your days debating the finer points of TypeScript syntax? You’re like a modern-day Socrates, asking the important questions like “should I use `type` or `interface`?" while the rest of us plebs are busy swiping left on Tinder. So buckle up, my friend, because we're about to dive deep into the exciting world of type definitions for React components. Using `type` or `interface` to define props in a React component is largely a matter of personal preference, as both options provide similar functionality. However, there are a few differences to consider when choosing between them. `interface` is a commonly used approach for defining props in React components. This is because `interface` allows you to define the shape of the props object in a way that makes it easy to extend and reuse. For example, you can create a base `Props` interface that defines the common props for a group of components and then extend it for each individual component as needed: ```TypeScript interface Props { className?: string; onClick?: () => void; } interface ButtonProps extends Props { type?: 'button' | 'submit' | 'reset'; } function Button({ className, onClick, type }: ButtonProps) { // render button with props } ``` In this example, we define a `Props` interface with two optional props: `className` and `onClick`. We then extend the `Props` interface to create a `ButtonProps` interface that adds an optional `type` prop. We can then use the `ButtonProps` interface to define the props for our `Button` component. `type`, on the other hand, is useful when you need to define more complex types, such as union or intersection types, in your props. For example, you can use `type` to define a `Size` type that represents different size options for a component: ```TypeScript type Size = 'small' | 'medium' | 'large'; interface Props { size: Size; } function Component({ size }: Props) { // render component with props } ``` In this example, we use `type` to define a `Size` type that represents the different size options for our component. We then use the `Props` interface to define the `size` prop, which is required and can only be one of the values in the `Size` type. In general, using `interface` is a good choice for defining simple props that can be extended or reused, while using `type` is a good choice for defining more complex types in your props. In conclusion, both `type` and `interface` can be used to define props in a React component, but they have some differences in functionality. `interface` is useful for defining simple props that can be extended or reused, while `type` is useful for defining more complex types in your props. Ultimately, the choice between `type` and `interface` is a matter of personal preference and the specific needs of your project. ## Summary In general, `interface` is often used to define the shape of objects that are used as interfaces with other systems or libraries. `type`, on the other hand, is often used to define more complex types, such as unions, intersections, or mapped types, or to define types that cannot be expressed as interfaces, such as function types or conditional types. *Cheers*!