<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: lef237</title>
    <description>The latest articles on DEV Community by lef237 (@lef237).</description>
    <link>https://dev.to/lef237</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3432083%2Fa7f2ce66-61e8-43cc-b1b6-e87e67fdf0e2.jpg</url>
      <title>DEV Community: lef237</title>
      <link>https://dev.to/lef237</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lef237"/>
    <language>en</language>
    <item>
      <title>clauhist: browse full Claude Code history and resume sessions across projects</title>
      <dc:creator>lef237</dc:creator>
      <pubDate>Sat, 28 Mar 2026 04:43:38 +0000</pubDate>
      <link>https://dev.to/lef237/clauhist-browse-full-claude-code-history-and-resume-sessions-across-projects-1c1o</link>
      <guid>https://dev.to/lef237/clauhist-browse-full-claude-code-history-and-resume-sessions-across-projects-1c1o</guid>
      <description>&lt;p&gt;Claude Code can already help you resume work from a project if you move into that working directory and use &lt;code&gt;/resume&lt;/code&gt; there.&lt;/p&gt;

&lt;p&gt;The limitation is that this is tied to the current working directory. If you want to look back across all of your past work, including sessions from other repositories and directories, that gets awkward.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;clauhist&lt;/code&gt; is a small CLI tool for that case. It shows your Claude Code history in &lt;code&gt;fzf&lt;/code&gt;, lets you browse sessions across working directories, and resume one from the list.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it shows
&lt;/h2&gt;

&lt;p&gt;Sessions are sorted by recent activity. Each row includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the last activity time&lt;/li&gt;
&lt;li&gt;the project path&lt;/li&gt;
&lt;li&gt;whether the path still exists&lt;/li&gt;
&lt;li&gt;a preview of the first message&lt;/li&gt;
&lt;li&gt;the message count&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also a preview pane with the session ID, timestamps, and message list. Once you find the one you want, press &lt;code&gt;Enter&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;clauhist&lt;/code&gt; reads &lt;code&gt;~/.claude/history.jsonl&lt;/code&gt;, groups entries by session, and passes the result to &lt;code&gt;fzf&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;After you select a session, it runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;claude &lt;span class="nt"&gt;--resume&lt;/span&gt; &amp;lt;session-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;It does not keep its own database or add another history layer. It is just a thin local browser over Claude Code's existing history file.&lt;/p&gt;
&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cargo &lt;span class="nb"&gt;install &lt;/span&gt;clauhist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or install from source:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/lef237/clauhist.git
&lt;span class="nb"&gt;cd &lt;/span&gt;clauhist
cargo &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--path&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You also need &lt;code&gt;fzf&lt;/code&gt; and Claude Code installed.&lt;/p&gt;
&lt;h2&gt;
  
  
  Usage
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;clauhist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Main controls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Enter&lt;/code&gt;: resume the selected session&lt;/li&gt;
&lt;li&gt;type: filter the list&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Ctrl-/&lt;/code&gt;: toggle preview&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Ctrl-C&lt;/code&gt;: exit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is displayed on the terminal like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;╭───────────────────────────────────────────────────────────────────────────────────────╮
│ Claude Code History Browser  &lt;span class="o"&gt;[&lt;/span&gt;Enter: resume  Ctrl-/: toggle preview  Ctrl-C: cancel]  │
├───────────────────────────────────────────────────────────────────────────────────────┤
│ Search:                                                                               │
│ &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; 2026-03-18 09:12  ✓ ~/projects/myapp      Tell me about Rust error handling…  &lt;span class="o"&gt;(&lt;/span&gt;12&lt;span class="o"&gt;)&lt;/span&gt;  │
│   2026-03-17 22:45  ✓ ~/sandbox/api-client  Generate client from OpenAPI schema  &lt;span class="o"&gt;(&lt;/span&gt;8&lt;span class="o"&gt;)&lt;/span&gt;  │
│   2026-03-17 14:30  ✗ ~/old-project         Database migration steps             &lt;span class="o"&gt;(&lt;/span&gt;3&lt;span class="o"&gt;)&lt;/span&gt;  │
╰───────────────────────────────────────────────────────────────────────────────────────╯
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;✓&lt;/code&gt; means the project directory still exists. &lt;code&gt;✗&lt;/code&gt; means it was moved or deleted. You can still resume the session, but &lt;code&gt;cd&lt;/code&gt; into the original directory may fail.&lt;/p&gt;
&lt;h2&gt;
  
  
  Shell integration
&lt;/h2&gt;

&lt;p&gt;If you add this to your shell config:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;clauhist init zsh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;then &lt;code&gt;clauhist&lt;/code&gt; will now change to the project directory within your current shell before launching Claude Code.&lt;/p&gt;

&lt;p&gt;This allows you to immediately return to your original directory with &lt;code&gt;cd -&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Without this, &lt;code&gt;clauhist&lt;/code&gt; uses a subshell for the directory change, so &lt;code&gt;exit&lt;/code&gt; to return to your original shell.&lt;/p&gt;
&lt;h2&gt;
  
  
  Local-only
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;clauhist&lt;/code&gt; only reads &lt;code&gt;~/.claude/history.jsonl&lt;/code&gt; and launches &lt;code&gt;claude --resume&lt;/code&gt;. It does not send your history anywhere.&lt;/p&gt;

&lt;p&gt;That is the whole tool: a simple way to browse past Claude Code sessions across projects and reopen one without moving directory by directory.&lt;/p&gt;
&lt;h2&gt;
  
  
  GitHub
&lt;/h2&gt;

&lt;p&gt;You can find the repository here :)&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/lef237" rel="noopener noreferrer"&gt;
        lef237
      &lt;/a&gt; / &lt;a href="https://github.com/lef237/clauhist" rel="noopener noreferrer"&gt;
        clauhist
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Browse Claude Code history across working directories and resume sessions
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;clauhist&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;Browse Claude Code history across working directories and resume sessions.&lt;/p&gt;

&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;╭───────────────────────────────────────────────────────────────────────────────────────╮
│ Claude Code History Browser  [Enter: resume  Ctrl-/: toggle preview  Ctrl-C: cancel]  │
├───────────────────────────────────────────────────────────────────────────────────────┤
│ Search:                                                                               │
│ &amp;gt; 2026-03-18 09:12  ✓ ~/projects/myapp      Tell me about Rust error handling…  (12)  │
│   2026-03-17 22:45  ✓ ~/sandbox/api-client  Generate client from OpenAPI schema  (8)  │
│   2026-03-17 14:30  ✗ ~/old-project         Database migration steps             (3)  │
╰───────────────────────────────────────────────────────────────────────────────────────╯
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;em&gt;(Example output — actual appearance depends on your terminal and fzf version)&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Claude Code can already resume work from the current project directory. clauhist solves a different problem: browsing the full history across all of your working directories from one place.&lt;/p&gt;
&lt;p&gt;Select a session and press &lt;code&gt;Enter&lt;/code&gt; — clauhist opens &lt;code&gt;claude --resume&lt;/code&gt; in the project directory. When you exit Claude, you return to your original shell.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why clauhist exists&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;If you &lt;code&gt;cd&lt;/code&gt; into a project and use Claude Code's &lt;code&gt;/resume&lt;/code&gt; there, you can inspect…&lt;/p&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/lef237/clauhist" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;br&gt;
&lt;/div&gt;
&lt;br&gt;


&lt;p&gt;&lt;a href="https://github.com/lef237/clauhist" rel="noopener noreferrer"&gt;https://github.com/lef237/clauhist&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cli</category>
      <category>showdev</category>
      <category>tooling</category>
    </item>
    <item>
      <title>One-Time Editor, an editor for drafts before sending</title>
      <dc:creator>lef237</dc:creator>
      <pubDate>Mon, 09 Mar 2026 13:16:35 +0000</pubDate>
      <link>https://dev.to/lef237/one-time-editor-an-editor-for-drafts-before-sending-59gi</link>
      <guid>https://dev.to/lef237/one-time-editor-an-editor-for-drafts-before-sending-59gi</guid>
      <description>&lt;p&gt;I've created an app called "One-Time Editor".&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fidnldxnu6gqyty5xb40f.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fidnldxnu6gqyty5xb40f.png" alt="Dark Mode"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The image above shows dark mode. Here's what it looks like in light mode.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7069llna9a6mjy7tfcbt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7069llna9a6mjy7tfcbt.png" alt="Light Mode"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What kind of app is it, you ask? It's a &lt;strong&gt;text editor that can be used for drafting messages before sending them in a chat&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;By using this app, you can avoid the worry of accidentally sending a message prematurely by hitting Enter.&lt;/p&gt;

&lt;p&gt;To quickly understand how it works, I recommend watching the video, which I've attached below.&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/qwj9fr77vQg"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;&lt;a href="https://youtu.be/qwj9fr77vQg" rel="noopener noreferrer"&gt;https://youtu.be/qwj9fr77vQg&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Install
&lt;/h2&gt;

&lt;p&gt;For macOS, use Homebrew. You can start using it right away by typing the following commands in your terminal.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew tap lef237/tap
brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; one-time-editor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Since it's an Electron-based app, it's also cross-platform compatible.&lt;/p&gt;

&lt;p&gt;Windows and Linux users can install the pre-built app from here.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/lef237/one-time-editor/releases" rel="noopener noreferrer"&gt;https://github.com/lef237/one-time-editor/releases&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Windows users should select the .exe file, and Linux users should select the AppImage to install it. &lt;sup id="fnref1"&gt;1&lt;/sup&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;p&gt;The main highlight is that you can &lt;strong&gt;toggle (show/hide) the window using a keyboard shortcut&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;What's more, &lt;strong&gt;it automatically copies the content you've entered as soon as you toggle it, allowing for instant pasting.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open One-Time Editor with a keyboard shortcut.&lt;/li&gt;
&lt;li&gt;Type the content you want to send.&lt;/li&gt;
&lt;li&gt;Close it with the keyboard shortcut.

&lt;ul&gt;
&lt;li&gt;The content will be automatically copied.&lt;/li&gt;
&lt;li&gt;On macOS, focus will automatically return to the window you were previously using.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Paste and then send.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;It also supports switching between light and dark modes. I think the design is also stylish.&lt;/p&gt;

&lt;p&gt;Of course, &lt;strong&gt;you can also customize the keyboard shortcut!&lt;/strong&gt; The default is Ctrl+J, but feel free to assign any shortcut you like.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why I Made It
&lt;/h2&gt;

&lt;p&gt;Many people have probably experienced indecision about whether to use Enter, Shift+Enter, or Ctrl+Enter to send a chat message.&lt;/p&gt;

&lt;p&gt;And then, accidentally sending a message in the middle of writing...&lt;/p&gt;

&lt;p&gt;Using some kind of editor for drafting isn't bad, but it can be a bit cumbersome to constantly be asked to create a new file or to manage windows.&lt;/p&gt;

&lt;p&gt;This app is designed for throwaway writing, and it allows for displaying/hiding the window and copying content with a single shortcut, making it quite fast.&lt;/p&gt;
&lt;h2&gt;
  
  
  Challenges During Development
&lt;/h2&gt;

&lt;p&gt;There were several challenges.&lt;/p&gt;

&lt;p&gt;However, the most difficult problem was dealing with code signing and notarization.&lt;/p&gt;

&lt;p&gt;When you create an app with Electron, it works fine on your local machine, but if you upload it to the internet and download it, the app becomes corrupted.&lt;/p&gt;

&lt;p&gt;You might have seen this warning when downloading things like Docker:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F44djmq4zivqd2qccfzyq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F44djmq4zivqd2qccfzyq.png" alt="warning"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Therefore, you need to use the &lt;code&gt;xattr&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://osxhub.com/fix-macos-app-damaged-and-cant-be-opened-issue-guide/" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.osxhub.com%2F2025%2F06%2Fmacos-app-damaged-and-cant-be-opend-issue" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://osxhub.com/fix-macos-app-damaged-and-cant-be-opened-issue-guide/" rel="noopener noreferrer" class="c-link"&gt;
            Fix macOS app is damaged and can't be opened" Error - Complete Guide - osxhub
          &lt;/a&gt;
        &lt;/h2&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fcdn.osxhub.com%2Fuploads%2F2025%2F06%2Ffavicon.png"&gt;
          osxhub.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;





&lt;p&gt;This means you need to run the command &lt;code&gt;xattr -rc "/Applications/hoge.app"&lt;/code&gt; after installation, which is a bit troublesome.&lt;/p&gt;

&lt;p&gt;When I looked into simplifying this, I realized that using &lt;code&gt;postflight&lt;/code&gt; could automate this process if installed via homebrew-tap.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/lef237/homebrew-tap/blob/main/Casks/one-time-editor.rb" rel="noopener noreferrer"&gt;https://github.com/lef237/homebrew-tap/blob/main/Casks/one-time-editor.rb&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're looking to distribute Electron apps, please use this as a reference!&lt;/p&gt;

&lt;h2&gt;
  
  
  GitHub
&lt;/h2&gt;

&lt;p&gt;GitHub is also public.&lt;/p&gt;

&lt;p&gt;If you have any feature requests, please open an Issue. 🐘&lt;/p&gt;

&lt;p&gt;

&lt;/p&gt;
&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/lef237" rel="noopener noreferrer"&gt;
        lef237
      &lt;/a&gt; / &lt;a href="https://github.com/lef237/one-time-editor" rel="noopener noreferrer"&gt;
        one-time-editor
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      A lightweight scratchpad that lives one shortcut away
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;
&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;One-Time Editor&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;A lightweight scratchpad that lives one shortcut away. Draft a message, hit the shortcut again, and it's already on your clipboard — ready to paste anywhere.&lt;/p&gt;
&lt;p&gt;Built for the workflow of writing chat messages, AI prompts, and quick notes that you type once and send.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://youtu.be/qwj9fr77vQg" rel="nofollow noopener noreferrer"&gt;&lt;img src="https://camo.githubusercontent.com/831acaaa14e6e538238c6c9af7341628428f9638d6ebab57e7052245eaf3937b/68747470733a2f2f696d672e796f75747562652e636f6d2f76692f71776a39667237377651672f6d617872657364656661756c742e6a7067" alt="Demo"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://youtu.be/qwj9fr77vQg" rel="nofollow noopener noreferrer"&gt;https://youtu.be/qwj9fr77vQg&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Install&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;macOS (Homebrew)&lt;/h3&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;brew tap lef237/tap
brew install --cask one-time-editor&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;Manual download&lt;/h3&gt;

&lt;/div&gt;
&lt;p&gt;Pre-built binaries for macOS, Windows, and Linux are available on the &lt;a href="https://github.com/lef237/one-time-editor/releases" rel="noopener noreferrer"&gt;Releases&lt;/a&gt; page.&lt;/p&gt;
&lt;p&gt;If you download manually on macOS, the app is not signed with an Apple Developer certificate, so macOS may show a warning. To allow it, run:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;xattr -cr &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;/Applications/One-Time Editor.app&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;How it works&lt;/h2&gt;

&lt;/div&gt;
&lt;ol&gt;
&lt;li&gt;Press &lt;code&gt;Ctrl+J&lt;/code&gt; to summon the editor&lt;/li&gt;
&lt;li&gt;Type your text&lt;/li&gt;
&lt;li&gt;Press the shortcut again — the window disappears and your text is copied to clipboard&lt;/li&gt;
&lt;li&gt;Paste wherever you need it&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That's it. No save dialog, no file management, no friction.&lt;/p&gt;
&lt;p&gt;The shortcut is…&lt;/p&gt;
&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/lef237/one-time-editor" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;




&lt;p&gt;&lt;a href="https://github.com/lef237/one-time-editor" rel="noopener noreferrer"&gt;https://github.com/lef237/one-time-editor&lt;/a&gt;&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;If it doesn't work on Windows or other OSs, please comment on GitHub! ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>productivity</category>
      <category>showdev</category>
      <category>sideprojects</category>
      <category>tooling</category>
    </item>
    <item>
      <title>Writing a Domain Model in Ruby Without Using class</title>
      <dc:creator>lef237</dc:creator>
      <pubDate>Wed, 13 Aug 2025 10:47:28 +0000</pubDate>
      <link>https://dev.to/lef237/writing-a-domain-model-in-ruby-without-using-class-5h06</link>
      <guid>https://dev.to/lef237/writing-a-domain-model-in-ruby-without-using-class-5h06</guid>
      <description>&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;From Ruby 3.2, the &lt;code&gt;Data&lt;/code&gt; class has been introduced, allowing us to explore ways of representing domain models without using &lt;code&gt;class&lt;/code&gt;. In this article, I’ll examine how we can do that.&lt;/p&gt;

&lt;p&gt;When writing business logic in Ruby, we naturally tend to use &lt;code&gt;class&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We set attributes in &lt;code&gt;initialize&lt;/code&gt;, and change state via instance methods. Especially if you’ve been writing Rails code, this feels natural.&lt;/p&gt;

&lt;p&gt;However, whether managing logic with a stateful class is suitable for &lt;em&gt;every&lt;/em&gt; case is debatable. For domain logic where we want to avoid side effects, class-based design might not always be optimal. In recent years, designs influenced by functional programming have gained attention, and as has been discussed on X (formerly Twitter), the mainstream approach in TypeScript is moving toward avoiding classes entirely.&lt;/p&gt;

&lt;p&gt;With the &lt;code&gt;Data&lt;/code&gt; class introduced in Ruby 3.2, we can represent domains while keeping structural data and logic separate. In this article, I’ll show you how.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; This article focuses on &lt;em&gt;representing domain logic&lt;/em&gt;, separate from Rails' ActiveRecord models.&lt;br&gt;&lt;br&gt;
I’m specifically looking at a style that works purely with plain Ruby syntax, without being tied to the Rails environment. This is driven more by curiosity than by real-world constraints, so please keep that in mind.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Is the &lt;code&gt;Data&lt;/code&gt; Class?
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;Data&lt;/code&gt; class introduced in Ruby 3.2 lets you define immutable data structures easily. It’s similar to the &lt;a href="https://docs.ruby-lang.org/en/master/Struct.html" rel="noopener noreferrer"&gt;Struct&lt;/a&gt; that Ruby already had, but all fields are immutable — to change a value, you must create a new instance.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://docs.ruby-lang.org/en/master/Data.html" rel="noopener noreferrer"&gt;class Data - Documentation for Ruby&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To use a loose analogy: Struct is like a hash, while Data is like a read-only hash.&lt;/p&gt;

&lt;p&gt;Let’s actually design a domain model using the &lt;code&gt;Data&lt;/code&gt; class.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example Domain Model
&lt;/h2&gt;

&lt;p&gt;First, let’s think of an example.&lt;/p&gt;

&lt;p&gt;We’ll imagine a &lt;code&gt;Customer&lt;/code&gt; model with the following attributes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;id&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;email&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;first_name&lt;/code&gt; / &lt;code&gt;last_name&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;is_active&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;created_at&lt;/code&gt; / &lt;code&gt;updated_at&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We want to be able to perform operations such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Changing the name&lt;/li&gt;
&lt;li&gt;Changing the email address&lt;/li&gt;
&lt;li&gt;Activating/deactivating the customer&lt;/li&gt;
&lt;li&gt;Getting the full name&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Especially the first three operations would naturally become methods that change state.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Class-Based Implementation
&lt;/h2&gt;

&lt;p&gt;First, let’s design it in the usual class-based way. You might find there are too many instance variables, among other nitpicks, but let’s just write it out as a sample.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Customer&lt;/span&gt;
  &lt;span class="nb"&gt;attr_reader&lt;/span&gt; &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:is_active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:updated_at&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="ss"&gt;is_active: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;validate_name!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;validate_email!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;
    &lt;span class="vi"&gt;@email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;
    &lt;span class="vi"&gt;@first_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;
    &lt;span class="vi"&gt;@last_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;
    &lt;span class="vi"&gt;@is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;is_active&lt;/span&gt;
    &lt;span class="vi"&gt;@created_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
    &lt;span class="vi"&gt;@updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;validate_name!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@first_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;
    &lt;span class="vi"&gt;@last_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;
    &lt;span class="n"&gt;touch&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;validate_email!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="vi"&gt;@email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;
    &lt;span class="n"&gt;touch&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deactivate&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="vi"&gt;@is_active&lt;/span&gt;
    &lt;span class="vi"&gt;@is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
    &lt;span class="n"&gt;touch&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;activate&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_active&lt;/span&gt;
    &lt;span class="vi"&gt;@is_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
    &lt;span class="n"&gt;touch&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;full_name&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="kp"&gt;private&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;touch&lt;/span&gt;
    &lt;span class="vi"&gt;@updated_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_name!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;
      &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;ArgumentError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Invalid name"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_email!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/\A[^@\s]+@[^@\s]+\z/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;ArgumentError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Invalid email format"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can be used as follows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="s2"&gt;"c1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="s2"&gt;"alice@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;first_name: &lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;last_name: &lt;/span&gt;&lt;span class="s2"&gt;"Anderson"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;change_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Brown"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deactivate&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;full_name&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; "Bob Brown"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, we create an instance with &lt;code&gt;new&lt;/code&gt; and call methods to change its state — a very common approach.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;Data&lt;/code&gt; Class Available from Ruby 3.2
&lt;/h2&gt;

&lt;p&gt;In Ruby 3.2, the &lt;code&gt;Data&lt;/code&gt; class was introduced.&lt;/p&gt;

&lt;p&gt;It feels similar to &lt;code&gt;Struct&lt;/code&gt;, but all fields are immutable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;:is_active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:updated_at&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To change a value, you use &lt;code&gt;.with&lt;/code&gt; to create a new instance.&lt;/p&gt;

&lt;p&gt;Suppose &lt;code&gt;customer1&lt;/code&gt; has &lt;code&gt;first_name&lt;/code&gt; set to &lt;code&gt;Bob&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;customer1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="s2"&gt;"c1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="s2"&gt;"bob@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;first_name: &lt;/span&gt;&lt;span class="s2"&gt;"Bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;last_name: &lt;/span&gt;&lt;span class="s2"&gt;"Johnson"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;is_active: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;created_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;updated_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To change &lt;code&gt;first_name&lt;/code&gt; to &lt;code&gt;Carol&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;customer2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;customer1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;first_name: &lt;/span&gt;&lt;span class="s2"&gt;"Carol"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;customer1&lt;/code&gt; remains unchanged; &lt;code&gt;customer2&lt;/code&gt; is a new object with only &lt;code&gt;first_name&lt;/code&gt; different.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;customer1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first_name&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; "Bob"&lt;/span&gt;
&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="n"&gt;customer2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first_name&lt;/span&gt;  &lt;span class="c1"&gt;# =&amp;gt; "Carol"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;Data.with&lt;/code&gt;, we handle state not by mutation but by replacement, avoiding unintended side effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing Domain Logic with &lt;code&gt;Data&lt;/code&gt; and Functions
&lt;/h2&gt;

&lt;p&gt;Let’s rewrite the earlier class-based code using &lt;code&gt;Data&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;We’ll define a &lt;code&gt;CustomerService&lt;/code&gt; module, separate from &lt;code&gt;Customer&lt;/code&gt;, to hold our domain operations as functions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="no"&gt;Customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;define&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;:id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;:is_active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:created_at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:updated_at&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;CustomerService&lt;/span&gt;
  &lt;span class="kp"&gt;module_function&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;create_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;validate_name!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;validate_email!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
    &lt;span class="no"&gt;Customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="n"&gt;now&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;validate_name!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="ss"&gt;first_name: &lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;last_name: &lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="ss"&gt;updated_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;change_email&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;validate_email!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;updated_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;deactivate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_active&lt;/span&gt;
    &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;is_active: &lt;/span&gt;&lt;span class="kp"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;updated_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;activate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_active&lt;/span&gt;
    &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;is_active: &lt;/span&gt;&lt;span class="kp"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;updated_at: &lt;/span&gt;&lt;span class="no"&gt;Time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_name!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;empty?&lt;/span&gt;
      &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;ArgumentError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Invalid name"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_email!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;unless&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/\A[^@\s]+@[^@\s]+\z/&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;ArgumentError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"Invalid email format"&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here, &lt;code&gt;create_customer&lt;/code&gt; is used as a factory in &lt;code&gt;CustomerService&lt;/code&gt;. By defining domain operations as functions, we can handle changes to state in a way that minimizes side effects.&lt;/p&gt;

&lt;p&gt;Each function doesn’t change the &lt;code&gt;Customer&lt;/code&gt; data structure directly — it uses &lt;code&gt;.with&lt;/code&gt; to create a new instance. This preserves data immutability and brings the design closer to a functional style.&lt;/p&gt;

&lt;p&gt;Also, by using &lt;code&gt;module_function&lt;/code&gt;, we can call these functions without creating an instance of the module. Validation is performed inside the functions where needed.&lt;/p&gt;

&lt;p&gt;This cleanly separates data (&lt;code&gt;Customer&lt;/code&gt;) from behavior (functions).&lt;/p&gt;

&lt;h3&gt;
  
  
  Example Usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CustomerService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;id: &lt;/span&gt;&lt;span class="s2"&gt;"c1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;email: &lt;/span&gt;&lt;span class="s2"&gt;"alice@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;first_name: &lt;/span&gt;&lt;span class="s2"&gt;"Alice"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;last_name: &lt;/span&gt;&lt;span class="s2"&gt;"Anderson"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CustomerService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;change_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;first_name: &lt;/span&gt;&lt;span class="s2"&gt;"Bob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;last_name: &lt;/span&gt;&lt;span class="s2"&gt;"Brown"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;customer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;CustomerService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;deactivate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="no"&gt;CustomerService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;full_name&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# =&amp;gt; "Bob Brown"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note that when reassigning, we return a &lt;em&gt;new&lt;/em&gt; object instead of modifying the original. I considered naming them &lt;code&gt;customer_updated&lt;/code&gt;, &lt;code&gt;customer_updated2&lt;/code&gt;, etc., but left it as-is for now.&lt;/p&gt;

&lt;p&gt;By not using instance variables and passing all state through arguments, we avoid side effects. At that point, all that’s left is to store the final state in the database or handle it however needed.&lt;/p&gt;

&lt;p&gt;For example, saving with SQLite might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="s2"&gt;"INSERT OR REPLACE INTO customers (id, email, first_name, last_name, is_active, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;is_active&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;created_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iso8601&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;customer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;updated_at&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;iso8601&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can access attributes with &lt;code&gt;customer.xxx&lt;/code&gt;, so it feels similar to using a class.&lt;/p&gt;

&lt;h2&gt;
  
  
  Points to Note
&lt;/h2&gt;

&lt;p&gt;Of course, there’s nothing wrong with using &lt;code&gt;class&lt;/code&gt;. For UI logic, controllers, database models like ActiveRecord, and so on, class-based design often makes sense.&lt;/p&gt;

&lt;p&gt;In Rails projects, it’s common to extract structured data handling into POROs (Plain Old Ruby Objects) written as classes.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/sulmanweb/plain-old-ruby-objects-poros-in-rails-fat-models-3l7f"&gt;Plain Old Ruby Objects (POROs) in Rails Fat Models - DEV Community&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;However, as domain logic grows more complex, tracking “state changes” through the code becomes harder. In such cases, a design where “values are immutable and logic is functional” — like the one shown here — can make maintenance easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Ruby is a flexible language, and you can represent domain logic without relying on classes or instance variables.&lt;/p&gt;

&lt;p&gt;Ruby 3.2’s &lt;code&gt;Data&lt;/code&gt; class can be a useful tool for designing the domain layer as structured data plus explicit functions. You don’t have to use it for every case, but simply knowing it’s an option can broaden your design possibilities.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
      <category>ruby</category>
    </item>
  </channel>
</rss>
