<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[David Montesdeoca]]></title><description><![CDATA[Passionate software engineer and continuous learner]]></description><link>https://davidmontesdeoca.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 15 Apr 2026 23:47:27 GMT</lastBuildDate><atom:link href="https://davidmontesdeoca.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[The one about multi-API support in legacy code]]></title><description><![CDATA[In a previous post I mentioned I had been working on replicating some functionality for collecting beneficiaries data in an old SPA.
The code in that application was quite legacy and highly coupled to]]></description><link>https://davidmontesdeoca.dev/the-one-about-multi-api-support-in-legacy-code</link><guid isPermaLink="true">https://davidmontesdeoca.dev/the-one-about-multi-api-support-in-legacy-code</guid><category><![CDATA[Ruby]]></category><category><![CDATA[sinatrarb]]></category><category><![CDATA[legacy code]]></category><category><![CDATA[spa]]></category><category><![CDATA[api]]></category><category><![CDATA[proxy]]></category><category><![CDATA[refactoring]]></category><category><![CDATA[Testing]]></category><category><![CDATA[React]]></category><dc:creator><![CDATA[David Montesdeoca]]></dc:creator><pubDate>Mon, 23 Mar 2026 20:14:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/642d433eeaad3d174f737099/30c7fd89-597f-4546-9b33-3814549dd7b3.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In a <a href="https://davidmontesdeoca.dev/the-one-about-a-new-project-working-for-an-american-fintech">previous post</a> I mentioned I had been working on replicating some functionality for collecting beneficiaries data in an old SPA.</p>
<p><strong>The code in that application was quite legacy and highly coupled to an existing API.</strong> It was poorly tested, and even those tests did not have a make command to run locally nor were they running in the CI pipeline.</p>
<p>It consisted of a <a href="https://sinatrarb.com/">Sinatra</a> application that acts as a proxy between a React frontend and a backend API. For a long time, this proxy only knew about one API, so everything was built around that single dependency.</p>
<p>Then I needed to add support for a new API. But first, I had to isolate anything that was coupled to the existing API, thus making room for the upcoming changes.</p>
<blockquote>
<p>In the examples below, I have omitted code that is unnecessary to understand the process, such as logging and observability instrumentation.</p>
</blockquote>
<h2>The initial setup</h2>
<p>The backend application is very simple. The entry point is <code>config.ru</code>, which maps routes to different Rack apps via a Dispatcher:</p>
<pre><code class="language-ruby"># system/infrastructure/dispatcher.rb
module Infrastructure
  Dispatcher = Rack::Builder.app do
    map '/' do
      run System::Server
    end

    map '/api' do
      run System::Proxy
    end
  end
end
</code></pre>
<p>The server handles static file serving for the React SPA:</p>
<pre><code class="language-ruby"># system/server.rb
module System
  class Server &lt; Sinatra::Base
    set root: File.expand_path('..', __dir__)

    not_found do
      render_static_file
    end

    get '/' do
      render_static_file
    end

    private

    def render_static_file
      File.read(File.join('public', 'index.html'))
    end
  end
end
</code></pre>
<p>The proxy delegates to a request handler, where the actual API proxying happens:</p>
<pre><code class="language-ruby"># system/request_handler.rb
module System
  class RequestHandler
    UNAUTHORIZED_CODE = 401
    ALLOWED_PATHS = %w(
      /beneficiary_collections
    )
    ALLOWED_METHODS = %w(GET PUT PATCH POST)
    HEADERS = { 'Content-Type' =&gt; 'application/json' }

    def initialize(env)
      @request = Rack::Request.new(env)
    end

    def call
      return render(UNAUTHORIZED_CODE, { error: 'Unauthorized' }.to_json) if invalid_path &amp;&amp; invalid_request_method

      response = # HTTParty logic

      render(response.code, response.body)
    end

    private

    def invalid_path
      !ALLOWED_PATHS.include?(path)
    end

    def invalid_request_method
      !ALLOWED_METHODS.include?(@request.request_method)
    end

    def render(status, body)
      [status, HEADERS, [body]]
    end

    def api_url
      ENV['API_BASE_URL']
    end

    # other private methods omitted
  end
end
</code></pre>
<p>If you look closely at the <code>call</code> method, the guard clause uses <code>&amp;&amp;</code> instead of <code>||</code>. This means it only returns <em>unauthorized</em> when both the path and the method are invalid, not when either one is. This was a bug that was present from the start and that I would fix later as part of this process.</p>
<p>Notice that <code>api_url</code> always returns the same environment variable regardless of the request path. The whole request handler assumes there is only one API.</p>
<h2>First changes: routing through the proxy</h2>
<p>Here is something I had not noticed before: <strong>in the local development environment, the frontend was bypassing the proxy entirely</strong>. The webpack dev server was configured to send API requests directly to the API stub.</p>
<p>The webpack dev server configuration was as follows:</p>
<pre><code class="language-javascript">// client/webpack/webpack.development.js
const API_BASE_URL = process.env.API_BASE_URL || 'http://beneficiary_collections_api:5300';

module.exports = {
  // ...
  devServer: {
    host: '0.0.0.0',
    port: 3001,
    allowedHosts: 'all',
    historyApiFallback: true,
    proxy: {
      '/api': {
        target: API_BASE_URL,
        pathRewrite: { '^/api': '' }
      },
      // ...
    }
  }
}
</code></pre>
<p>The <code>pathRewrite</code> stripped the <code>/api</code> prefix and sent requests straight to the stub. This meant the Sinatra proxy was never involved in local development. Consequently, <strong>you cannot test routing if you are skipping the proxy</strong>.</p>
<p>To fix this, I added a backend service to <code>docker-compose.yml</code> and renamed <code>app</code> to <code>frontend</code>:</p>
<pre><code class="language-yaml">services:
  frontend:
    ports:
      - 3001:3001
    depends_on:
      - backend
    command: npm start

  backend:
    ports:
      - 8080:8080
    depends_on:
      - beneficiary_collections_api
    command: bundle exec rerun --dir . --pattern "**/*.{rb}" --background -- rackup --port=8080 -o 0.0.0.0 config.ru

  beneficiary_collections_api:
    build:
      context: ./stubs/beneficiary_collections_api
    ports:
      - 5300:5300
    command: bundle exec rerun --dir . --background -- rackup --port=5300 -o 0.0.0.0 config.ru
</code></pre>
<p>I installed the <a href="https://github.com/alexch/rerun">rerun gem</a> so the backend application would automatically restart when any Ruby file changed.</p>
<blockquote>
<p>Note that the page still needs to be reloaded in the browser. Until the backend is fully reloaded, requests will return a 504 error.</p>
</blockquote>
<p>Then I updated the webpack dev server to point at the backend instead of the API stub directly:</p>
<pre><code class="language-javascript">// client/webpack/webpack.development.js
const BACKEND_URL = process.env.BACKEND_URL || 'http://backend:8080';

module.exports = {
  // ...
  devServer: {
    host: '0.0.0.0',
    port: 3001,
    allowedHosts: 'all',
    historyApiFallback: true,
    proxy: {
      '/api': {
        target: BACKEND_URL
      },
      // ...
    }
  }
}
</code></pre>
<p>No more <code>pathRewrite</code>. The <code>/api</code> prefix is now kept as-is, and the backend proxy handles it from there. This matches how the application works in staging and production.</p>
<p>Finally, I refactored the scarce tests to route them through the <code>Dispatcher</code> instead of testing the proxy in isolation, and added the make command to be able to run those tests locally:</p>
<pre><code class="language-ruby"># spec/requests/api/proxy_spec.rb
RSpec.describe 'Proxy' do
  include Rack::Test::Methods

  def app
    Infrastructure::Dispatcher
  end

  context 'when beneficiary_collections' do
    let(:base_url) { 'https://beneficiary-collections.lol' }

    before do
      allow(ENV).to receive(:[]).with('API_BASE_URL').and_return(base_url)

      stub_request(:get, "#{base_url}/beneficiary_collections")
        .to_return(status: 200, body: { message: 'Success' }.to_json)

      stub_request(:get, "#{base_url}/beneficiary_collections/invalid")
        .to_return(status: 401, body: { message: 'Unauthorized' }.to_json)
    end

    context 'when the URL is allowed' do
      it 'returns a 200 status code' do
        get '/api/beneficiary_collections'

        expect(last_response.status).to eq(200)
      end
    end

    context 'when the URL is not allowed' do
      it 'returns a 401 status code' do
        get '/api/beneficiary_collections/invalid'

        expect(last_response.status).to eq(401)
      end
    end
  end
end
</code></pre>
<h2>Isolating the old API</h2>
<p>I applied a set of changes in both the frontend and the backend.</p>
<p>The key backend change was making <code>api_url</code> conditional in the request handler instead of returning always the same environment variable (properly renamed below):</p>
<pre><code class="language-ruby"># system/request_handler.rb
def api_url
  if path.start_with?('/beneficiary_collections')
    ENV['BENEFICIARY_COLLECTIONS_API_BASE_URL']
  end
end
</code></pre>
<p>The tests were properly updated, too, adding a specific context for the beneficiary collections API.</p>
<p>On the frontend side, the template that renders the beneficiary collection form was extracted from the page where it was being rendered, so it could be easily duplicated later. I also updated the existing test suite using <a href="https://testing-library.com/docs/react-testing-library/intro/">React Testing Library</a>, adding a new context to ensure the extracted components still behaved correctly in isolation.</p>
<h2>Adding the new API</h2>
<p>With the groundwork done, adding the new API was straightforward.</p>
<p>On the React side, a new route was added:</p>
<pre><code class="language-javascript">// client/src/pages/App/App.js
import Home from 'pages/Home';
import CollectionRequestsHome from 'pages/CollectionRequestsHome';

function App() {
  return (
    &lt;Router&gt;
      &lt;ContextWrapper&gt;
        &lt;Switch&gt;
          &lt;Route exact path="/" component={Home} /&gt;
          &lt;Route exact path="/collection_requests" component={CollectionRequestsHome} /&gt;
        &lt;/Switch&gt;
      &lt;/ContextWrapper&gt;
    &lt;/Router&gt;
  );
}

export default App;
</code></pre>
<p>All components were duplicated and adapted to the collection requests form, including the endpoints specific to the new API. Every change on the frontend was accompanied by new unit and integration tests.</p>
<p>On the backend, the request handler received several improvements:</p>
<ul>
<li><p>All constants are frozen.</p>
</li>
<li><p>The status code for invalid requests was changed from 401 to 404, which is more semantically correct.</p>
</li>
<li><p>The validation methods became proper predicate methods.</p>
</li>
<li><p>An attribute reader was added for the request.</p>
</li>
<li><p>The <code>api_url</code> method now routes based on the request path prefix.</p>
</li>
</ul>
<pre><code class="language-ruby"># system/request_handler.rb
module System
  class RequestHandler
    NOT_FOUND_STATUS_CODE = 404
    ALLOWED_PATHS = %w[
      /beneficiary_collections
      /collection_requests
    ].freeze
    ALLOWED_METHODS = %w[GET PUT PATCH POST].freeze
    HEADERS = { 'Content-Type' =&gt; 'application/json' }

    def initialize(env)
      @request = Rack::Request.new(env)
    end

    def call
      if invalid_path_info? || invalid_request_method?
        return render(NOT_FOUND_STATUS_CODE, { error: 'Not found' }.to_json)
      end

      response = # HTTParty logic

      render(response.code, response.body)
    end

    private

    attr_reader :request

    def invalid_path_info?
      !(request.path_info.match?(Regexp.union(ALLOWED_PATHS)))
    end

    def invalid_request_method?
      !(ALLOWED_METHODS.include?(request.request_method))
    end

    def render(status, body)
      [status, HEADERS, [body]]
    end

    def api_url
      if path.start_with?('/beneficiary_collections')
        ENV['BENEFICIARY_COLLECTIONS_API_BASE_URL']
      elsif path.start_with?('/collection_requests')
        ENV['COLLECTION_REQUESTS_API_BASE_URL']
      end
    end

    # other private methods omitted
  end
end
</code></pre>
<p>The server needed a new route too:</p>
<pre><code class="language-ruby"># system/server.rb
get '/collection_requests/' do
  render_static_file
end
</code></pre>
<p>That trailing slash on <code>get '/collection_requests/'</code> will come back to haunt me.</p>
<p>A new stub application was added for the collection requests API, and <code>docker-compose.yml</code> was updated to include it:</p>
<pre><code class="language-yaml">services:
  collection_requests_api:
    build:
      context: ./stubs/collection_requests_api
    ports:
      - 5301:5301
    command: bundle exec rerun --dir . --background -- rackup --port=5301 -o 0.0.0.0 config.ru
</code></pre>
<p>A new context block for the collection requests API was added to the proxy test, mirroring the beneficiary collections one.</p>
<h2>The trailing slash problem</h2>
<p>The code was working perfectly locally. Tests were green. It got merged and deployed to staging. And then <strong>the collection requests form did not render</strong>.</p>
<p>The issue turned out to be a trailing slash added to the route, while React router navigated to <code>/collection_requests</code>, without the trailing slash.</p>
<p>Sinatra does not treat these the same way by default, and it is well documented in <a href="https://sinatrarb.com/faq.html#slash">their FAQ section</a>.</p>
<p>The fix was to make the route accept both:</p>
<pre><code class="language-ruby"># system/server.rb
get '/collection_requests/?' do
  render_static_file
end
</code></pre>
<p>The <code>/?</code> at the end makes the trailing slash optional. The <code>not_found</code> handler was also restored so that unknown routes would still serve the SPA. It returns a 404 status code so the React app can show its own not found page.</p>
<p>On the frontend, the webpack dev server configuration also changed. The <code>historyApiFallback</code> option was replaced with a custom middleware that validates routes and handles trailing slashes:</p>
<pre><code class="language-javascript">// client/webpack/webpack.development.js
const VALID_PATHS = ['/', '/collection_requests'];

module.exports = {
  historyApiFallback: false,
  setupMiddlewares: (middlewares, devServer) =&gt; {
    devServer.app.get('*', (req, res, next) =&gt; {
      if (req.headers.accept?.includes('text/html')) {
        const normalizedPath = req.path.replace(/\/$/, '') || '/';

        if (!VALID_PATHS.includes(normalizedPath)) {
          res.status(404);
        }

        req.url = '/index.html';
      }

      next();
    });

    return middlewares;
  },
</code></pre>
<p>Not very elegant, but it works.</p>
<p>The React app also got a catch-all route for unknown paths, with a <code>NotFound</code> component:</p>
<pre><code class="language-javascript">// client/src/pages/App/App.js
import NotFound from 'pages/NotFound';

// ...
&lt;Route path="*" component={NotFound} /&gt;
</code></pre>
<p>I also added minimal testing for the server to cover both with and without trailing slash:</p>
<pre><code class="language-ruby"># spec/requests/server_spec.rb
RSpec.describe 'Server' do
  include Rack::Test::Methods

  def app
    Infrastructure::Dispatcher
  end

  before do
    allow(File).to receive(:read).and_call_original
    allow(File).to receive(:read).with(File.join('public', 'index.html')).and_return('html content')
  end

  context 'when path is /' do
    it 'returns a 200 status code and HTML content' do
      get '/'

      expect(last_response.status).to eq(200)
      expect(last_response.headers['Content-Type']).to include('text/html')
    end
  end

  context 'when path is /collection_requests' do
    it 'returns a 200 status code and HTML content' do
      get '/collection_requests'

      expect(last_response.status).to eq(200)
      expect(last_response.headers['Content-Type']).to include('text/html')
    end
  end

  context 'when path is /collection_requests/' do
    it 'returns a 200 status code and HTML content' do
      get '/collection_requests/'

      expect(last_response.status).to eq(200)
      expect(last_response.headers['Content-Type']).to include('text/html')
    end
  end

  context 'when path is invalid' do
    it 'returns a 404 status code and HTML content' do
      get '/invalid'

      expect(last_response.status).to eq(404)
      expect(last_response.headers['Content-Type']).to include('text/html')
    end
  end
end
</code></pre>
<p>To prevent this from happening again, I added some make commands to run the application in production mode locally:</p>
<pre><code class="language-yaml"># docker-compose.prod.yml
services:
  app:
    ports:
      - 8080:80
    depends_on:
      - beneficiary_collections_api
      - collection_requests_api
    environment:
      - NODE_ENV=production

  beneficiary_collections_api:
    build:
      context: ./stubs/beneficiary_collections_api
    ports:
      - 5300:5300
    volumes:
      - ./stubs/beneficiary_collections_api:/opt/stubs
    command: bundle exec rerun --dir . --background -- rackup --port=5300 -o 0.0.0.0 config.ru

  collection_requests_api:
    build:
      context: ./stubs/collection_requests_api
    ports:
      - 5301:5301
    volumes:
      - ./stubs/collection_requests_api:/opt/stubs
    command: bundle exec rerun --dir . --background -- rackup --port=5301 -o 0.0.0.0 config.ru
</code></pre>
<p>From now on, catching the kind of failure caused by trailing slashes locally is way easier.</p>
<h2>Conclusion</h2>
<p>Adding support for a new API provided an opportunity to harden a legacy application. We moved from a poorly tested setup that bypassed its own proxy locally to a robust environment with proper routing and test coverage.</p>
<p>The challenges I faced, from the logic bug in the guard clause to the trailing slash discrepancy, demonstrate why dev-to-production parity is essential. With these changes in place, the application is no longer tied to a single dependency, and the team can now develop and test new features with the confidence that "working locally" actually means "working in production."</p>
<p>Thank you for reading and see you in the next one!</p>
]]></content:encoded></item><item><title><![CDATA[The one about rendering and displaying code examples in ERB]]></title><description><![CDATA[I have been building a component showcase page for an internal design system, using Sinatra and ERB templates. The page renders each UI component visually and, right below it, offers a toggle to revea]]></description><link>https://davidmontesdeoca.dev/the-one-about-rendering-and-displaying-code-examples-in-erb</link><guid isPermaLink="true">https://davidmontesdeoca.dev/the-one-about-rendering-and-displaying-code-examples-in-erb</guid><category><![CDATA[Ruby]]></category><category><![CDATA[ERB]]></category><category><![CDATA[Design Systems]]></category><category><![CDATA[sinatrarb]]></category><category><![CDATA[dry]]></category><dc:creator><![CDATA[David Montesdeoca]]></dc:creator><pubDate>Tue, 24 Feb 2026 18:07:14 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/642d433eeaad3d174f737099/4a71704b-945a-4785-a58e-65acf68f2309.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I have been building a component showcase page for an internal design system, using <a href="https://sinatrarb.com/">Sinatra</a> and <a href="https://ruby-doc.org/stdlib/libdoc/erb/rdoc/ERB.html">ERB</a> templates. The page renders each UI component visually and, right below it, offers a toggle to reveal the source code that produced it.</p>
<h2>The problem: code duplication</h2>
<p>The naive approach looks like this:</p>
<pre><code class="language-erb">&lt;div class="example"&gt;
  &lt;%= primary_button "Button" %&gt;
&lt;/div&gt;

&lt;%= erb :_toggle_code, locals: { code: 'primary_button "Button"' } %&gt;
</code></pre>
<p>The first block calls the Ruby helper to render the actual button. The second block passes the same call as a string to a partial that displays it:</p>
<pre><code class="language-erb">&lt;code&gt;&lt;%= code %&gt;&lt;/code&gt;
</code></pre>
<p>This works, but the code is duplicated. Update one, forget the other, and they drift apart. With dozens of examples, that drift becomes inevitable.</p>
<h2>Single source of truth</h2>
<p>The idea is straightforward: store the code as a string, then <code>eval</code> it to render the component, and display the source.</p>
<p>I wrapped the logic in a helper module:</p>
<pre><code class="language-ruby">def render_code_example(code)
  eval(code.strip, binding)
end
</code></pre>
<blockquote>
<p>Disclaimer: this is an internal tool, never use this pattern with untrusted or user-generated input.</p>
</blockquote>
<p>That worked for simple, single-line examples:</p>
<pre><code class="language-erb">&lt;div class="example"&gt;
  &lt;% example = 'primary_button "Button"' %&gt;

  &lt;%= render_code_example(example) %&gt;

  &lt;%= erb :_toggle_code, locals: { code: example } %&gt;
&lt;/div&gt;
</code></pre>
<p>However, I found that examples with multiple lines, like a fieldset containing several radio buttons, would only render the last one. <code>eval</code> returns the value of the last expression, so intermediate lines were lost:</p>
<pre><code class="language-erb">&lt;div class="example"&gt;
  &lt;%
    example = &lt;&lt;-CODE
      radio_input(name: "contact", value: "email", id: "contact_email", label: "Email")
      radio_input(name: "contact", value: "phone", id: "contact_phone", label: "Phone")
    CODE
  %&gt;

  &lt;fieldset&gt;
    &lt;legend&gt;Select your preferred contact method:&lt;/legend&gt;
    &lt;%= render_code_example(example) %&gt;
  &lt;/fieldset&gt;

  &lt;%= erb :_toggle_code, locals: { code: example } %&gt;
&lt;/div&gt;
</code></pre>
<p>The fix was to split the string into lines and eval each one independently:</p>
<pre><code class="language-ruby">def render_code_example(code)
  code.strip.split("\n").map { |line| eval(line.strip, binding) }.join("\n")
end
</code></pre>
<p>The method splits the string into lines, evaluates each one in the current <a href="https://ruby-doc.org/core/Binding.html">binding</a> (which has access to all the component helper methods), and joins the results. Each line is an independent Ruby expression that returns HTML.</p>
<p>The example is now defined once: <code>render_code_example</code> executes it, and the <code>_toggle_code</code> partial displays it.</p>
<p>That solved the rendering side, but the output code was not quite right. The <code>&lt;code&gt;</code> element collapses whitespace by default, so both radio buttons are shown in a single line:</p>
<pre><code class="language-erb">radio_input(name: "contact", value: "email", id: "contact_email", label: "Email") radio_input(name: "contact", value: "phone", id: "contact_phone", label: "Phone")
</code></pre>
<p>Wrapping the output in a <code>&lt;pre&gt;</code> tag preserves the line breaks:</p>
<pre><code class="language-erb">&lt;pre&gt;&lt;code&gt;&lt;%= code %&gt;&lt;/code&gt;&lt;/pre&gt;
</code></pre>
<p>That fixed the formatting, but introduced a new issue. The heredoc (<code>&lt;&lt;-CODE</code>) preserves the indentation from the ERB template, so the output code was rendered with leading spaces:</p>
<pre><code class="language-erb">            radio_input(name: "contact", value: "email", id: "contact_email", label: "Email")
            radio_input(name: "contact", value: "phone", id: "contact_phone", label: "Phone")
</code></pre>
<p>Switching to a <a href="https://docs.ruby-lang.org/en/master/syntax/literals_rdoc.html#here-document-literals">squiggly heredoc</a> (<code>&lt;&lt;~CODE</code>) stripped the common leading indentation automatically.</p>
<p>Finally, since the code string may contain HTML characters, the partial escapes it before rendering:</p>
<pre><code class="language-erb">&lt;pre&gt;&lt;code&gt;&lt;%= escape_code_example(code) %&gt;&lt;/code&gt;&lt;/pre&gt;
</code></pre>
<p>That new helper uses <code>Rack::Utils.escape_html</code> to handle the escaping.</p>
<p>This eliminated the duplication problem for all examples where each line is an independent Ruby expression.</p>
<h2>The new challenge: HTML components</h2>
<p>Then a new table component was needed, and instead of building a Ruby helper the first approach was a CSS-only component: plain HTML with custom styles.</p>
<pre><code class="language-html">&lt;table class="fw-table"&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th&gt;Name&lt;/th&gt;
      &lt;th&gt;Email&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Alice&lt;/td&gt;
      &lt;td&gt;alice@example.com&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
</code></pre>
<p>You cannot <code>eval</code> HTML. It is not Ruby. A different approach was needed.</p>
<p>When ERB processes a template, it appends the rendered content to a string buffer. In Sinatra, that buffer is the instance variable <a href="https://github.com/sinatra/sinatra/blob/v4.1.1/lib/sinatra/base.rb#L866">@_out_buf</a>. The idea was to record the buffer's length before a block executes, let the block run, and then slice out everything that was appended during it:</p>
<pre><code class="language-ruby">def capture_html(&amp;block)
  buffer = @_out_buf
  pos = buffer.length
  yield
  dedent(buffer.slice!(pos..-1))
end
</code></pre>
<p>The <code>slice!</code> call is important: it removes the captured content from the buffer so it is not rendered twice.</p>
<p>There was one more problem. The captured HTML carries the indentation of the ERB template it lives in. Since the code display uses a <code>&lt;pre&gt;</code> tag, that indentation shows up as unwanted leading spaces.</p>
<p>A <code>dedent</code> helper strips the common leading whitespace, normalizing the output regardless of how deeply nested the ERB block was:</p>
<pre><code class="language-ruby">def dedent(text)
  margin = text.scan(/^[ \t]*(?=\S)/).map(&amp;:size).min || 0

  text.gsub(/^[ \t]{#{margin}}/, '').strip
end
</code></pre>
<p>With those two helpers in place, the ERB usage followed the same single-definition pattern:</p>
<pre><code class="language-erb">&lt;% example = capture_html do %&gt;
  &lt;table class="fw-table"&gt;
    &lt;thead&gt;
      &lt;tr&gt;
        &lt;th&gt;Name&lt;/th&gt;
        &lt;th&gt;Email&lt;/th&gt;
      &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
      &lt;tr&gt;
        &lt;td&gt;Alice&lt;/td&gt;
        &lt;td&gt;alice@example.com&lt;/td&gt;
      &lt;/tr&gt;
    &lt;/tbody&gt;
  &lt;/table&gt;
&lt;% end %&gt;

&lt;%= example %&gt;

&lt;%= erb :_toggle_code, locals: { code: example } %&gt;
</code></pre>
<p>Write the HTML once. <code>capture_html</code> grabs it as a string. Output it for rendering, pass it for display.</p>
<h2>Unification: block detection</h2>
<p>The CSS-only table did not last long. It was eventually replaced with a Ruby block DSL, where the table is built through nested method calls:</p>
<pre><code class="language-ruby">table do
  table_head do
    table_row do
      table_header_cell("Name")
      table_header_cell("Email")
    end
  end
  table_body do
    table_row do
      table_cell("Alice")
      table_cell("alice@example.com")
    end
  end
end
</code></pre>
<p>Now <code>render_code_example</code> had to handle this too. But unlike the radio buttons from before, these lines are not independent expressions. They form a single <code>do...end</code> block, and evaluating <code>table do</code> in isolation is a syntax error.</p>
<p>The fix was to detect whether the code is a block expression and, if so, evaluate it as a whole instead of line by line:</p>
<pre><code class="language-ruby">def render_code_example(code)
  stripped = code.strip

  if block_expression?(stripped)
    eval(stripped, binding)
  else
    stripped.split("\n").map { |line| eval(line.strip, binding) }.join("\n")
  end
end

private

def block_expression?(code)
  code.match?(/\bend\z/)
end
</code></pre>
<p>The check is simple: if the code ends with the <code>end</code> keyword, treat it as a block. <code>\b</code> ensures it matches the whole word, and <code>\z</code> anchors to the end of the string.</p>
<p>In the ERB template, multi-line code uses a <a href="https://ruby-doc.org/core/doc/syntax/literals_rdoc.html#label-Here+Documents+-28Heredocs-29">heredoc</a> to keep things readable:</p>
<pre><code class="language-erb">&lt;%
  example = &lt;&lt;~CODE
    table do
      table_head do
        table_row do
          table_header_cell("Name")
          table_header_cell("Email")
        end
      end
      table_body do
        table_row do
          table_cell("Alice")
          table_cell("alice@example.com")
        end
      end
    end
  CODE
%&gt;

&lt;%= render_code_example(example) %&gt;

&lt;%= erb :_toggle_code, locals: { code: example } %&gt;
</code></pre>
<p>With this change, <code>capture_html</code> and <code>dedent</code> were no longer needed. Every component in the showcase now uses Ruby helpers, so the buffer-interception approach was removed entirely.</p>
<h2>Conclusion</h2>
<p>The core idea never changed: define each example once, then use it for both rendering and display. What evolved was the mechanism: from <code>eval</code> on single lines, to line-by-line splitting, to buffer interception for raw HTML, and finally to block detection when the Ruby DSL arrived. Each iteration solved a real problem introduced by the previous one.</p>
<p>The final code is straightforward, but it earned that simplicity one edge case at a time:</p>
<pre><code class="language-ruby">module Helpers
  module CodeExample
    def escape_code_example(*strings)
      Rack::Utils.escape_html(strings.join("\n\n"))
    end

    def render_code_example(code)
      stripped = code.strip

      if block_expression?(stripped)
        eval(stripped, binding)
      else
        stripped.split("\n").map { |line| eval(line.strip, binding) }.join("\n")
      end
    end

    private

    def block_expression?(code)
      code.match?(/\bend\z/)
    end
  end
end
</code></pre>
<p>If you have an interesting alternative approach to this problem, feel free to share it with me.</p>
<p>Thank you for reading and see you in the next one!</p>
]]></content:encoded></item><item><title><![CDATA[The one with a guide for configuring a proxy-email service]]></title><description><![CDATA[Disclaimer: This is not a sponsored post.

I recently registered the domain davidmontesdeoca.dev through sav.com. At the time of this writing, they offer a highly competitive price of $4.99 for the first year, with a renewal rate of $12.68 per year.
...]]></description><link>https://davidmontesdeoca.dev/the-one-with-a-guide-for-configuring-a-proxy-email-service</link><guid isPermaLink="true">https://davidmontesdeoca.dev/the-one-with-a-guide-for-configuring-a-proxy-email-service</guid><category><![CDATA[ proxiedmail]]></category><category><![CDATA[proxy-email]]></category><category><![CDATA[dns]]></category><category><![CDATA[email]]></category><category><![CDATA[privacy]]></category><category><![CDATA[email-forwarding]]></category><category><![CDATA[Custom Domain]]></category><dc:creator><![CDATA[David Montesdeoca]]></dc:creator><pubDate>Tue, 27 Jan 2026 22:22:02 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1770663122646/050d08f7-f412-482a-8ed0-3abd2def15c2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<blockquote>
<p>Disclaimer: This is not a sponsored post.</p>
</blockquote>
<p>I recently registered the domain <a target="_blank" href="https://davidmontesdeoca.dev">davidmontesdeoca.dev</a> through <a target="_blank" href="https://sav.com/">sav.com</a>. At the time of this writing, they offer a highly competitive price of $4.99 for the first year, with a renewal rate of $12.68 per year.</p>
<p>After configuring the blog DNS on Hashnode, I was surprised to find no option for setting up an email account. Since the feature was missing from the control panel, I contacted customer support via their web chat. They confirmed that they do not provide email services, though they did recommend several third-party email providers.</p>
<p>Until now, every time I registered a domain, I had access to a webmail service and had the possibility of creating an email account and multiple aliases for it. Usually, I would configure Gmail to receive and send emails for and from that email address, too.</p>
<p>Ultimately, I chose <a target="_blank" href="https://proxiedmail.com/">ProxiedMail</a> as my proxy-email provider, a service that was not among the recommendations from the sav.com team. I was impressed by their <a target="_blank" href="https://proxiedmail.com/en/blog/using-email-directly-not-safe-anymore">security and privacy-focused approach</a>, the intuitive dashboard for configuring proxy-emails, and the availability of the following features within their free tier:</p>
<p><img alt="Free plan's features" src="https://github.com/user-attachments/assets/e1904a50-7ce7-4ef2-bbc9-b0889ff41844" /></p>
<p>The following steps outline how I configured a proxy-email specifically for this post: <code>blog-post[@]davidmontesdeoca.dev</code>.</p>
<p>After registration, the dashboard allows you to create a new proxy-email:</p>
<p><img alt="ProxiedMail's dashboard" src="https://github.com/user-attachments/assets/3397dd3b-a69a-4b4d-a9a1-41ea0c2ce78e" /></p>
<p>Since I am using a custom domain, I must first add it to the system:</p>
<p><img alt="Form to add a custom domain" src="https://github.com/user-attachments/assets/67c961bc-4f57-4de6-8e70-25dde3d74afb" /></p>
<p>The verification process involves several steps:</p>
<p><img alt="Step 1 of the domain configuration process: Domain ownership" src="https://github.com/user-attachments/assets/62c7a42e-ec43-46f4-bdfc-823ec40dd2b2" /></p>
<p>To begin the verification, navigate to sav.com: <code>My Domains</code> -&gt; <code>davidmontesdeoca.dev</code> -&gt; <code>Manage DNS</code> -&gt; <code>Custom DNS</code>.</p>
<p>The process for each record is identical: create a record in the domain provider dashboard using the data provided by the proxy-email service, then verify the record.</p>
<p>First, you must verify the domain ownership:</p>
<pre><code>Type: TXT
<span class="hljs-attr">Name</span>: davidmontesdeoca.dev
<span class="hljs-attr">Value</span>: proxiedmail-verification=<span class="xml"><span class="hljs-tag">&lt;<span class="hljs-name">verification</span> <span class="hljs-attr">code</span>&gt;</span></span>
</code></pre><blockquote>
<p>Using @ as the name works exactly the same way.</p>
</blockquote>
<p>In the Proxy settings, you must select <em>DNS Only</em>. Choosing <em>Enabled</em> will trigger the following error:</p>
<pre><code>Code <span class="hljs-number">9004</span>: This record type cannot be proxied.
</code></pre><p>Once ownership is verified, proceed to the MX record configuration:</p>
<p><img alt="Step 2 of the domain configuration process: MX record" src="https://github.com/user-attachments/assets/99bfbfc7-5574-432f-b26c-7c7e73297c67" /></p>
<p>Add the following record at your domain provider:</p>
<pre><code>Type: MX
<span class="hljs-attr">Name</span>: davidmontesdeoca.dev
<span class="hljs-attr">Value</span>: mx.proxiedmail.com
<span class="hljs-attr">Priority</span>: <span class="hljs-number">10</span>
<span class="hljs-attr">Proxy</span>: DNS Only
</code></pre><p>Once the MX record is verified, proceed to the SPF record configuration:</p>
<p><img alt="Step 3 of the domain configuration process: SPF record" src="https://github.com/user-attachments/assets/67d87c8a-bbf4-4233-b7ec-3cbbcf65d5b1" /></p>
<p>The process for adding this record is the same:</p>
<pre><code>Type: TXT
<span class="hljs-attr">Name</span>: davidmontesdeoca.dev
<span class="hljs-attr">Value</span>: v=spf1 include:spf.proxiedmail.com ~all
<span class="hljs-attr">Proxy</span>: DNS Only
</code></pre><p>Once the SPF record is verified, optionally proceed to the DKIM record configuration, which helps ensure your forwarded emails are not marked as spam by receiving servers:</p>
<p><img alt="Step 4 of the domain configuration process: DKIM record" src="https://github.com/user-attachments/assets/70c47fa5-f771-47c4-8002-4aa55b5d81e3" /></p>
<p>To enable DKIM, add the following record:</p>
<pre><code>Type: CNAME
<span class="hljs-attr">Name</span>: dkim._domainkey.davidmontesdeoca.dev
<span class="hljs-attr">Value</span>: dkim._domainkey.pxdmail.com
<span class="hljs-attr">Proxy</span>: Enabled
</code></pre><p>While other verifications are near-instant, this one may take longer to propagate. Once applied, you will see the message: <em>You are all set</em>.</p>
<p>You are now ready to create your first proxy-email using your verified custom domain:</p>
<p><img alt="Form to create a proxy-email" src="https://github.com/user-attachments/assets/4f3aaa9a-5648-49d1-99b3-ad91a4fb8301" /></p>
<p>A confirmation message will appear:</p>
<p><img alt="Modal with message regarding the first proxy-email created" src="https://github.com/user-attachments/assets/03b94771-a55c-419b-acd0-6f52382b9cf7" /></p>
<p>With forwarding enabled by default, emails sent to the proxied email address will be received at your specified real email account:</p>
<p><img alt="Test of the proxy-email sent" src="https://github.com/user-attachments/assets/314107c0-bcca-4542-bc8e-92321551e3af" /></p>
<p><img alt="Details of the test of the proxy-email sent" src="https://github.com/user-attachments/assets/a4f34959-a12f-45e6-9a12-fb3e0c90f297" /></p>
<p>The email arrived in less than a minute:</p>
<p><img alt="Test of the proxy-email received" src="https://github.com/user-attachments/assets/6aee4af7-0f22-4034-8773-b42f17c690ae" /></p>
<p><img alt="Details of the test of the proxy-email received" src="https://github.com/user-attachments/assets/f0d95a67-8892-43b5-912e-0280149e3aa2" /></p>
<blockquote>
<p>This only works if the recipient is in the <em>To</em> field. During testing, emails where the recipient was placed in the <em>BCC</em> field were not delivered.</p>
</blockquote>
<p>In the dashboard you can see the total number of emails forwarded to the new address:</p>
<p><img alt="The proxy-email just generated in the dashboard" src="https://github.com/user-attachments/assets/d2b5a18f-e720-497e-a4f1-d5348d78eeea" /></p>
<p>ProxiedMail also supports wildcard proxy email addresses:</p>
<p><img alt="Form to create a wildcard proxy-email" src="https://github.com/user-attachments/assets/fd4b23fe-99a2-4bba-a50e-ea6f5ddea207" /></p>
<p>A modal explains the functionality:</p>
<p><img alt="Modal explaining what are the wildcard proxy-emails" src="https://github.com/user-attachments/assets/2434e725-6137-4755-8b3c-dba4a8d44310" /></p>
<p>As with the other proxy-email, forwarding is active immediately:</p>
<p><img alt="Test of the wildcard proxy-email sent" src="https://github.com/user-attachments/assets/0deb2b4c-d509-4fc0-abc1-63788d1ebce1" /></p>
<p><img alt="Details of the test of the wildcard proxy-email sent" src="https://github.com/user-attachments/assets/11d60c0b-933a-4bc8-a7c6-e8c79b14f57e" /></p>
<p>The email sent to this email address was also delivered in less than a minute:</p>
<p><img alt="Test of the wildcard proxy-email received" src="https://github.com/user-attachments/assets/35b25286-2a66-464c-a137-8732281d2b7f" /></p>
<p><img alt="Details of the test of the wildcard proxy-email received" src="https://github.com/user-attachments/assets/f4ca4c53-3102-4889-8b2a-5a29f6905596" /></p>
<p>In the dashboard you can see the total number of emails forwarded to this new address, too:</p>
<p><img alt="The wildcard proxy-email just generated in the dashboard" src="https://github.com/user-attachments/assets/930a7e2d-e795-4490-8c9a-99a506d497e2" /></p>
<p>To conclude, I will highlight several other features available on the platform, both free and paid:</p>
<ul>
<li><p>Deleting proxy-emails is a paid feature. On the free plan, you can only disable unused email addresses:</p>
<p><img alt="Modal asking the user to upgrade their plan to delete a proxy-email" src="https://github.com/user-attachments/assets/80ff31a3-7b0e-41d5-8c89-547821bec104" /></p>
</li>
<li><p>Hiding the forwarded email banner is a paid feature, too:</p>
<p><img alt="Modal with message telling the user that removing the forwarded email banner from the emails is a paid feature" src="https://github.com/user-attachments/assets/861ffb90-3d87-4980-bc28-69fd9dc5fab6" /></p>
</li>
<li><p>Storing a password to send emails via an alias did not work with Gmail during my testing; so most likely it requires a paid plan:</p>
<p><img alt="Form to store a password for a proxy-email" src="https://github.com/user-attachments/assets/30fbbe33-abfc-4329-be23-0ccfbfa9cb3e" /></p>
<p><img alt="Form to generate a password for a proxy-email" src="https://github.com/user-attachments/assets/0f839ce8-e5f4-4d3c-af06-917fa2b971ac" /></p>
<p>However, they offer a compelling feature for managing contacts:</p>
<p><img alt="Form to create a new contact" src="https://github.com/user-attachments/assets/e79cde22-7822-404c-95be-05ea3e5a1197" /></p>
<p><img alt="Explanation of the reverse alias process used for contacts" src="https://github.com/user-attachments/assets/68447ace-498f-45c7-8570-8197bf708ec5" /></p>
<p>This uses a reverse alias process, which worked perfectly in my tests (though the initial email was flagged as spam):</p>
<p><img alt="Test using reverse alias for a contact" src="https://github.com/user-attachments/assets/124f1636-3db3-4524-baf6-8cc5a3532170" /></p>
<p><img alt="Details of the test using reverse alias for a contact" src="https://github.com/user-attachments/assets/5b43e11d-195b-4856-9da4-99cc30bf4ddb" /></p>
</li>
<li><p>Using unique email addresses for every site where you register to track potential data leaks:</p>
<p><img alt="Form to add websites where a given proxy-email has been used" src="https://github.com/user-attachments/assets/a3056df9-7ed1-4813-9344-c30f5623a5de" /></p>
</li>
<li><p>Adding context to remember the purpose of each proxy-email:</p>
<p><img alt="Form to add a description to know where a given proxy-email has been used" src="https://github.com/user-attachments/assets/bb3eacf0-cda2-4037-9e72-8891072a6eb9" /></p>
</li>
<li><p>Identify which proxy received a specific message with a reverse lookup:</p>
<p><img alt="Modal with the form for the proxy email reverse lookup" src="https://github.com/user-attachments/assets/e62e40d8-5f37-4498-b5ce-6cf3b5f8a79c" /></p>
<p><img alt="Modal with the result of the proxy email reverse lookup" src="https://github.com/user-attachments/assets/4fe21f25-bd24-4f78-bd6c-8678d4beb6bb" /></p>
</li>
</ul>
<p>The service offers other functionalities beyond those listed here. I encourage you to see for yourself.</p>
<p>Although I have not tested every feature yet, what I have discovered while writing this post is impressive and definitely offer a significantly better user experience than relying on a provider's native webmail interface.</p>
<p>I really like ProxiedMail's approach, being a compelling alternative to traditional email setups. It is also significantly easier to configure.</p>
<p>Thank you for reading and see you in the next one!</p>
]]></content:encoded></item><item><title><![CDATA[The one about true flexible schedules]]></title><description><![CDATA[A while back, I wrote about my experience working remotely. I mentioned that, having seen the advantages of this way of working, today I would not consider accepting office-based or mandatory hybrid work models.
But that's not all. As I see it, the p...]]></description><link>https://davidmontesdeoca.dev/the-one-about-true-flexible-schedules</link><guid isPermaLink="true">https://davidmontesdeoca.dev/the-one-about-true-flexible-schedules</guid><category><![CDATA[flexible schedule]]></category><category><![CDATA[asynchronous work]]></category><category><![CDATA[presenteeism]]></category><category><![CDATA[remote work]]></category><category><![CDATA[flexible hours]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[burnout]]></category><category><![CDATA[Mental Health]]></category><category><![CDATA[deep work]]></category><category><![CDATA[work life balance]]></category><dc:creator><![CDATA[David Montesdeoca]]></dc:creator><pubDate>Tue, 30 Dec 2025 10:17:55 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1767090400402/9df4c3cc-e678-4a33-9295-21a00bfa4b5e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>A while back, I wrote about <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-my-experience-working-remotely">my experience working remotely</a>. I mentioned that, having seen the advantages of this way of working, today I would not consider accepting office-based or mandatory hybrid work models.</p>
<p>But that's not all. As I see it, <strong>the perfect combination is fully remote work with true flexible hours</strong>. More on this below.</p>
<p>In my first job in Madrid, I worked from 9 to 7 for a year, with a 2-hour lunch break. Totally incomprehensible, considering I had my food already prepared and just had to heat it up in the microwave. 20 minutes was more than enough time; the rest was a complete waste of time.</p>
<p>Furthermore, during this time of year, with the winter schedule, I essentially did not see the light of day during workdays.</p>
<p>After the first year, the schedule changed to 9 to 6, with a 1-hour lunch break. A considerable improvement, but essentially, I still felt like I was wasting much more time than necessary.</p>
<p>Regardless of how productive I was being on any given day, I had to <em>be warming the chair in the office</em> whether I wanted to or not until quitting time.</p>
<p>Of course, I still missed being able to see the light of day for longer, because with the winter schedule, it gets dark shortly after 6 PM in this part of the world.</p>
<p>Little is said about the <strong>effect of sunlight on mood</strong>, which can even lead to <a target="_blank" href="https://en.wikipedia.org/wiki/Seasonal_affective_disorder">Seasonal Affective Disorder (SAD)</a>.</p>
<p>The situation I am describing is known as <strong>presenteeism</strong>, although it must be clarified that this term can refer to both virtual and physical presence.</p>
<p>By expecting you to always be available, a pressure is generated that can <strong>cause burnout</strong> in team members and make their <strong>productivity drop</strong>, achieving exactly the opposite of what is intended with that <em>culture of control</em>.</p>
<p>Unfortunately, this situation is widespread in the software industry. It has been normalized to such an extent that it is common to find job offers selling the idea of <strong>fake flexible hours</strong>.</p>
<p>For example, <a target="_blank" href="https://www.getmanfred.com/en">Manfred</a>, a Spanish company that takes improving job offers and recruitment processes seriously, <a target="_blank" href="https://www.getmanfred.com/en/job-offers/8228/ludus-backend-developer-dic25#horario">sells that idea in many of its offers</a> (this one available only in Spanish) when the company publishing the offer simply offers slight flexibility to start, but the workday remains rigid.</p>
<p>Certainly, that is better than having no flexibility at all. It may even be enough for people who, for instance, have to take their children to school.</p>
<p>However, true schedule flexibility comes when the company understands that with a <em>culture of objectives</em> (as opposed to the <em>culture of control</em> I mentioned earlier), the norm is to achieve better results and <strong>reduce burnout</strong>.</p>
<p>Of course, enjoying that privilege implies some obligations on your part:</p>
<ul>
<li>Demonstrate reliability, <strong>consistently delivering high-quality work on time</strong> to build the necessary trust.</li>
<li>Adopt <strong>asynchronous and proactive communication</strong> with your manager and the rest of the team, so they know when you will be available and your progress on the task you are currently working on. This ensures the team remains unblocked even when you are not online.</li>
<li>Be <strong>available during the team's core hours</strong> (e.g., from 10 am to 2 pm). This is usually a common requirement for companies, although my experience has always been that attending team meetings is sufficient.</li>
</ul>
<p>If you meet those obligations, I see no reason not to have the freedom to work when you feel your <strong>productivity will be highest</strong> or when you need blocks of <strong>absolute concentration without interruptions</strong>; whether that is early in the morning or late at night. It is simply adapting to your natural peak performance hours, which may not be the same every day.</p>
<p>The point is that the company considers you a responsible person who knows how to organize themselves in the best possible way. The main consequence of enjoying a flexible schedule is having a <strong>better work-life balance</strong> and <strong>greater autonomy</strong>.</p>
<p>You might work only 5 hours one day and 11 the next. You might even decide to work during the weekend to make up for time you could not dedicate during the week or to get ahead on work because you know that during the following week, you will not be able to dedicate the necessary time.</p>
<p>Perhaps another day you cannot attend a meeting because you have a medical appointment or need to take a break for a few hours because you do not feel well; perhaps you have to go grocery shopping or feel like going out for lunch with friends who are visiting for a few days.</p>
<p>There may be many and varied reasons, but they all come down to the same thing: the freedom to manage your life while fulfilling your work duties.</p>
<p>In the end, it is not about working less, but working better. And you, <em>are you still warming the seat?</em></p>
<p>Thank you for reading and see you in the next one!</p>
<hr />
<div class="hn-embed-widget" id="buy-me-a-coffee"></div>]]></content:encoded></item><item><title><![CDATA[The one about adding borders between columns in a grid]]></title><description><![CDATA[Here begins a new blog series where I plan to share useful tips I discover in my day-to-day development work.

Translating a Figma design into code for the new project I am currently working on, I encountered a specific UI requirement: a container wi...]]></description><link>https://davidmontesdeoca.dev/the-one-about-adding-borders-between-columns-in-a-grid</link><guid isPermaLink="true">https://davidmontesdeoca.dev/the-one-about-adding-borders-between-columns-in-a-grid</guid><category><![CDATA[TIL]]></category><category><![CDATA[CSS]]></category><category><![CDATA[HTML]]></category><category><![CDATA[Tailwind CSS]]></category><category><![CDATA[Tailwind CSS]]></category><category><![CDATA[tailwind]]></category><dc:creator><![CDATA[David Montesdeoca]]></dc:creator><pubDate>Sat, 29 Nov 2025 11:58:57 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1764417508943/7deb490d-8389-4aed-bab2-c971d85968b6.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Here begins a new blog series where I plan to share useful tips I discover in my day-to-day development work.</p>
<hr />
<p>Translating a Figma design into code for the <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-a-new-project-working-for-an-american-fintech">new project I am currently working on</a>, I encountered a specific UI requirement: a container with 3 column sections separated by vertical borders. The catch was that these borders could not span the full height of the container; they needed inset padding at the top and bottom.</p>
<p><img alt="Image" src="https://github.com/user-attachments/assets/5bffa209-be51-4ffe-9512-741ab5af7b07" /></p>
<p>Right away, it was clear this was a perfect use case for the wonderful CSS <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/grid">grid</a> property. However, exactly how to achieve those inset borders appearing between the columns was less obvious.</p>
<p>Thanks especially to the invaluable help found in <a target="_blank" href="https://www.reddit.com/r/webdev/comments/15w6ptw/how_to_get_borders_between_columns/">this Reddit thread</a> and <a target="_blank" href="https://www.youtube.com/watch?v=QjddVRthBrU">this video</a>, I landed on a clean solution involving the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/::after\">:after</a> <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/Pseudo-elements">pseudo-element</a>.</p>
<p>I created a CodePen demonstrating this using plain HTML and CSS:</p>
<iframe height="300" style="width:100%" src="https://codepen.io/backpackerhh/embed/vEGWPxz?default-tab=html%2Cresult">
  See the Pen <a href="https://codepen.io/backpackerhh/pen/vEGWPxz">
  Borders between columns in grid container using CSS</a> by David Montesdeoca (<a href="https://codepen.io/backpackerhh">@backpackerhh</a>) on <a href="https://codepen.io">CodePen</a>.
</iframe>

<p>In my specific case, I am not working with plain CSS but rather <a target="_blank" href="https://tailwindcss.com/">Tailwind CSS</a>, so I also created a CodePen adapting the solution to that framework:</p>
<iframe height="300" style="width:100%" src="https://codepen.io/backpackerhh/embed/NPNwJwP?default-tab=html%2Cresult">
  See the Pen <a href="https://codepen.io/backpackerhh/pen/NPNwJwP">
  Borders between columns in grid container using Tailwind</a> by David Montesdeoca (<a href="https://codepen.io/backpackerhh">@backpackerhh</a>) on <a href="https://codepen.io">CodePen</a>.
</iframe>

<p>Regarding the Tailwind example, I want to highlight a specific decision. My initial intention was to define the utility classes inline directly on the HTML elements, as is standard Tailwind practice.</p>
<p>However, combining the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:not\">:not</a> and <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:last-child\">:last-child</a> <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/Pseudo-classes">pseudo-classes</a> with the <code>:after</code> pseudo-element seemed to prevent Tailwind from generating the correct CSS rule. Therefore, I opted to use the <a target="_blank" href="https://tailwindcss.com/docs/functions-and-directives#apply-directive">@apply directive</a> instead and keep the HTML clean.</p>
<p>Finally, because this is a static design with exactly 3 columns, I briefly considered using <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/::before\">:before</a> and <code>:after</code> pseudo-elements only on the middle column. However, applying a right border to every item except the last one felt like a more robust approach. It ensures the solution remains scalable should a fourth column be added in the future.</p>
<p>There are surely thousands of ways to achieve this. If you have an interesting alternative method, feel free to share it with me.</p>
<p>Thank you for reading and see you in the next one!</p>
<hr />
<div class="hn-embed-widget" id="buy-me-a-coffee"></div>]]></content:encoded></item><item><title><![CDATA[The one about a new project working for an American fintech]]></title><description><![CDATA[At the beginning of this year I talked about the layoffs in the American fintech I work for as a contractor. That story is key to what comes next.
For a long time, the company was focused entirely on education. They finally decided to diversify, bett...]]></description><link>https://davidmontesdeoca.dev/the-one-about-a-new-project-working-for-an-american-fintech</link><guid isPermaLink="true">https://davidmontesdeoca.dev/the-one-about-a-new-project-working-for-an-american-fintech</guid><category><![CDATA[AI]]></category><category><![CDATA[#ai-tools]]></category><category><![CDATA[ai agents]]></category><category><![CDATA[mcp]]></category><category><![CDATA[claude-code]]></category><category><![CDATA[Ruby]]></category><category><![CDATA[sinatrarb]]></category><category><![CDATA[htmx]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[frontend]]></category><category><![CDATA[Backend Development]]></category><category><![CDATA[backend]]></category><category><![CDATA[contractor]]></category><category><![CDATA[fintech]]></category><category><![CDATA[fintech software development]]></category><dc:creator><![CDATA[David Montesdeoca]]></dc:creator><pubDate>Wed, 29 Oct 2025 19:35:45 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1761157290326/87cd9574-3b97-4c30-a212-6a499d74e73b.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>At the beginning of this year I talked about the <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-layoffs-in-an-american-fintech">layoffs</a> in the American fintech I work for as a contractor. That story is key to what comes next.</p>
<p>For a long time, the company was focused entirely on education. They finally <strong>decided to diversify, betting on the travel and hospitality industry</strong>. This new strategy kicked off a project at the start of Q3: building a new tool that allows the company's operators to manage the beneficiary network on behalf of clients.</p>
<p>To add some context, there is already a tool for managing beneficiaries still in use. The problem is, no one on the tech team is happy with it. The general consensus is that, looking back, poor design decisions were made.</p>
<p>In addition to that backend, there is an old SPA, developed with React, that collects the beneficiary data. These beneficiaries get an email notifying them that a client is missing some data to send a payment, which includes a link to a form. <em>Sounds like a scam, right?</em></p>
<p>The new tool consists of <strong>two brand-new applications</strong>: a REST API, developed with Sinatra, and an SPA, developed with React.</p>
<p>The backend application is being developed by the same team that has had the governance of the old beneficiaries application, in the <em>Transactional</em> area of the company.</p>
<p>Some members of other teams were selected to temporarily join that team, including myself, to gain context about the project and, in time, we will become part of the <strong>new beneficiaries team</strong> to take over the governance of all the relevant applications.</p>
<p>The new frontend application, however, is being developed by a team from Romania. They are not part of the <em>Transactional</em> area, a decision made due to a lack of internal staff and the very tight deadline we already had.</p>
<p>The idea was to get an MVP ready by the end of the quarter for a demo with a major German client, a big name in luxury resorts and 5-star hotels. Landing them would be a huge boost, though it was not a <em>sine qua non</em> condition for the project to continue.</p>
<p>From the beginning, the head of the <em>Transactional</em> area was uncomfortable with the fact that a team outside of the area was developing that application, so he soon started pulling strings to see what the options were for starting what became known as "<strong>the migration</strong>."</p>
<p>According to what I was told, the main reason they chose me for this new team was my willingness to work with frontend applications. In this company, almost everything is backend, and the little frontend that exists tends to be internal-use applications that do not get much love.</p>
<p>I was asked to do an analysis of how long it would take to do the <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-shortcuts-in-cursor-with-karabiner-elements">migration with AI</a> for the two options on the table:</p>
<ul>
<li>Being an SPA, move the logic into another existing SPA, even if the applications were unrelated.</li>
<li>Create a new <a target="_blank" href="https://martinfowler.com/articles/micro-frontends.html">microfrontend</a> with <strong>Sinatra + htmx</strong> that would be embedded in the <em>transactional registry</em> application, where other applications are already embedded.</li>
</ul>
<p>Initially, I had no doubt that the SPA option was the best one, especially if speed was the main goal. Of course, some adjustments would be needed. The two applications had been developed by different teams and the code was organized differently. For example, one managed state locally with <code>useState</code> while the other used Redux for managing the state of the whole application.</p>
<p>However, what they forgot to tell me was that a critical decision had already been made the previous year on future frontend development in the <em>Transactional</em> area: the preferred option is to create microfrontends with Sinatra + htmx.</p>
<p>The main reason being a general <strong>lack of frontend expertise</strong> among the area's developers. With the high rotation of team members in the same area of the company, they believed that sticking with React would just create friction for future changes or bug fixes.</p>
<p>Obviously, knowing that changed everything. <strong>The final goal was to build the microfrontend with Sinatra + htmx in any case</strong>.</p>
<p>Before making an estimate, I created a <em>proof of concept</em> UI with <a target="_blank" href="https://www.claude.com/product/claude-code">Claude Code</a> to generate a paginated list of beneficiaries, following the design of the current frontend application and the same approach as other microfrontends.</p>
<p>The experience was satisfying and frustrating in equal parts. Replicating the design and code from other applications was relatively easy (after iterating a few times), but it usually meant giving the AI agent too much context. As a result, it was very easy for it to end up doing too much or too little.</p>
<p>In the end, I made a rather <strong>naive estimate of 4 weeks to complete the migration</strong>. Even so, the head of the <em>Transactional</em> area thought it was too much time. The manager of the team later told me he had thought that with AI, it would be a matter of a couple of days.</p>
<p>Of course, I only consider that estimate naive from my current perspective, now that I have much more context on what actually needs to be done.</p>
<p>Shortly after that, I met with my manager for a couple of sessions where we did a much more precise estimation. We divided all the work into tasks with a very clear and reduced scope, and we added a conservative estimate of how long we might take for each one.</p>
<p>As a result of those two sessions, we <strong>estimated it would take a single person approximately 8 weeks</strong>. Of course, this is not a job where eight people could complete the work in one week. Many tasks simply cannot be parallelized.</p>
<p>With this new estimate, my manager got the buy-in we needed: if new features were strictly necessary, they would be added to the new React SPA, but during Q4, our team would focus entirely on the migration to the Ruby microfrontend.</p>
<p>In the meantime, I took on the task of replicating the existing functionality for collecting beneficiary information from the old SPA for the new beneficiaries application. This turned out to be a not-so-simple task as initially thought. I will talk about that in a future post.</p>
<p>Before the end of Q3, <strong>they confirmed that I was going to lead the migration project</strong>, which I have already been working on for a few weeks.</p>
<p>The project team consists of an engineering manager, a product manager, a designer, and three software engineers, including myself. The other two engineers will be more focused on backend tasks.</p>
<p>I started by defining the foundational tasks that will set the groundwork for our work in the coming months, while the product manager will take charge of defining the rest of the upcoming tasks.</p>
<p>I want to highlight that, as part of the detailed estimations we did, we factored in time to properly define the rules Claude Code would use, which are mostly already defined in a dedicated repository available company wide. We also dedicated time to learning how to get the most out of the AI with good prompting, though that is a continuous work in progress.</p>
<p>I have also been spending time deepening my knowledge of htmx and Tailwind, which I have little experience with so far.</p>
<p>At the moment, I am creating a <strong>design system</strong> in Ruby. I am basing it on components from our existing microfrontends and from the design in Figma, using Claude Code and <a target="_blank" href="https://modelcontextprotocol.io/docs/getting-started/intro">MCPs</a> like <a target="_blank" href="https://github.com/ChromeDevTools/chrome-devtools-mcp">chrome-devtools</a> and <a target="_blank" href="https://help.figma.com/hc/en-us/articles/32132100833559-Guide-to-the-Figma-MCP-server">Figma</a> for assistance.</p>
<p>We have plans to create a gem that will allow us to reuse the components created for this design system in the various microfrontends that already exist.</p>
<p>I do not expect to talk about the project in the coming months, at least until we have finished it. Then, I will write a retrospective post sharing what went well, and what could have gone better.</p>
<p>I would also like to share the technical details of how we solved the obstacles that we will surely find along the way.</p>
<p>I appreciate the trust placed in me, even as a contractor. I believe it is the result of a job well done.</p>
<p>Thank you for reading and see you in the next one!</p>
<hr />
<div class="hn-embed-widget" id="buy-me-a-coffee"></div>]]></content:encoded></item><item><title><![CDATA[The one about my experience at SNGULAR]]></title><description><![CDATA[It has been a full year since I joined SNGULAR. In this post, I want to talk about my experience with them so far.
As I mentioned when I landed this job, working for a consulting firm was not in my plans when I started looking for a new job at the be...]]></description><link>https://davidmontesdeoca.dev/the-one-about-my-experience-at-sngular-so-far</link><guid isPermaLink="true">https://davidmontesdeoca.dev/the-one-about-my-experience-at-sngular-so-far</guid><category><![CDATA[Consultancy]]></category><category><![CDATA[consulting]]></category><category><![CDATA[consulting firm]]></category><dc:creator><![CDATA[David Montesdeoca]]></dc:creator><pubDate>Sun, 28 Sep 2025 18:14:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1758645523723/d6c60567-fdd2-41fe-a032-d31f5549dd35.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It has been a full year since I joined <a target="_blank" href="https://www.sngular.com/">SNGULAR</a>. In this post, I want to talk about my experience with them so far.</p>
<p>As I <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-my-new-job">mentioned when I landed this job</a>, working for a consulting firm was not in my plans when I started looking for a new job at the beginning of 2024.</p>
<p>For most of my professional career, I have worked in product companies. And that is not a coincidence. I really love the deep ownership and the opportunity to build and nurture a single product over the long term.</p>
<p>However, after a few months in which things did not go as I expected, I had to lower my expectations. That included having to consider the possibility of working for a consulting firm.</p>
<p>In fact, I applied for the offer that SNGULAR published on LinkedIn, where they were looking for a backend developer specializing in Ruby to work exclusively with a client in the fintech industry, fully remote.</p>
<h2 id="heading-selection-process">Selection process</h2>
<p>The next day I received a phone call from a person from human resources (HR). We talked for a few minutes to see if there could be a fit on both sides and finally scheduled a video call for a few days later.</p>
<p>During that interview, the HR person shared a presentation where they told me in great detail what the company is like and how things work, what was expected of me, and how I would work with the client. They did not reveal who the client was, though.</p>
<p>Then we moved on to the most common part of any interview, where we went into more detail about what we had already discussed briefly on the phone, especially my work experience and my professional and financial expectations. We also spoke in English for a while to test my level.</p>
<p>In the next step, I appreciated that the technical interview felt more like a friendly conversation than a test, which helped ease my nerves and allowed for a more natural chat about my skills. This step was lead by another SNGULAR developer who works for the same client where I would work and who also has a manager role for the rest of the developers working for this client.</p>
<p>He finally told me who the client was, <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-how-things-work-in-an-american-fintech">how things work with them</a>, and asked me about my experience with this or that technology. The usual.</p>
<p>There was a third step that they considered unnecessary, which was an interview with the head of SNGULAR and liaison with the client.</p>
<p>A few days later I received a call from HR people and they confirmed that they wanted to hire me if I was interested, but they wanted to wait and see how the interview with the client went.</p>
<p>I had that interview with the client a few days later. It was very similar to the technical interview I had had with the people from SNGULAR previously. It was mainly a conversation in English with the engineering manager and product manager of the team I would be part of if I ended up joining them.</p>
<p>They told me how the American fintech works, they asked me questions about my previous experience and how I had worked on the different previous projects, from a quite technical point of view.</p>
<p>SNGULAR told me afterwards that I would soon hear back about the client's feedback and that, if all went well, they would make me a formal offer. That offer arrived two days later.</p>
<p>The entire process was very straightforward and pleasant, and I felt very comfortable throughout. I am especially grateful that if any of the steps took longer than expected, HR people always kept me informed.</p>
<h2 id="heading-onboarding">Onboarding</h2>
<p>Before my first day at SNGULAR, I received some things at home: a laptop (which you can choose between Windows or macOS), a monitor, a keyboard, a mouse, a backpack, etc.</p>
<p>The first day begins with a meeting with the HR people to welcome you. They share a new presentation where they tell you where to find everything you will need and what to do in case you need help.</p>
<p>Of course, you have to sign the employment contract and configure the laptop following the instructions you receive by email.</p>
<p>Normally, after a couple of weeks of onboarding, you start working with the client. In my case, I already had a vacation scheduled, so my start with the client was delayed until I was back. In the meantime, I started a <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-learning-ruby">Ruby training</a> to some colleagues who were not assigned to a project at that time.</p>
<h2 id="heading-relationship-with-the-company">Relationship with the company</h2>
<p>On a daily basis, I do not have any type of contact with anyone from SNGULAR, except for a teammate.</p>
<p>Weekly we have optional half-hour meetings where all the SNGULAR colleagues who work in the American fintech get together with the company's liaison with the client to share news from either side.</p>
<p>I also have a 1-to-1 once a month with the manager of the SNGULAR developers.</p>
<p>At the end of the year you have your own performance review and that of your managers.</p>
<p>During this time, I have had a meeting with the HR people when I completed 3 months in the company and another one when I celebrated the first year to see how things were going for me and to know how integrated I felt in the company. In both cases the answer was the same. As I work full-time with the client, my relationship with SNGULAR is mostly reduced to receiving my paycheck at the end of the month.</p>
<p>As a curiosity, they also send us a weekly survey to find out how our week has gone.</p>
<p>The company has an office in Madrid, which is quite far from home, more than 1 hour by metro or bus each way. So it is not an option for me to go there regularly.</p>
<p>On the other hand, everyone I have spoken to when I have requested help of any kind has always been very kind, from HR to IT, for example.</p>
<p>Every year, the company celebrates a Christmas and summer party in various parts of Spain and the rest of the world. Those people who do not live in a city where the party is held have the option of going to the party closest to their city and requesting a travel and accommodation allowance of up to €200.</p>
<p>In addition, this month we celebrated the company's 10th anniversary party in Madrid, which was attended by more than 600 people from all over the world. It was a great opportunity to meet most of my colleagues in person.</p>
<h2 id="heading-advantages">Advantages</h2>
<ul>
<li>I work full-time for the same client.</li>
<li>In case of <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-layoffs-in-an-american-fintech">layoffs at the client</a>, you do not lose your job directly, but are reassigned to another project.<ul>
<li>If things do not work out in the assigned project on your part either, you can be reassigned to another project as well.</li>
</ul>
</li>
<li>You have an annual training budget of €500, which can be spent on courses, books, or tickets for technology events; and an annual well-being budget of €200, which can be spent on gym memberships or psychology sessions, to give a few examples.<ul>
<li>Although it is true that there is not much flexibility to spend it. For example, you cannot use it to buy a standing desk.</li>
<li>On the other hand, if the company considers that a course could be interesting for your work, they pay the corresponding license without deducting it from your annual budget. For instance, some of us got a license for the <a target="_blank" href="https://www.epicreact.dev/">Epic React</a> course by Kent C. Dodds.</li>
</ul>
</li>
<li>We have certain tax advantages for contracting a private health insurance policy. There are other benefits too that I do not enjoy, such as meal or transportation vouchers, because I do not go to the client's offices on a daily basis.</li>
<li>We have free access to AI tools, such as the Gemini pro model integrated into many of the company's tools.</li>
<li>We have the possibility of attending 1-hour English classes once a week, but outside of working hours.</li>
</ul>
<h2 id="heading-disadvantages">Disadvantages</h2>
<ul>
<li>In my experience, you earn significantly less than in product development companies, such as start-ups or scale-ups.</li>
<li>Salary reviews are only conducted once a year, in February. If you have not been working at the company for at least 9 months at that time, usually you have to wait another year for a salary review.<ul>
<li>As I was told, in very exceptional cases of excellent performance you can opt for a salary review sooner.</li>
<li>In my case, my manager promised me a salary review next year and that they would also take into account that I did not have one this year.</li>
</ul>
</li>
<li>A lot of bureaucracy for everything, which can sometimes mean a simple request takes a few extra steps and days to approve, especially when it comes to an expense related to the money you are assigned annually for training or for well-being.</li>
<li>I have to report the hours I work twice: once in SNGULAR, where it is simpler because everything is charged to the same client, and another time at the client, where I have to specify the time dedicated to the different team initiatives.</li>
<li>By working full-time with the client, the feeling of belonging to the company is practically non-existing.</li>
</ul>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Probably, many of the advantages and disadvantages mentioned above, if not all, are inherent to most consulting firms.</p>
<p>Taking everything into account, I think my experience so far is positive, although there are things that I would undoubtedly change.</p>
<p>Ultimately, my experience shows that the line between <em>consultancy</em> and <em>product company</em> can sometimes be blurry. I would say the most important question is the nature of your specific assignment and your day-to-day team.</p>
<p>Here I just wanted to talk honestly about what my experience with SNGULAR has been so far instead of just leaving a message on Glassdoor.</p>
<p>Looking forward to my second year in the company.</p>
<p>Thank you for reading and see you in the next one!</p>
<hr />
<div class="hn-embed-widget" id="buy-me-a-coffee"></div>]]></content:encoded></item><item><title><![CDATA[The one about accepting Tab suggestions in Cursor]]></title><description><![CDATA[In the previous post, I mentioned that I was starting to use Cursor as my main IDE, after having worked with VSCode and Copilot for a while.
A key advantage of Cursor for VSCode users is its support for the same keyboard shortcuts, allowing for a sea...]]></description><link>https://davidmontesdeoca.dev/the-one-about-accepting-tab-suggestions-in-cursor</link><guid isPermaLink="true">https://davidmontesdeoca.dev/the-one-about-accepting-tab-suggestions-in-cursor</guid><category><![CDATA[cursor IDE]]></category><category><![CDATA[cursor ai]]></category><category><![CDATA[cursor]]></category><category><![CDATA[AI]]></category><category><![CDATA[VS Code]]></category><category><![CDATA[macOS]]></category><dc:creator><![CDATA[David Montesdeoca]]></dc:creator><pubDate>Mon, 25 Aug 2025 18:29:19 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1756143334985/5b51dc25-e807-44b8-93ad-38116dff3f2d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-shortcuts-in-cursor-with-karabiner-elements">previous post</a>, I mentioned that I was starting to use Cursor as my main IDE, after having worked with VSCode and Copilot for a while.</p>
<p>A key advantage of Cursor for VSCode users is its support for the same keyboard shortcuts, allowing for a seamless transition:</p>
<p><img src="https://github.com/user-attachments/assets/cf849e77-a2b8-47c8-8160-9d3c73d0d0d1" alt="Cursor's Quick Start" /></p>
<p>It allowed me to be really productive with this IDE almost immediately. And I say almost, because I ran into some issues when trying to accept AI suggestions on macOS.</p>
<p>Cursor allows you to autocomplete with <a target="_blank" href="https://docs.cursor.com/en/tab/overview">Tab</a>, the model they have trained in-house. With Tab, you can autocomplete multiple lines and blocks of code and jump in and across files to the next autocomplete suggestion.</p>
<p>As an example, I am going to show what happens when I try to accept a suggestion by adding a simple <code>console.log</code> to a JS file:</p>
<p><img src="https://github.com/user-attachments/assets/effbc211-2d1a-4c66-8976-24fc468a5e50" alt="User cannot accept suggestions with tab key in Cursor" /></p>
<p>You can see that I can only accept a suggestion if I place the cursor over the suggestion itself and click <em>Accept</em>.</p>
<p>I dealt with this issue for longer than I would like to admit before I finally had time to investigate the root cause.</p>
<p>When I was able to look into the problem, I started with a quick Google search, where I did not find a <a target="_blank" href="https://www.reddit.com/r/cursor/comments/1juzs0d/does_anyone_elses_cursor_tab_autocomplete_get_in/">real solution</a>. Actually, that was a good sign, because it probably meant it was due to a misconfiguration on my end.</p>
<p>So, I continued by visually searching for the keyboard shortcuts associated with the <code>tab</code> key.</p>
<p>To do this, you have to go to the command palette (<code>control + shift + p</code>) and type <em>keyboard shortcuts</em>. There, you choose the option that does not specify <em>JSON</em>.</p>
<p>Then, you have to type <code>"tab"</code>, with the quotes, or on the right side of the search bar, click <code>Record Keys</code> and press the <code>tab</code> key.</p>
<p><img src="https://github.com/user-attachments/assets/3841c8e5-3a5d-4417-8239-c1c44066f1d0" alt="Cursor's Keyboard Shortcuts" /></p>
<p>The first detail you might note is that the source for almost all the commands is <em>User</em>. This is because I do not like working with macOS, but I have to for work, so I configured the system to be as similar as possible to my personal laptop with Linux. I have a <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-working-with-macos-being-a-linux-user#heading-vscode">post</a> where I discussed this topic in more detail.</p>
<p>Another important detail to note is the first command associated with this key and its <a target="_blank" href="https://code.visualstudio.com/api/references/when-clause-contexts">when clause context</a>: <code>cpp.shouldAcceptTab</code>. We will need to use it pretty soon.</p>
<p>Next, we go back to the command palette and type <em>keyboard shortcuts</em> again. This time, we choose the option that specifies <em>JSON</em>.</p>
<p>Here we have to search for the commands associated with the <code>tab</code> key and disable them one by one until we find the one that is interfering with the expected behavior in Cursor. I started with the most obvious choice:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"key"</span>: <span class="hljs-string">"tab"</span>,
  <span class="hljs-attr">"command"</span>: <span class="hljs-string">"tab"</span>,
  <span class="hljs-attr">"when"</span>: <span class="hljs-string">"editorTextFocus &amp;&amp; !editorReadonly &amp;&amp; !editorTabMovesFocus"</span>
}
</code></pre>
<p>To test my theory, I commented out the entire entry for that shortcut:</p>
<p><img src="https://github.com/user-attachments/assets/57fe0a4a-2984-401a-9c46-5bad9de061c5" alt="Shortcut disabled in Cursor to validate default behavior" /></p>
<p>As expected, with that command disabled, I can now accept Tab's suggestions. However, I noticed an strange behavior when indenting code, for example.</p>
<p><img src="https://github.com/user-attachments/assets/4d45787b-fd8a-47c7-a1a8-8672fc767d3c" alt="Weird behavior found while trying to indent code with tab key in Cursor" /></p>
<p>So, I added the condition we saw earlier, negated, to this command's <code>when</code> clause:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"key"</span>: <span class="hljs-string">"tab"</span>,
  <span class="hljs-attr">"command"</span>: <span class="hljs-string">"tab"</span>,
  <span class="hljs-attr">"when"</span>: <span class="hljs-string">"!cpp.shouldAcceptTab &amp;&amp; editorTextFocus &amp;&amp; !editorReadonly &amp;&amp; !editorTabMovesFocus"</span>
}
</code></pre>
<p>Now I can accept Tab's suggestions without interfering with the default behavior of the <code>tab</code> key.</p>
<p><img src="https://github.com/user-attachments/assets/4361b086-3dfd-4c45-9db4-d21ab10992ee" alt="Weird behavior found regarding indentation in the code with tab key in Cursor is now solved" /></p>
<p>Once again, there was one small detail that still was not working as I expected:</p>
<p><img src="https://github.com/user-attachments/assets/3dccdbf5-968d-4cdd-b372-49e56cba50bc" alt="User not always can accept suggestions with tab key in Cursor" /></p>
<p>When I had a Tab suggestion and other suggestions from the current context, pressing the <code>tab</code> key accepted the first suggestion from the context, and only when I pressed the <code>tab</code> key again did it accept the expected suggestion.</p>
<p>To find the command that was interfering with this behavior, I disabled them one by one until I found the one:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"key"</span>: <span class="hljs-string">"tab"</span>,
  <span class="hljs-attr">"command"</span>: <span class="hljs-string">"acceptSelectedSuggestion"</span>,
  <span class="hljs-attr">"when"</span>: <span class="hljs-string">"suggestWidgetHasFocusedSuggestion &amp;&amp; suggestWidgetVisible &amp;&amp; textInputFocus"</span>
}
</code></pre>
<blockquote>
<p>Remember to re-enable each command after you have confirmed it is not the one you were looking for.</p>
</blockquote>
<p>I added the same negated condition to this <code>when</code> clause:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"key"</span>: <span class="hljs-string">"tab"</span>,
  <span class="hljs-attr">"command"</span>: <span class="hljs-string">"acceptSelectedSuggestion"</span>,
  <span class="hljs-attr">"when"</span>: <span class="hljs-string">"!cpp.shouldAcceptTab &amp;&amp; suggestWidgetHasFocusedSuggestion &amp;&amp; suggestWidgetVisible &amp;&amp; textInputFocus"</span>
}
</code></pre>
<p>The result is now exactly what I expected:</p>
<p><img src="https://github.com/user-attachments/assets/54167738-de7b-476a-b667-9b34010a2a3b" alt="User can accept suggestions with tab key in Cursor" /></p>
<p>In this case, the standard behavior of the <code>tab</code> key was clashing with Cursor's AI suggestion feature. The solution was to only trigger the default <em>tab</em> and <em>acceptSelectedSuggestion</em> commands when the AI does not have a suggestion ready.</p>
<p>The key takeaway is not just about this specific fix, but about the power of <code>when</code> clauses for fine-tuning the IDE to behave exactly how you want it to. Hopefully, this guide saves you some time and helps you get back to coding faster.</p>
<p>Thank you for reading and see you in the next one!</p>
<hr />
<div class="hn-embed-widget" id="buy-me-a-coffee"></div>]]></content:encoded></item><item><title><![CDATA[The one about shortcuts in Cursor with Karabiner-Elements]]></title><description><![CDATA[Recently, I was assigned a spike to check the feasibility of migrating a React application to a microfrontend with Sinatra + htmx entirely using AI. In this case, with Claude Code.
Of course, I did not have to try to do the entire migration at once, ...]]></description><link>https://davidmontesdeoca.dev/the-one-about-shortcuts-in-cursor-with-karabiner-elements</link><guid isPermaLink="true">https://davidmontesdeoca.dev/the-one-about-shortcuts-in-cursor-with-karabiner-elements</guid><category><![CDATA[AI]]></category><category><![CDATA[cursor IDE]]></category><category><![CDATA[cursor]]></category><category><![CDATA[VS Code]]></category><category><![CDATA[osascript]]></category><category><![CDATA[macOS]]></category><category><![CDATA[macOS Tips]]></category><category><![CDATA[copilot]]></category><category><![CDATA[karabiner-elements]]></category><dc:creator><![CDATA[David Montesdeoca]]></dc:creator><pubDate>Wed, 23 Jul 2025 13:43:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1753278053674/2552475e-82db-479e-b67f-abf5761fdd8f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Recently, I was assigned a spike to check the feasibility of migrating a React application to a microfrontend with <a target="_blank" href="https://sinatrarb.com/">Sinatra</a> + <a target="_blank" href="https://htmx.org/">htmx</a> entirely using AI. In this case, with <a target="_blank" href="https://www.anthropic.com/claude-code">Claude Code</a>.</p>
<p>Of course, I did not have to try to do the entire migration at once, but rather migrate a list of beneficiaries and a detail view of a specific beneficiary, following the existing design.</p>
<p>The main goal was to find out if it was possible to do it with AI and, if so, how long it would take to complete each functionality compared to how long I estimated it would take to do the same manually.</p>
<p>The client I work for gives each developer a <a target="_blank" href="https://github.com/features/copilot">Copilot</a> license for VSCode or a license for <a target="_blank" href="https://cursor.com/">Cursor</a>. You can switch between them as many times as you want, but whenever you activate one, the other automatically expires.</p>
<p>In my case, I have been working with VSCode for a while, but I was offered the possibility of trying Cursor as well, to test the integration with Claude Code.</p>
<p>Cursor is an AI-native IDE forked from VS Code, so it allows for a very easy transition from one IDE to the other. However, I found that a few basic shortcuts did not work as expected: save, find, copy, paste, undo, etc.</p>
<p>The issue was directly related to the shortcuts I configured with <a target="_blank" href="https://karabiner-elements.pqrs.org/">Karabiner-Elements</a> in macOS to behave like in Linux. I talked about it some time ago <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-working-with-macos-being-a-linux-user#heading-karabiner-elements">here</a>.</p>
<p>VSCode's <em>bundler identifier</em> is very easy to find anywhere if you look for it:</p>
<pre><code>com.microsoft.VSCode
</code></pre><p>Cursor's bundler identifier is not as widely known. Finally, I came across <a target="_blank" href="https://github.com/cursor/cursor/issues/901">this Cursor issue</a>, which shows how to get it:</p>
<pre><code class="lang-bash">osascript -e <span class="hljs-string">'id of app "Cursor"'</span>
</code></pre>
<p>The surprise was that the value seems quite random:</p>
<pre><code>com.todesktop<span class="hljs-number">.230313</span>mzl4w4u92
</code></pre><p>I did not know <a target="_blank" href="https://victorscholz.medium.com/what-is-osascript-e48f11b8dec6">osascript</a>, so I thought it might not be the bundler identifier I was looking for.</p>
<p>To be sure, I did the same with VSCode:</p>
<pre><code class="lang-bash">osascript -e <span class="hljs-string">'id of app "code"'</span>
</code></pre>
<p>The value is as expected:</p>
<pre><code>com.microsoft.VSCode
</code></pre><p>I assumed that, at least for now, that Cursor's bundle identifier would be enough. I added that value to the array of bundle identifiers of one of the shortcuts configured for VSCode in Karabiner-Elements and it worked perfectly.</p>
<p>Finally, I added it to the rest of the shortcuts for VSCode. You can see the complete configuration in <a target="_blank" href="https://gist.github.com/backpackerhh/2448998967f178f0114de6c6a3eb37df">this gist</a>.</p>
<p>In case you did not know, the <a target="_blank" href="https://karabiner-elements.pqrs.org/docs/json/complex-modifications-manipulator-definition/conditions/frontmost-application/#investigate-the-bundle-identifier-and-file-path">official Karabiner-Elements documentation</a> includes an alternative way to get an application's bundle identifier.</p>
<p>In any case, I continued investigating about that unusual bundler identifier and found <a target="_blank" href="https://forum.cursor.com/t/cursor-bundle-identifier/779">this thread</a> in the Cursor forum, where Cursor's CEO and founder confirm that this value will not change, because the application is already published.</p>
<p>Therefore, we can rest assured that it will not be necessary to change the configuration in the future.</p>
<p>Thank you for reading and see you in the next one!</p>
<hr />
<div class="hn-embed-widget" id="buy-me-a-coffee"></div>]]></content:encoded></item><item><title><![CDATA[The one with issues scheduling Sidekiq jobs in production]]></title><description><![CDATA[In the project I am currently working on, we have faced some issues lately scheduling Sidekiq jobs in one of our applications.
In any case, I must admit that these issues were caused both by bad decis]]></description><link>https://davidmontesdeoca.dev/the-one-with-issues-scheduling-sidekiq-jobs-in-production</link><guid isPermaLink="true">https://davidmontesdeoca.dev/the-one-with-issues-scheduling-sidekiq-jobs-in-production</guid><category><![CDATA[sidekiq-scheduler]]></category><category><![CDATA[software development]]></category><category><![CDATA[sidekiq]]></category><category><![CDATA[Ruby]]></category><category><![CDATA[sinatrarb]]></category><category><![CDATA[Redis]]></category><category><![CDATA[observability]]></category><category><![CDATA[o11y]]></category><category><![CDATA[Zeitwerk]]></category><category><![CDATA[RTFM]]></category><dc:creator><![CDATA[David Montesdeoca]]></dc:creator><pubDate>Sun, 29 Jun 2025 08:36:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1751186071676/7fba24fa-a92e-443e-8c4e-df8e2bea110f.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In the project I am currently working on, we have faced some issues lately scheduling <a href="https://github.com/sidekiq/sidekiq">Sidekiq</a> jobs in one of our applications.</p>
<p>In any case, I must admit that these issues were caused both by bad decisions made in the past and by not having configured these tools properly more recently.</p>
<blockquote>
<p>I will use the <a href="https://github.com/sidekiq/sidekiq/wiki/Best-Practices#4-use-precise-terminology">precise terminology</a> that the Sidekiq wiki recommends. Therefore, I will not use the term <em>worker</em>.</p>
</blockquote>
<h2>Context</h2>
<p>We started with a legacy payouts application, what we call <strong>payouts v1</strong> (just <strong>v1</strong> from now on).</p>
<p>It is a somewhat old Sinatra application that, among other things, does not use <a href="https://github.com/fxn/zeitwerk">Zeitwerk</a>, so:</p>
<ul>
<li><p>It does not follow a conventional file structure. The namespace of the different classes and modules does not respect the directory hierarchy, so it is common not to find what you expect where you would expect to find it.</p>
</li>
<li><p>There is no configuration for <a href="https://github.com/fxn/zeitwerk#inflection">inflection</a>, but submodules like AMQP or SFTP exist anyway.</p>
</li>
<li><p>It does not use <em>eager loading</em> in much of the code, so you must require classes and modules explicitly at the beginning of each file.</p>
</li>
</ul>
<p>For a new feature of internal payouts between the company's own accounts, it was decided to do it separately from v1.</p>
<p>The team considered the possibility of making what we call <strong>payouts v2</strong> (just <strong>v2</strong> from now on) as an independent (micro)service that would communicate with v1 through domain events if needed, but the final choice was leaving the source code of v2 in the same repository as v1.</p>
<p>However, it does not follow the classic directory structure of other <strong>monorepos</strong> I have worked on before, where each application has a separate subdirectory. In this case, v1 code is still at the root of the repository, while v2 code is inside a <code>/v2</code> directory.</p>
<p>Although v2 is a somewhat more modern application, using Zeitwerk, for example, there is also a lot of logic copied from v1 and adapted as needed. Sidekiq configuration was not copied, though.</p>
<p>In v1, all available queues, their weight, the maximum concurrency, and the queue where each job will run in are explicitly defined:</p>
<pre><code class="language-bash">bundle exec sidekiq -r ./config/sidekiq_boot.rb -q high_priority,6 -q medium_priority,3 -q low_priority,1 —concurrency 10
</code></pre>
<p>In v2, nothing is explicitly defined, so by default, all jobs run in the <a href="https://github.com/sidekiq/sidekiq/wiki/Advanced-Options#queues">default queue</a> with a <a href="https://github.com/sidekiq/sidekiq/wiki/Advanced-Options#concurrency">concurrency with 5 threads</a>:</p>
<pre><code class="language-bash">bundle exec sidekiq -r ./config/sidekiq_boot.rb
</code></pre>
<p>Both applications run in separate Docker containers.</p>
<p>Although we also have separate containers to run the background jobs with Sidekiq separately, <strong>both applications share the same Redis instance</strong>. Not only between them, but they also share it with other main applications of the project.</p>
<blockquote>
<p>The team is expected to start working on using a non-shared Redis for the payouts application very soon.</p>
</blockquote>
<p>The current configuration has caused us many problems, especially with Sidekiq, although we have faced other problems that we will not discuss here.</p>
<p>In the case of v1, we use the <a href="https://github.com/sidekiq-scheduler/sidekiq-scheduler">sidekiq-scheduler</a> extension to schedule some Sidekiq jobs:</p>
<pre><code class="language-ruby"># config/initializers/sidekiq.rb
require "sidekiq"
require "sidekiq-scheduler"

# other logic omitted

redis_config = {
  namespace: "sidekiq_payouts",

  # other keys omitted
}

Sidekiq.configure_client do |config|
  config.redis = redis_config

  # other configurations omitted
end

Sidekiq.configure_server do |config|
  config.redis = redis_config

  # other configurations omitted
end

Sidekiq::Scheduler.enabled = true
Sidekiq::Scheduler.dynamic = true
</code></pre>
<pre><code class="language-ruby"># config/sidekiq_boot.rb
require "sidekiq"
require "sidekiq-scheduler"

require_relative "./boot"

# other logic omitted

Sidekiq.set_schedule(
  "bank_acknowledge_notification_job",
  {
    every: "3 minutes",
    class: "Jobs::BankAcknowledgedNotificationJob",
    queue: "high_priority"
  }
)

SidekiqScheduler::Scheduler.instance.load_schedule!
</code></pre>
<h2>Part I</h2>
<p>Recently, a new functionality require us to schedule jobs in v2 as well, and we used the same extension for that purpose.</p>
<p>We started by copying the scheduling configuration, not from v1 but from other more modern project, where the same Redis instance is not shared.</p>
<p>An important detail is that when Sidekiq was configured in v2, the same Redis configuration was kept, including the namespace, for the server and the client:</p>
<pre><code class="language-ruby"># v2/config/initializers/sidekiq.rb
require "sidekiq"

redis_config = {
  namespace: "sidekiq_payouts",

  # other keys omitted
}

Sidekiq.configure_client do |config|
  config.redis = redis_config

  # other configurations omitted
end

Sidekiq.configure_server do |config|
  config.redis = redis_config

  # other configurations omitted
end
</code></pre>
<p>In the first approach to schedule jobs in v2, the configuration was something like this:</p>
<pre><code class="language-ruby"># v2/config/sidekiq_boot.rb
require "sidekiq"
require "sidekiq-scheduler"

require_relative "./boot"

# other logic omitted

Sidekiq.schedule = {
  "bank_transfers_download_feedback_files_job": {
    cron: "0 */5 * * * *",
    class: "Jobs::BankTransfers::DownloadFeedbackFilesJob"
  }
}.freeze

Sidekiq::Scheduler.enabled = true
</code></pre>
<p>This job is responsible for downloading feedback files from the banks indicating whether the bank transfers were successful or not.</p>
<p>We deployed the changes to production and checked that <strong>the new job was running correctly every 5 minutes</strong>.</p>
<blockquote>
<p>These changes were part of an epic that was still in progress at that moment, so our clients were not yet using them in production.</p>
</blockquote>
<p>Coincidentally, the team members involved in this feature had the next day off because it was a public holiday in Madrid.</p>
<p>During our day off, a support ticket was opened reporting that, for some payments from a certain bank, the receipt confirmation had not been sent.</p>
<p>The next day, when we returned to work, we quickly identified the problem: The bank saves the feedback files on the same SFTP server and in the same directory for both processes, with slightly different filename patterns.</p>
<p>The process in v2 was saving a backup copy of those files in an S3 bucket and deleting the files that v1 was supposed to process from the SFTP server.</p>
<p><strong>The process in v1 runs every 3 minutes</strong>, so in a particular case, the process in v2 got ahead of the process in v1. The same would have happened the other way around if the new feature for v2 would have been used in production.</p>
<p>Both processes use a <em>filename pattern</em> to determine if they should process a file.</p>
<p>e.g. <code>dbdi.000_report_00000000000000000.xml</code>:</p>
<pre><code class="language-ruby"># v1 filename pattern
/dbdi.000_report_*/
</code></pre>
<pre><code class="language-ruby"># v2 filename pattern
/dbdi\.\d{3}_report_\d{17}\.xml\z/
</code></pre>
<blockquote>
<p>The filename pattern in v2 is more restrictive but also matches the given filename.</p>
</blockquote>
<p>During the QA process of the new feature in v2, we did not notice this problem because it never happened that the process in v1 got ahead of the process in v2.</p>
<p>The solution was quite clear: change the filename pattern in v2, as it includes a different prefix than v1.</p>
<p>However, we first quickly deployed a hotfix to production to prevent the same thing from happening again and thus be able to take enough time to test the change calmly:</p>
<pre><code class="language-ruby"># v2/config/sidekiq_boot.rb
require "sidekiq"
require "sidekiq-scheduler"

require_relative "./boot"

# other logic omitted

# Sidekiq.schedule = {
#   "bank_transfers_download_feedback_files_job": {
#      cron: "0 */5 * * * *",
#      class: "Jobs::BankTransfers::DownloadFeedbackFilesJob"
#   }
# }.freeze

# Sidekiq::Scheduler.enabled = true
</code></pre>
<blockquote>
<p>A better approach is to use environment variables for enabling/disabling that functionality, so we would not need a new deployment.</p>
</blockquote>
<p>In production, we simply checked that the new job from v2 was no longer running every 5 minutes. Then, we proceeded to apply the change in the filename pattern and re-enabled Sidekiq Scheduler.</p>
<pre><code class="language-ruby"># v1 filename pattern
/\Adbdi.000_report_*/
</code></pre>
<pre><code class="language-ruby"># v2 filename pattern
/\A[A-Z]{3}_dbdi\.\d{3}_report_\d{17}\.xml\z/
</code></pre>
<p>We pushed the changes, the CI pipeline passed, and we calmly tested in our test environment that the new job from v2 did not process the files for v1.</p>
<p>Approximately 4 hours after having deployed the hotfix, we deployed the new changes to production.</p>
<p>It was then that we started monitoring the scheduled jobs in both v1 and v2.</p>
<p>There we realized that <strong>since the deployment of the hotfix, no scheduled job from v1 had been processed again</strong>.</p>
<p>I'll tell you the reason later in <a href="#part-ii">part II</a>. No spoilers here!</p>
<p>We ran a Rake task in our test environment that shows us the output of <code>Sidekiq.get_schedule</code> in v1:</p>
<pre><code class="language-json">{
  "bank_transfers_download_feedback_files_job": {
     "cron": "0 */5 * * * *",
     "class": "Jobs::BankTransfers::DownloadFeedbackFilesJob",
     "queue": "default"
  }
}
</code></pre>
<p>There was no doubt. <strong>The scheduler from v2 was overwriting the scheduler from v1</strong>.</p>
<pre><code class="language-mermaid">graph TD;
    subgraph "Sidekiq Containers"
        V1[Payouts v1 Container];
        V2[Payouts v2 Container];
    end

    subgraph Redis[Shared Redis Instance]
        SharedNamespace("sidekiq_payouts" namespace);
    end

    V1 -- "Writes schedule to" --&gt; SharedNamespace;
    V2 -- "OVERWRITES schedule in" --&gt; SharedNamespace;

    style V2 fill:#f7baba,stroke:#c71f1f,stroke-width:2px
</code></pre>
<p>We thought then that we could schedule the job from v2 in v1, since we knew that the configuration for v1 had been working well until then:</p>
<pre><code class="language-ruby"># config/sidekiq_boot.rb

require "sidekiq"
require "sidekiq-scheduler"

require_relative "./boot"

# other logic omitted

Sidekiq.set_schedule(
  "bank_acknowledge_notification_job",
  {
    every: "3 minutes",
    class: "Jobs::BankAcknowledgedNotificationJob",
    queue: "high_priority"
  }
)

Sidekiq.set_schedule(
  "bank_transfers_download_feedback_files_job",
  {
    every: "5 minutes",
    class: "Jobs::BankTransfers::DownloadFeedbackFilesJob"
  }
)

SidekiqScheduler::Scheduler.instance.load_schedule!
</code></pre>
<p><strong>Desperate times call for desperate measures.</strong></p>
<p>We deleted all the configuration for Sidekiq Scheduler from v2 and tested that everything worked.</p>
<p>Note that I said <em>the configuration</em>. This is an important detail. More on that later.</p>
<p>To give some more context, when we merge the changes of an <a href="https://docs.gitlab.com/user/project/merge_requests/">MR</a>, they are first deployed to the staging environment. There we were able to check that everything was working as expected by scheduling all the jobs, both for v1 and v2.</p>
<p>We deployed the changes to production and requested in a support ticket to run the same Rake task that before, the one that shows the scheduled jobs.</p>
<p>We confirmed that the output of that task was showing all the scheduled jobs we expected.</p>
<p>That day we finished around 6 p.m. Not bad for a Friday afternoon.</p>
<p>As you may have imagined, this story does not end here.</p>
<h2>Part II</h2>
<p>For 10 days, we forgot about the scheduled Sidekiq jobs. There had been 6 deployments to production since that fateful Friday and everything had been working as expected. We had been lucky until then.</p>
<p>With the 7th deployment, which had nothing to do with Sidekiq jobs, our luck ran out.</p>
<p>The day after that deployment, we realized that none of the scheduled jobs had been processed since then.</p>
<p>We first checked what had been deployed to production since the last time we fixed it. Again, nothing related to Sidekiq jobs.</p>
<p>We found the following trace in our logs in production:</p>
<blockquote>
<p>Removing schedule bank_acknowledge_notification</p>
<p>Removing schedule bank_transfers_download_feedback_files_job</p>
</blockquote>
<p>We tried restarting the application in production without success. Same trace in the logs.</p>
<p>We had to deploy some changes to production that same morning, so we decided to wait and see if we got lucky again, although we were still trying to figure out the root of the problem. Again, no luck.</p>
<p>That afternoon, the team dedicated the refinement meeting we had scheduled to figuring out what was going on.</p>
<p>Although we had removed the configuration for Sidekiq Scheduler from the Sidekiq container from v2, we were still seeing the <a href="https://github.com/sidekiq-scheduler/sidekiq-scheduler/blob/master/lib/sidekiq-scheduler/scheduler.rb#L71">following trace</a> in the logs:</p>
<blockquote>
<p>Scheduling Info</p>
</blockquote>
<p>We started the application locally and carefully checked the logs, filtering for references to "scheduler".</p>
<p>Finally, we found <a href="https://github.com/getsentry/sentry-ruby/blob/master/sentry-sidekiq/lib/sentry/sidekiq-scheduler/scheduler.rb#L5">the problem</a> in one of our dependencies:</p>
<pre><code class="language-ruby">begin
  require "sidekiq-scheduler"
rescue LoadError
  return
end
</code></pre>
<p>We had deleted the configuration from v2, but <strong>we had not removed the gem from the Gemfile in v2</strong>.</p>
<p>We deployed that change to production and checked that everything was working correctly.</p>
<p><strong>We had found a race condition</strong>. We had been lucky from the beginning because the Sidekiq container from v1 had been starting after the Sidekiq container from v2.</p>
<p>The moment the Sidekiq container from v2 started after the Sidekiq container from v1, all scheduled jobs from v1 were removed, because v2 had no scheduled jobs.</p>
<p>Of course, we did not carefully read the <a href="https://github.com/sidekiq-scheduler/sidekiq-scheduler#notes-when-running-multiple-sidekiq-processors-on-the-same-redis">gem's documentation</a>, where we would have found the following recommendation:</p>
<blockquote>
<p>If you're running multiple Sidekiq processes on the same Redis namespace with different configurations, you'll want to explicitly disable Sidekiq Scheduler for the other processes not responsible for the schedule. If you don't, the last booted Sidekiq processes' schedule will be what is stored in Redis.</p>
</blockquote>
<p><a href="https://en.wikipedia.org/wiki/RTFM">RTFM!</a></p>
<p>And that is the reason why the scheduled jobs from v1 had stopped running when we had disabled Sidekiq Scheduler in v2 with the hotfix.</p>
<p>We also discovered that scheduling jobs from v2 in the Sidekiq container from v1 had worked by chance from the beginning.</p>
<p>The scheduled job from v2 did not have a explicitly defined queue, so it was being processed in the <em>default</em> queue. As that queue was not defined in the Sidekiq container from v1, by sharing the same Redis instance and the same namespace, that job had been processed in the Sidekiq container from v2.</p>
<h2>Part III</h2>
<p>During one of the tests that the stakeholders did in production of this new functionality, we received an alert in the corresponding Slack channel because an error had been registered during the process of downloading the feedback files from the bank.</p>
<p>We rushed to investigate it and found that the same file had been processed twice.</p>
<p>The first time the process had succeeded, so a record was created in the database with details of the downloaded feedback file, including the filename. The second time an error was registered, because we already had that record in the database for the same filename.</p>
<blockquote>
<p>For now, it is registered as an error, although we should probably change it to a warning later.</p>
</blockquote>
<p>Our first thought was that maybe the bank had mistakenly left us the same file on the SFTP server twice. However, that hypothesis did not add up because we only delete files from the SFTP server when the entire process succeeds, and the job is scheduled every 5 minutes, so we should have been registering the exact same error every 5 minutes.</p>
<p>Then we realized that <strong>the same file had been processed twice in a matter of 2 seconds</strong>.</p>
<p>Without finding an apparent reason for this to be happening, we assumed that the cause would be related to still having multiple Sidekiq containers sharing the same Redis namespace.</p>
<p>We already had a task on our board to explicitly add queues to jobs classes and scheduled jobs in v2, so we started from there.</p>
<p>The idea was to deploy it in two steps, doing the following:</p>
<ol>
<li>Define the <em>default</em> queue in v1, so that any job from v2 that was already enqueued at the time of deployment could be successfully processed in v1.</li>
</ol>
<pre><code class="language-bash">bundle exec sidekiq -r ./config/sidekiq_boot.rb -q default,1 -q high_priority,6 -q medium_priority,3 -q low_priority,1 —concurrency 10
</code></pre>
<p>And explicitly specify in every job class in v2 in which queue should be processed:</p>
<pre><code class="language-bash">bundle exec sidekiq -r ./config/sidekiq_boot.rb -C ./config/sidekiq.yml
</code></pre>
<p>In that configuration file, something like the following is specified:</p>
<pre><code class="language-yaml"># v2/config/sidekiq.yml
:concurrency: 10
:queues:
  - [v2_high_priority,6]
  - [v2_medium_priority,3]
  - [v2_low_priority,1]
</code></pre>
<p>In the job class, it is defined as follows:</p>
<pre><code class="language-ruby"># v2/app/jobs/bank_transfers/download_feedback_files_jobs.rb
require "sidekiq"

module Jobs
  module BankTransfers
    class DownloadFeedbackFilesJob
      include Sidekiq::Job

      sidekiq_options retry: false, queue: "v2_medium_priority"

      # other logic omitted
    end
  end
end
</code></pre>
<p>Note that when a job is scheduled from v1, a queue from v1 is used, while when the same job is processed in a non-scheduled way, a queue from v2 is used:</p>
<pre><code class="language-ruby"># config/sidekiq_boot.rb

# other logic omitted

Sidekiq.set_schedule(
  "bank_transfers_download_feedback_files_job",
  {
    every: "5 minutes",
    class: "Jobs::BankTransfers::DownloadFeedbackFilesJob",
    queue: "medium_priority"
  }
)
</code></pre>
<ol>
<li>Delete the <em>default</em> queue from v1, now that all jobs run in a specific queue.</li>
</ol>
<p>However, with this configuration, we encountered unexpected errors at that time when processing the scheduled job from v2 in v1:</p>
<blockquote>
<p>NameError: Services::BankTransfers::DownloadBatchFiles::Dry</p>
</blockquote>
<p>In our services, we use <code>Dry::Monads</code> to return <code>Success</code> or <code>Failure</code>, so it should be enough to add the following:</p>
<pre><code class="language-yaml"># v2/app/services/bank_transfers/download_feedback_files.rb
require "dry-monads"
</code></pre>
<p>Indeed, that error was already resolved, but one error after another kept appearing, in what seemed like an endless loop. With each require we added, a new dependency had to be specifically required. Some tests in v1 started to fail due to this problem.</p>
<p>The problem basically is that the v2 files are not autoloaded in v1 because it is not really necessary. In practice, they are independent applications, although we were taking advantage of the fact that both are in the same repository to process jobs from v2 in v1.</p>
<p>A possible solution for the dependency issue is the following:</p>
<pre><code class="language-bash">bundle exec sidekiq -r ./config/sidekiq_boot.rb -r ./v2/config/sidekiq_boot.rb &lt;...rest omitted&gt;
</code></pre>
<p>Although that configuration did not solve the problem of the scheduled jobs running twice.</p>
<p>Finally, we opted for implementing another solution that the team had agreed on in another of the tasks that were on our board: <strong>Configure Sidekiq in payouts v1 and v2 to use different Redis namespaces</strong>.</p>
<p>We added sidekiq-scheduler to the Gemfile again in v2 and defined a different namespace for each application:</p>
<pre><code class="language-ruby"># v2/config/initializers/sidekiq.rb
require "sidekiq"

redis_config = {
  namespace: "v2_sidekiq_payouts",

  # other keys omitted
}

Sidekiq.configure_client do |config|
  config.redis = redis_config

  # other configurations omitted
end

Sidekiq.configure_server do |config|
  config.redis = redis_config

  # other configurations omitted
end
</code></pre>
<pre><code class="language-ruby"># v2/config/sidekiq_boot.rb
require "sidekiq"
require "sidekiq-scheduler"

require_relative "./boot"

# other logic omitted

Sidekiq.schedule = {
  "bank_transfers_download_feedback_files_job": {
    cron: "0 */5 * * * *",
    class: "Jobs::BankTransfers::DownloadFeedbackFilesJob",
    queue: "v2_medium_priority"
  }
}.freeze

Sidekiq::Scheduler.enabled = true
</code></pre>
<pre><code class="language-mermaid">graph TD;
    subgraph "Sidekiq Containers"
        V1[Payouts v1 Container];
        V2[Payouts v2 Container];
    end

    subgraph Redis[Shared Redis Instance]
        direction LR
        V1_NS("sidekiq_payouts" namespace);
        V2_NS("v2_sidekiq_payouts" namespace);
    end

    V1 -- "Reads/Writes" --&gt; V1_NS;
    V2 -- "Reads/Writes" --&gt; V2_NS;

    style V1_NS fill:#d4edda,stroke:#155724,stroke-width:2px
    style V2_NS fill:#d4edda,stroke:#155724,stroke-width:2px
</code></pre>
<p>We also checked that the namespace change only affected Sidekiq. For example, the gem developed by the company to define feature flags uses Redis internally, but uses its own namespace.</p>
<p>We deployed these changes in our test environment and were able to verify that everything worked as expected. We deployed to staging and production and we got the same result.</p>
<p>With this configuration, each Sidekiq container is independent at the namespace level, although they still share Redis between them and with other applications.</p>
<p>We also managed to get the scheduled jobs to run only once.</p>
<h2>Conclusion</h2>
<p>We definitely learned the hard way how to configure Sidekiq properly according to our current needs.</p>
<p>It was not easy, it took us a lot of time, but we are happy with the result.</p>
<p>Some lessons learned:</p>
<ul>
<li><p>Better organize the structure of applications that share the same repository.</p>
</li>
<li><p>Carefully read the documentation of the tools we use.</p>
</li>
<li><p>Add alerts when a certain period passes without processing scheduled jobs.</p>
</li>
<li><p>Test any change more exhaustively before uploading it to production.</p>
</li>
<li><p>Do not share a Redis instance between multiple applications, especially the same namespace, because it can lead to unpredictable behavior.</p>
</li>
</ul>
<p>Thank you for reading, and see you in the next one!</p>
]]></content:encoded></item><item><title><![CDATA[The one about unrealistic expectations on software engineers]]></title><description><![CDATA[This is a topic I have discussed countless times with engineer peers and friends outside the tech world alike.
It is generally agreed that software engineers occupy a privileged position. This profess]]></description><link>https://davidmontesdeoca.dev/the-one-about-unrealistic-expectations-on-software-developers</link><guid isPermaLink="true">https://davidmontesdeoca.dev/the-one-about-unrealistic-expectations-on-software-developers</guid><category><![CDATA[software development]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[technology]]></category><category><![CDATA[tech ]]></category><category><![CDATA[job search]]></category><dc:creator><![CDATA[David Montesdeoca]]></dc:creator><pubDate>Wed, 28 May 2025 19:26:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1748369490093/dd8f716c-0002-403b-ba5f-3fd1821f9d45.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is a topic I have discussed countless times with engineer peers and friends outside the tech world alike.</p>
<p>It is generally agreed that software engineers occupy a privileged position. This profession is not only highly in-demand but also offers a host of advantages rarely found elsewhere: competitive salaries, <a href="https://davidmontesdeoca.dev/the-one-about-my-experience-working-remotely">remote work</a>, and flexible schedules, among others.</p>
<p>However, this privilege comes with a set of <strong>unrealistic expectations</strong>. A case of <em>divine justice</em>, you might say?</p>
<p>I recently came across an excellent post about <a href="https://0x1.pt/2025/04/06/the-insanity-of-being-a-software-engineer/">the insanity of being a software engineer</a> and felt compelled to expand on its insights.</p>
<p><strong>Getting started is not easy at all</strong>, especially if you have no experience. I would say that it is difficult to find job offers for junior engineers, but in those rare cases where you do find them, it is common that companies ask for 2-3 years of experience. How can one get the experience if no one will provide the first opportunity?</p>
<p>Nowadays, you have bootcamps, for example, that have collaboration agreements with tech companies that give the student a chance to start an internship upon finishing, but they are not guaranteed to continue in the company once the internship is over.</p>
<p>That brings me to another key point. While it is probably nothing new, I feel that the <strong>hiring processes nowadays are wild</strong>.</p>
<p>Companies have <a href="https://mrshiny608.github.io/MrShiny608/commentary/2025/04/29/AdversarialInterviews.html#unrealistic-expectations-">unrealistic job requirements</a>, often <strong>demanding an entire development team's worth of skills in a single candidate</strong>. It risks discouraging candidates who would otherwise be excellent fits but feel they do not tick enough boxes to apply.</p>
<p>But even when you decide to apply for a position, many candidates are rejected because <strong>interviewers fail to see that skills are transferable</strong> and the match should not be limited to a predefined checklist.</p>
<p>The way I see it, this situation is a consequence of the normalization that engineers must master (almost) all related areas. For instance, check the roadmaps for <a href="https://roadmap.sh/backend">backend</a>, <a href="https://roadmap.sh/frontend">frontend</a>, <a href="https://roadmap.sh/full-stack">full stack</a>, or <a href="https://roadmap.sh/software-architect">software architects</a>.</p>
<p>For a typical <em>backend</em> position, you are expected to master the main programming language used in the project, but preferably you should master more than one, because that will give you the tools to apply different approaches depending on the occasion, especially if those languages allow you to apply different programming paradigms, such as OOP and functional.</p>
<p>Of course, you must be fluent with the entire ecosystem surrounding those languages, starting with the most popular frameworks, because they are the tool you will work with in your day-to-day.</p>
<p>You are also expected to master databases, at least one of the most popular SQL DBMS (PostgreSQL, MySQL) and some NoSQL (MongoDB, Redis). Performance tuning, data modeling, security... What happened to the role of the <a href="https://www.oracle.com/database/what-is-a-dba/">database administrator (DBA)</a>?</p>
<p>Experience designing, implementing and consuming REST APIs or GraphQL is also a must.</p>
<p>Depending on your level, you must also be able to design efficient systems, always with performance, scalability, maintainability, and security in mind.</p>
<p>In a world dominated by (micro)services, you are also expected to know every architectural pattern, such as <a href="https://aws.amazon.com/what-is/eda/">event-driven architecture (EDA)</a>.</p>
<p>You could think that is enough, right? Well, not quite...</p>
<p>You must know your way around the command line to be able to configure the system you are working on and ensure it is working nicely.</p>
<p>Do not forget about monitoring the system, responding to incidents caused by changes introduced to applications in your team's governance, and automating tasks to ensure the reliability of the system.</p>
<p>You are expected to be proficient with containers and cloud providers too, configuring and provisioning the infrastructure using code instead of manual processes and settings (<a href="https://aws.amazon.com/what-is/iac/">IaC</a>). Who needs <a href="https://roadmap.sh/r/system-engineer">SysAdmins</a>, <a href="https://roadmap.sh/devops">SREs, or DevOps</a> anymore, right?</p>
<p>Do not forget about <a href="https://roadmap.sh/ai-engineer">AI</a>, which makes it increasingly common to find job offers asking for knowledge in LLMs, RAG, fine-tuning, or transformers, to name a few.</p>
<p>And all of that is without even getting into the <em>full-stack</em> role, where you will need to add <em>frontend</em> skills to the equation.</p>
<p>On the one hand, if the main workload is on the <em>backend</em>, you will not need to be an expert, but you will certainly have to know JS and the web ecosystem well enough.</p>
<p>On the other hand, if the entire project stack is based on JS, also working with Node on the <em>backend</em>, for example, then you will have to be an expert in the entire web and JS ecosystem in particular, whether it is frameworks, UI libraries, or other tools.</p>
<p>For sure, in a more purely <em>frontend</em> role, you are expected to be an excellent designer, have good taste for interfaces, and have solid knowledge of UX and usability.</p>
<p>And we are still just getting started, because I have not yet mentioned all the expertise required no matter the role.</p>
<p>You are required a high level of technical proficiency and a strong understanding of software development best practices, partaking in all stages of the development lifecycle, from initial task definition to final deployment.</p>
<p>Needless to say, your code must be clean, scalable, maintainable, and testable, preferably applying <a href="https://martinfowler.com/bliki/TestDrivenDevelopment.html">TDD</a>. For that, you will need to master <a href="https://refactoring.guru/design-patterns">design patterns</a>, <a href="https://refactoring.guru/refactoring">refactoring techniques</a>, and every software design principle ever coined, such as <a href="https://en.wikipedia.org/wiki/SOLID">SOLID</a>.</p>
<p>I really like what the author that somehow inspired this post says about it:</p>
<blockquote>
<p>Software gets more complicated. All of this complexity is there for a reason. But what happened to specializing? When a house is being built, tons of people are involved: architects, civil engineers, plumbers, electricians, bricklayers, interior designers, roofers, surveyors, pavers, you name it. You don't expect a single person, or even a whole single company, to be able to do all of those.</p>
</blockquote>
<p><strong>Jack of all trades, master of none.</strong></p>
<p>And there is still more, although it is not exclusive to this profession. You are expected to have not only a wide variety of hard skills but also a bunch of <strong>soft skills</strong> that would make you a solid candidate for "Employee of the Year".</p>
<p>You are expected to be a highly motivated team player with leadership skills; an excellent communicator with both technical and non-technical people; and possess proactivity, the ability to give and receive feedback, a capacity for self-management, abstract thinking, attention to detail, and adaptability to change, among many others.</p>
<p>Naturally, you should be someone with experience working in startups or fast-paced environments, with a business-driven, product-focused mindset, and be able to balance technical debt with delivering new features quickly.</p>
<p>Note that all those skills have been extracted from real job offers I have seen lately.</p>
<p>I love what I do, and the opportunity to constantly learn is one of the best parts of this job. However, I think the industry needs to move away from seeking <a href="https://www.simplethread.com/the-10x-programmer-myth/">mythical 10x engineers</a>.</p>
<p>My suggestion is to not expect to hire a single person to do the work of an entire department, and rely on the candidates' ability to learn and adapt. Foster a culture that values deep expertise as much as broad knowledge to build stronger teams.</p>
<p>Thank you for reading and see you in the next one!</p>
]]></content:encoded></item><item><title><![CDATA[The one about blogging]]></title><description><![CDATA[It has been two years now since I started writing this blog.
The way I see it, maintaining a blog requires discipline. Those who know me well know that discipline is one of my key strengths.
However, ]]></description><link>https://davidmontesdeoca.dev/the-one-about-blogging</link><guid isPermaLink="true">https://davidmontesdeoca.dev/the-one-about-blogging</guid><category><![CDATA[Blogging]]></category><category><![CDATA[blog]]></category><category><![CDATA[development]]></category><category><![CDATA[developers]]></category><category><![CDATA[knowledge]]></category><dc:creator><![CDATA[David Montesdeoca]]></dc:creator><pubDate>Tue, 29 Apr 2025 14:46:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1746204154363/eb94fb98-4299-4505-8cb9-20f9b56cc44d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>It has been two years now since I started writing this blog.</p>
<p>The way I see it, maintaining a blog <strong>requires discipline</strong>. Those who know me well know that discipline is one of my key strengths.</p>
<p>However, I never dared to take the leap before because several developer friends, whom I admire professionally, had tried and quickly fell by the wayside, usually after the first or second post. Why would I be any different?</p>
<p>I knew that if I were to start, I had to take it seriously. Two years later, I can say that, for now, I have achieved what I initially set out to do: <strong>publish at least one post per month</strong>.</p>
<p>It all started with <a href="https://davidmontesdeoca.dev/the-one-with-access-denied-to-aws-in-production">a post</a> where I talked about an error that caused Domestika application to lose access to AWS S3 in production for a few minutes.</p>
<p>Normally, I would have saved the solution to that error in an Evernote note, including a link to where I found it. I would also save that link as a bookmark. That was my <em>modus operandi</em> for many years.</p>
<p>Over the years, I rarely actually used those notes or bookmarks when I found myself in a similar situation. Typically, when in need of that information again, I had completely forgotten that I had noted it down or saved the bookmark. To be perfectly honest, it is very easy and convenient to perform a quick search and, in fact, I normally ended up on the same Stack Overflow thread where I had already found the solution before.</p>
<p>So why did I keep saving them? Old habits die hard.</p>
<p>My approach now, instead of saving that stuff in notes or bookmarks, is to write a post. Writing a post is completely different. There have been several occasions when I needed something I had previously written about, and I always come directly to the blog to see what I wrote instead of doing a quick search.</p>
<p>Do not get me wrong, I still save some notes with details that are not quite enough for a full blog post, but I do it much less than before.</p>
<p>An important decision was also which platform to host my blog on. I was pretty sure I preferred not to create a custom solution when very popular options already exist, especially for developers, like <a href="http://dev.to">dev.to</a> or <a href="https://medium.com/">Medium</a>, which would likely also give me more visibility. However, when I researched the options a bit, I quickly opted for <a href="https://hashnode.com/">Hashnode</a>. Without getting into too many comparisons, it is exactly what I was looking for: the ability to easily export my posts, write in Markdown, back up to a GitHub repository, add a custom domain, among many other things. Also, in my opinion, it is a platform with a much more attractive look and feel than other options. And, as if that were not enough, Hashnode has a <a href="https://apidocs.hashnode.com/">public API</a> that lets you interact with it.</p>
<p>Choosing the language to write in represented another important decision. Spanish is my mother tongue, thus it is the language where I feel most at ease. Furthermore, while Spanish content is growing, there is significantly less of it compared to English, presenting a chance for potentially greater impact on the Spanish-speaking community. Still, I knew the best path for me was writing in English, intending to improve my <strong>writing skills</strong> and aiming to become a better <strong>storyteller</strong> in that language.</p>
<p>The only post I have written in both languages so far is the one about <a href="https://davidmontesdeoca.dev/el-de-mi-experiencia-en-domestika">my experience at Domestika</a>. The reason was that some news had appeared in the press that, as far as I knew, were not entirely true and I wanted to avoid anything getting <em>lost in translation</em>.</p>
<p>Without a doubt, I still have room for improvement in that area, although I feel more comfortable over time. Sometimes, I still find myself writing the text in Spanish initially, as it comes more fluidly, before translating it into English. This approach, however, involves a considerably larger investment of time compared to writing directly in English.</p>
<p>On the one hand, I write for myself for the reasons I already explained, aiming to keep useful stuff accessible, functioning somewhat like a diary or logbook. While I was job hunting last year, every selection process involved questions about <a href="https://davidmontesdeoca.dev/the-one-about-mentoring-junior-developers">how I do certain things</a>, <a href="https://davidmontesdeoca.dev/the-one-about-my-favorite-project-so-far">projects I have worked on</a>, or <a href="https://davidmontesdeoca.dev/the-one-about-my-experience-working-remotely">my experience in general</a>. As time goes by, remembering specific details becomes harder, so I am sure having a post with lots of details will help me be more precise in future selection processes. That is also why I decided to write <a href="https://davidmontesdeoca.dev/series/working-for-an-american-fintech">the series about my experience working for an American fintech as a contractor</a>.</p>
<p>On the other hand, I like the idea of <a href="https://endler.dev/2025/best-programmers/#write">sharing knowledge</a>. If it is useful for me, it might be useful for others too. I am aware I do not have a huge audience; only <a href="https://davidmontesdeoca.dev/the-one-with-a-large-project-in-a-github-repository">a few</a> <a href="https://davidmontesdeoca.dev/the-one-with-openssl-issues-installing-older-ruby-versions-on-ubuntu-2204">posts</a> <a href="https://davidmontesdeoca.dev/the-one-with-a-mouse-jiggler-in-ubuntu">have had</a> <a href="https://davidmontesdeoca.dev/the-one-about-working-with-macos-being-a-linux-user">some impact</a> (according to analytics), but I am proud that the post where I talk about <a href="https://davidmontesdeoca.dev/the-one-about-learning-ruby">how to learn Ruby</a> was included in the <a href="https://newsletter.shortruby.com/p/edition-120">Short Ruby Newsletter</a>. Visits to that post increased considerably after that feature.</p>
<p><strong>Maintaining a blog takes more time than I initially thought</strong>. When you have the chance to write about something you have done recently, it is usually quite straightforward, especially for purely technical posts. However, for various reasons, writing about what you do day-to-day is not always possible, either because I have not finished it by the time I want to write the next post or due to confidentiality. It is in those moments that I turn to a note I keep in Notion where I jot down ideas I think might be interesting.</p>
<p>Once I decide what I am going to write about, the next thing I do is make a list of points I want to include in the post. After that, it is time to shape all those ideas so they make narrative sense and are not just a jumble of loose thoughts. This is probably the phase that takes me the longest.</p>
<p>I usually write at the end of my workday, especially when I can finish earlier. I try not to spend more than a couple of hours each time, because I do not like spending the whole day in front of the computer.</p>
<p>In the future, I would like to write more purely technical posts, because I think those are the ones I enjoy writing the most, and at the same time, they are the ones I feel are most useful. Especially when it comes to writing about <a href="https://davidmontesdeoca.dev/the-one-about-conditionals-in-ruby">my approach to a certain topic</a> or about <a href="https://davidmontesdeoca.dev/the-one-with-access-denied-to-aws-in-production">a mistake made and what I learned from it</a>.</p>
<p>And that is basically my process for maintaining this blog.</p>
<p>Thank you for reading and see you in the next one!</p>
]]></content:encoded></item><item><title><![CDATA[The one about layoffs in an American fintech]]></title><description><![CDATA[Shortly after I published a post about my experience working for an American fintech, the company send a message to the whole company via Slack.
For obvious reasons, I will not share that message, but I would like to share some highlights:

The reven...]]></description><link>https://davidmontesdeoca.dev/the-one-about-layoffs-in-an-american-fintech</link><guid isPermaLink="true">https://davidmontesdeoca.dev/the-one-about-layoffs-in-an-american-fintech</guid><category><![CDATA[fintech]]></category><category><![CDATA[fintech software development]]></category><category><![CDATA[Layoffs]]></category><category><![CDATA[layoff]]></category><dc:creator><![CDATA[David Montesdeoca]]></dc:creator><pubDate>Sun, 30 Mar 2025 14:29:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1742316733298/fe6db248-50e6-4f1a-b15f-3215702b6fa7.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Shortly after I published <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-my-experience-working-for-an-american-fintech-6-months">a post about my experience working for an American fintech</a>, the company send a message to the whole company via Slack.</p>
<p>For obvious reasons, I will not share that message, but I would like to share some highlights:</p>
<ul>
<li><p>The revenue growth was slowed in 2024, so to keep growing the company must adapt.</p>
</li>
<li><p>The company decided to restructure teams, and as a result, approximately 120 positions (both employees and contractor roles) will be eliminated globally, affecting around 10% of the organization.</p>
</li>
<li><p>The company announced the acquisition of a leading software provided to the travel and hospitality industry for over $300M, further helping to diversify the business.</p>
</li>
<li><p>All employees will receive an email stating whether their role was impacted by that restructure.</p>
</li>
</ul>
<p>That day I started working later than usual and the first thing I noticed was a Slack message from my engineering manager (EM) asking me if I had received the email, although he mentioned the communication plan might differ for contractors.</p>
<p>However, some colleagues from SNGULAR unexpectedly received that email, informing them their role was not impacted. I will get back to this later.</p>
<p>This whole situation took everyone off guard, since it was a decision coming from the top management of the company in the USA. Not even our team managers knew about it, so they had no answers to give to all of us.</p>
<p>Tying all the loose ends, for a few weeks before the big announcement they had been gradually installing <a target="_blank" href="https://www.kandji.io/">Kandji</a>, an automation-forward Apple device management (MDM) software, to remotely control our laptops. That software works like a middleware even in the login process of the operating system, so if they remove your access to the project the laptop is basically useless.</p>
<p>In this case, SNGULAR knew nothing about it and even worse, my manager told us he was hiring people to join to this project. So weird.</p>
<p>An <em>All Hands Engineering</em> meeting was scheduled that same day, where the CTO of the company answered some of the questions that were sent anonymously. Not much was clarified at that meeting.</p>
<p>One of the items he did confirm though was that a dozen contractors would stop working on the project and that they were already deciding who would be affected.</p>
<p>Of course, not all contractors affected by the restructure are from SNGULAR, as the client collaborates with other consulting firms as well. But we did not know who nor how many of us would be affected.</p>
<p>The very same day the layoffs were announced, some colleagues from other areas of the company no longer had access to the project. In the engineering area, however not many people was affected, especially in the <em>Transactional</em> area, of which my team is part of.</p>
<p>According to what I later found out, there are several reasons why they could not do the same to everybody else affected by these layoffs, mainly due to the laws of each country:</p>
<ul>
<li><p>In Spain, the standard notice period for terminating an employment contract is 15 days, applicable to both employers and employees, but it is common for companies to terminate employees without prior notice, assuming a severance pay.</p>
</li>
<li><p>In some cases, it was necessary to wait to remove the access to the project to workers in other time zones.</p>
</li>
<li><p>In other cases, there was a <a target="_blank" href="https://www.gov.uk/redundancy-your-rights/consultation">consultation period</a>.</p>
</li>
<li><p>In the case of contractors, it depends on the notice period agreed with the consulting firm.</p>
</li>
</ul>
<p>In our next <em>1 to 1</em> meeting, the EM confirmed that 2 or 3 people from SNGULAR would left the project, but they did not know who exactly yet. He also told me that I was among the top contractors in terms of performance and implied that he did not expect any changes concerning my position, but could not guarantee that I would not be affected. I felt somewhat relieved in that moment, although it was not yet confirmed.</p>
<p>A couple of weeks later, in my monthly <em>1 to 1</em> meeting with the manager from SNGULAR, he confirmed that two colleagues would left the project at the end of the month and they had already been notified. In that moment, he did not know if more people would be also affected.</p>
<p>Surprisingly, one of the colleagues who had received the email stating their role was not impacted, was one of the people affected. D'oh!</p>
<p>Since the notice period agreed upon with SNGULAR is 3 weeks, my colleagues are still working on the project, until the end of the month. Tough position for them.</p>
<p>Finally, a week later both managers confirmed that the budget for 2025 is set in the fintech company and no further changes are expected, barring a disaster. The EM also confirmed our team will stay the same during Q2.</p>
<p>Although the company's stock price fell by nearly 40% immediately following the release of their previous year's financial results and the announcement of the layoffs. Who knows what is yet to come.</p>
<p>Anyway, I must say that during those weeks, I was very concerned about my future, because I was hired specifically to work for this client and did not feel like working for another one, that would have been the worst case scenario for any of us.</p>
<p>Given the uncertainty, I seriously thought about starting to search for a new job, although I did not feel much like it either.</p>
<p>As I said in <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-my-experience-working-for-an-american-fintech-6-months">my previous post</a>, working on this project is allowing me to learn a lot and I want to continue learning a lot more in the upcoming months.</p>
<p>After <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-my-experience-at-domestika#heading-the-beginning-of-the-end">my experience at Domestika</a>, going through something like this again is really unpleasant. Luckily, for the time being, it is over.</p>
<p>Thank you for reading, and see you in the next one!</p>
<hr />
<div class="hn-embed-widget" id="buy-me-a-coffee"></div>]]></content:encoded></item><item><title><![CDATA[The one about my experience working for an American fintech]]></title><description><![CDATA[In a previous post, I talked about how things work in the American fintech I am currently working for as a contractor.
I initially planned to write this post after my first three months, but I postponed it when we learned that all teams in the Transa...]]></description><link>https://davidmontesdeoca.dev/the-one-about-my-experience-working-for-an-american-fintech-6-months</link><guid isPermaLink="true">https://davidmontesdeoca.dev/the-one-about-my-experience-working-for-an-american-fintech-6-months</guid><category><![CDATA[fintech]]></category><category><![CDATA[fintech software development]]></category><category><![CDATA[software development]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[agile]]></category><category><![CDATA[agile development]]></category><dc:creator><![CDATA[David Montesdeoca]]></dc:creator><pubDate>Wed, 26 Feb 2025 17:00:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1758564072564/140f54bf-40bc-419c-966c-e665288ab29e.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>In a <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-how-things-work-in-an-american-fintech">previous post</a>, I talked about how things work in the American fintech I am currently working for as a contractor.</p>
<p>I initially planned to write this post after my first three months, but I postponed it when we learned that all teams in the <em>Transactional</em> area would be reorganized at the start of the year.</p>
<p>This change took me by surprise. I knew that my team had been formed at the beginning of 2024 with members from other teams, but I did not realize this was something the company regularly does at the start of each year. The goal is to <strong>prevent knowledge from becoming siloed within specific teams</strong>.</p>
<p>This time, the teams were restructured based on the <strong>new architecture design</strong>, which was announced at the end of last year in an All Hands Engineering meeting. The goal is to complete it by 2027.</p>
<p>Given all this, I decided to wait until I had first impressions of the new team before writing about my experience. It has now been six months since I started working on this project and one month since the new team was formed.</p>
<p>Let me start from the beginning.</p>
<h2 id="heading-my-first-team">My first team</h2>
<p>All the details about how that team worked can be found in my <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-how-things-work-in-an-american-fintech#heading-my-team">previous post</a>.</p>
<p>Although it might sound like a cliché, I really felt like everyone on the team welcomed me with open arms.</p>
<p>For the first few weeks, I met every morning with my <strong>onboarding buddy</strong>, who explained how each application in the project works. We also spent part of the day <strong>pair programming</strong> on tasks she was working on.</p>
<p>Normally, I would spend the rest of the day completing tasks from the board that I had been assigned as part of the onboarding. I also spent several weeks <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-working-with-macos-being-a-linux-user">configuring the macOS laptop</a> they provided me.</p>
<p>During that time, <strong>I had no autonomy and often felt lost</strong>. I had no prior knowledge of the domain, and the project itself is quite complex, requiring many moving parts to come together. Still, my colleagues were incredibly patient and helped me as much as they could, especially when it came to testing tasks before moving them to "Ready for QA".</p>
<p>From the beginning, I noticed the team had been working together for several months because everything ran smoothly. Tasks arrived at refinements with enough detail, making it easy to decide what needed to be done.</p>
<p>I have to say that I integrated perfectly into their workflow. At first, I could barely participate in technical meetings, not due to a lack of technical knowledge, but because I lacked context and did not know the scope of certain changes. Over time, I gradually became more active in these meetings.</p>
<p>After applying several refactors and adding a couple of new features, I gained the trust of my teammates, who started to recognize how I could contribute and I quickly became a reference within the team when working with Ruby and applying best practices, as most of them had more experience with other programming languages. I always felt comfortable expressing my opinions and sharing ideas with them, even when we did not always agree. We would always reach a consensus for the good of the project. For example, I managed to <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-linting-in-a-legacy-ruby-project">add RuboCop</a> to all projects under our governance.</p>
<p>For the most part of my career, I have been working with Rails, but I never had the chance to work with Sinatra before. I am enjoying <strong>learning a simpler and lighter framework</strong>, although sometimes it lacks certain useful features, such as many methods available in the <a target="_blank" href="https://api.rubyonrails.org/classes/Time.html">Time class</a>.</p>
<p>That being said, I have often missed having a well-defined company-wide standard so that all teams could follow certain guidelines ensuring we work similarly. I understand this is hard to achieve, but I think it would be great to work on a project this large and always know where to find things, how to name them, what to test, and how to do it. In short, I <strong>miss consistency</strong>.</p>
<p>Regarding security, this project is unlike any othe project I have worked on before. For example, in AWS, we only have read permissions, and we do not have access to the database console in production, which makes perfect sense. Because of this, we <strong>rely heavily on observability</strong>. Any data changes in production are done through Rake tasks, which must be requested via a support ticket.</p>
<p>We must also be extra careful during what they call "peak times", such as Black Friday and Christmas, because breaking something in production could mean certain transactions do not reach their destination on time, potentially involving millions of euros, dollars, or any other currency.</p>
<p>In fact, a couple of months ago, I developed a feature that had to be deployed to production and executed right before Christmas. Of course, <strong>things did not go as planned</strong>. As a result of running thousands of background jobs, response times for one of the most critical tables in one of the most essential applications skyrocketed. I plan to talk about it in a future post.</p>
<p>Thankfully, the issue only affected that table, but I felt terrible about it. <strong>The engineering manager (EM) was very supportive</strong>, telling me that these things happen and that we must learn from them to prevent them to happen again in the future. His words were a huge relief.</p>
<p>As for the rest of the teammates, I have a very good relationship with the two developers I worked with the most, who also live in Madrid. Before Christmas we met in person when another teammate came to town for a conference and I also attended the company's Christmas dinner, where I met several colleagues from other teams.</p>
<h2 id="heading-my-new-team">My new team</h2>
<p>We started working together in mid-January.</p>
<p>The new team consists of an EM, a project manager (PM), a QA engineer, a tech lead and 5 software engineers, including myself.</p>
<p>I repeat in the team with the EM, the QA engineer and my onboarding buddy.</p>
<p>We follow the same agile ceremonies as before, but one major difference is that we <strong>do not have sprints</strong>. This is an unusual way of working for me, especially since we follow agile principles in almost every other aspect.</p>
<p>Our governance over the different applications in the project is different now, with a couple of exceptions, such as the payouts application that I have mentioned in other posts.</p>
<p>All team members currently live in Spain, so we usually have our daily meetings in the morning and scheduled meetings still happen in the afternoon, such as refinements.</p>
<p>Interestingly, almost half the team works as contractors through SNGULAR.</p>
<p>A role we did not have in my previous team is the tech lead. He has been at the company the longest and ensures everyone understands the overview and implications of new features to guarantee everything fits together correctly.</p>
<p>During the first few days, he organized several workshops to explain the so-called "transactional registry", which is a crucial component of the new architecture.</p>
<p>We also had several <strong>mob programming sessions</strong> where we started developing together the first functionality in that application.</p>
<p>I will not go into much detail about what that service does, but I should mention that its governance belongs to another team. As I mentioned in <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-how-things-work-in-an-american-fintech#heading-the-project">another post</a>, that ownership is not strict, as any developer can make changes to any application. Out of courtesy, the team with governance over each application is tagged.</p>
<p>Even though that other team shares with us the EM and the PM, and the tech lead often collaborates with them, we have encountered in a month several occasions where both teams have worked on similar functionality, because of a <strong>lack of proper communication</strong>.</p>
<p>For example, I recently worked on a feature that made certain attributes of an event optional, including them in the payload only if those attributes in the original event published in another application had led to a change in the corresponding object of our application. Normally, such change would involve changing the version of the event, but since no one was consuming the event yet, we decided in the refinement not to change it. However, by the time the feature was developed, reviewed, and tested by QA team, a couple of weeks had passed. When I deployed the changes to production, we suddenly started to see errors in a third application related to that event I had modified.</p>
<p>How was that possible if no one was consuming that event? In the time between our refinement and the deployment to production, another team had created a new event handler in that third application. Fortunately, it was only logging some information, so there was no real impact. This is an example of the lack of communication I mentioned earlier.</p>
<p>We clearly have <strong>room for improvement in terms of task planning</strong>. However, given that we are a newly formed team, it is understandable that these things happen.</p>
<p>That said, our timeline is tight. Ideally, we should <strong>complete several key components of the new architecture in Q1</strong>. The team was formed with that goal in mind.</p>
<p>The EM mentioned that it is likely the team will stay the same during Q2, but beyond that, no one knows what teams there will be or who will be part of each team.</p>
<p>For sure there is a good atmosphere in the team, but we have different points of view on key aspects of our work. This goes beyond just how we write and organize code.</p>
<p>Most of us prefer tasks to be as detailed as possible before refinement, while the tech lead prefers the opposite. I assume we will end up somewhere in between, especially since some of us lack context on the applications we are working with, making it difficult to move forward without enough details.</p>
<p>Our <strong>refinements are taking much longer than usual</strong>. We have scheduled a weekly 1-hour refinement, but we have often extended it for several hours or even continue the next day.</p>
<p>In some cases, a refinement has even resulted in <strong>re-refining some tasks already started</strong> because we totally changed the way we want to approach it. That situation is not sustainable over time and, of course, is an issue that we discussed in our first retrospective meeting.</p>
<p>Everybody in the team respect and follow the tech lead's vision, but right now it is creating a bottleneck, especially regarding how we want to implement certain details of the code. When in doubt, he is obviously the go-to guy.</p>
<p>In the same way we must find our <em>sweetspot</em>, being able to identify where we can be more or less demanding, thus generating some <strong>technical debt</strong> if necessary.</p>
<p>I have to admit that on a day-to-day basis <strong>we do not really feel pressure at all</strong>, but being a new team, it is noticeable that we all want to deliver and sometimes we have not paid enough attention to the whole software quality process and because of that <strong>we have had some unnecessary and relatively important bugs in production</strong>.</p>
<p>During a 1-to-1 meeting, the EM told me that if I ever feel any kind of pressure, I should let him know so we can handle it properly.</p>
<p>However I have noticed that even though we all discuss the progress in our tasks during the daily meeting, the EM sometimes asks me in private about the progress in certain tasks. In some cases, he even have moved a task to a different column on the board before I do.</p>
<p>Even though he tries not to rush us, I get the feeling that he is under pressure from his own manager to ensure we deliver the expected features on time.</p>
<p>On top of that, security restrictions cause additional delays in delivery. For example, to take a new application to production, we must first complete an initial use case and review it with the security team. Once they validate that first use case, the new application becomes available for deployment, and we can then proceed normally with new use cases.</p>
<p>Finally, as for me, I have to admit that <strong>my attention is somewhat divided</strong> because I am still working on a task related to the production incident I mentioned earlier. As I said before, I hope to talk about it soon.</p>
<hr />
<p>These first six months have flown by, and I have to say that not everything has been easy for me. However, the teams I have been part of have done their best to make me feel like one of them, even though I am a contractor.</p>
<p>This is by far <strong>the most complex project I have worked on</strong>. Many times, I have felt lost, but I never hesitated to ask for help, and the team was always there when I needed it.</p>
<p>It is a system where multiple applications interact, meaning <strong>a lot of things can go wrong</strong>. Moreover, this company operates in a completely different way than what I was used to in other projects, as production has far more security constraints than usual. And it makes sense, after all, we are constantly dealing with money, one way or another.</p>
<p>Just when I was starting to get into the rhythm of my first team, to better understand our processes and how our governance applications were connected, we had to split up.</p>
<p>I was really sad when I found out we would not be working together anymore. When I came back from the Christmas break, the team no longer existed. And unfortunately, it seems pretty likely that the same thing will happen again in the next few months.</p>
<p>On the other hand, I am happy because it is a <strong>technical challenge</strong> that is allowing me to learn a lot about the fintech world. That was one of my goals when I started looking for a new job last year.</p>
<p>Right now, it is a matter of being patient, as the beginning is always difficult for everyone. Hopefully, we will find our rhythm soon, and everything will start falling into place.</p>
<p>Thank you for reading, and see you in the next one!</p>
<hr />
<div class="hn-embed-widget" id="buy-me-a-coffee"></div>]]></content:encoded></item><item><title><![CDATA[The one about learning Ruby]]></title><description><![CDATA[I started working with Ruby in early 2010, on projects developed with Ruby on Rails. My mentor was influential enough to convince our bosses that this programming language and this framework were the best choice for future projects. We usually worked...]]></description><link>https://davidmontesdeoca.dev/the-one-about-learning-ruby</link><guid isPermaLink="true">https://davidmontesdeoca.dev/the-one-about-learning-ruby</guid><category><![CDATA[hanamirb]]></category><category><![CDATA[Ruby]]></category><category><![CDATA[Rails]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[rubyonrails]]></category><category><![CDATA[sinatrarb]]></category><category><![CDATA[training]]></category><category><![CDATA[learning]]></category><category><![CDATA[backend]]></category><dc:creator><![CDATA[David Montesdeoca]]></dc:creator><pubDate>Sat, 25 Jan 2025 12:08:35 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1737487504845/5aa24c3c-3b31-4dab-9b10-db7981d4b53c.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I started working with <a target="_blank" href="https://www.ruby-lang.org/en/">Ruby</a> in early 2010, on projects developed with <a target="_blank" href="https://rubyonrails.org/">Ruby on Rails</a>. My mentor was influential enough to convince our bosses that this programming language and this framework were the best choice for future projects. We usually worked with <a target="_blank" href="https://cakephp.org/">CakePHP</a>, so understanding how Rails works with the <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Glossary/MVC">MVC pattern</a> was really a piece of cake. Got it? Never mind...</p>
<p>15 years later, I work at <a target="_blank" href="https://www.sngular.com/">SNGULAR</a>, which is a consulting firm that mostly works with <a target="_blank" href="https://www.java.com/en/">Java</a> and its ecosystem in the backend of their software development projects. They also have projects with popular languages such as <a target="_blank" href="https://www.php.net/">PHP</a> or <a target="_blank" href="https://nodejs.org/en">Node.js</a>, among others. Focusing exclusively on what they call the <strong>Backend chapter</strong>, each of these technologies has an internal <strong>learning path</strong> defined. This is not the case with Ruby, since they do not have many projects where this is the main programming language.</p>
<p>When I started working at this company I was already assigned to a project, but as I already had booked my vacations, <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-my-new-job">my start at the American fintech</a> was delayed for a few weeks until I was back.</p>
<p>I had several weeks ahead of me before leaving, so in addition to completing my onboarding, my boss proposed to do a <strong>Ruby training</strong> to some colleagues who did not have a project assigned at the time. All of them were experienced developers who were used to work mainly with Java or PHP, but had no previous experience with Ruby. I accepted without hesitation.</p>
<p>You will find below the guide I shared with the company to learn Ruby, to which I have been adding some stuff that I consider interesting during the last months.</p>
<h2 id="heading-ruby-training">Ruby Training</h2>
<h3 id="heading-recommendations">Recommendations</h3>
<p>The most important recommendation is always <strong>learn Ruby first and then learn the framework of your choice if needed</strong>, usually the most popular. This is applicable to any ecosystem. That way, when you work on a gem, a script or anything else where you do not have available any of the libraries provided by the framework, you will be able to work with Ruby pretty easily.</p>
<p>Even if you already have experience with this language, I recommend you to access <a target="_blank" href="https://railshurts.com/frames/quiz/">this quiz</a> and you will probably understand what I mean.</p>
<p>Another recommendation that I consider important is that you look at the version of Ruby used in the articles and documentation that you find, because some things might be outdated. On the <a target="_blank" href="https://www.ruby-lang.org/en/">official website</a> you will always find news about the latest versions released.</p>
<h3 id="heading-documentation">Documentation</h3>
<ul>
<li><a target="_blank" href="https://docs.ruby-lang.org/en/">Ruby documentation</a></li>
<li><a target="_blank" href="https://rubyapi.org/">Ruby API</a></li>
<li><a target="_blank" href="https://www.ruby-lang.org/en/documentation/ruby-from-other-languages/">Ruby from other languages</a></li>
<li><a target="_blank" href="https://rubyreferences.github.io/rubyref/">The Ruby Reference</a></li>
<li><a target="_blank" href="https://learnxinyminutes.com/ruby/">Learn Ruby</a></li>
</ul>
<h3 id="heading-references-in-the-community">References in the community</h3>
<p>Not an exhaustive list and in no particular order:</p>
<ul>
<li><a target="_blank" href="https://en.wikipedia.org/wiki/Yukihiro_Matsumoto">Yukihiro Matsumoto "Matz"</a></li>
<li><a target="_blank" href="https://sandimetz.com/">Sandi Metz</a></li>
<li><a target="_blank" href="https://janko.io/">Janko Marohnić</a></li>
<li><a target="_blank" href="https://www.nateberkopec.com/">Nate Berkopec</a></li>
<li><a target="_blank" href="https://avdi.codes/">Avdi Grimm</a></li>
<li><a target="_blank" href="http://kytrinyx.com/">Katrina Owen</a></li>
<li><a target="_blank" href="https://tenderlovemaking.com/">Aaron Patterson</a></li>
<li><a target="_blank" href="https://www.mikeperham.com/">Mike Perham</a></li>
<li><a target="_blank" href="https://github.com/amatsuda">Akira Matsuda</a></li>
<li><a target="_blank" href="https://github.com/rafaelfranca">Rafael França</a></li>
<li><a target="_blank" href="https://dhh.dk/">DHH</a></li>
<li><a target="_blank" href="https://thoughtbot.com/blog">Thoughtbot</a></li>
<li><a target="_blank" href="https://evilmartians.com/chronicles">Evil Martians</a></li>
<li><a target="_blank" href="https://shopify.engineering/authors/shopify-engineering">Shopify Engineering</a></li>
<li><a target="_blank" href="https://github.blog/tag/ruby/">GitHub</a></li>
</ul>
<h3 id="heading-key-concepts">Key concepts</h3>
<p>Going beyond the most basic concepts present in every programming language we can find the following concepts:</p>
<ul>
<li><a target="_blank" href="https://en.wikipedia.org/wiki/Global_interpreter_lock">Global Interpreter Lock (GIL)</a>:<ul>
<li>Concurrency vs parallelism</li>
<li>Threads vs Fibers vs Ractors</li>
</ul>
</li>
<li><a target="_blank" href="https://shopify.engineering/ruby-yjit-is-production-ready">YJIT</a>:<ul>
<li>Ruby is an interpreted language</li>
<li>The JIT allows to dynamically compile Ruby code into machine code at runtime without interpretation</li>
<li>Improve performance and memory usage</li>
</ul>
</li>
<li>Types:<ul>
<li>Duck typing vs <code>is_a?</code>/<code>kind_of?</code> vs <code>instance_of?</code></li>
<li>There are <strong>no interfaces</strong></li>
<li>Type checkers:<ul>
<li>Not very extended in the Ruby community</li>
<li><a target="_blank" href="https://sorbet.org/">Sorbet</a></li>
<li><a target="_blank" href="https://github.com/ruby/rbs">RBS</a></li>
</ul>
</li>
</ul>
</li>
<li>Debugging:<ul>
<li><code>&lt;object&gt;.methods.sort</code>: what methods an object respond to.</li>
<li><code>&lt;object&gt;.method(:&lt;method_name&gt;).source_location</code>: where is defined a certain method.</li>
<li><code>&lt;object&gt;.method(:&lt;method_name&gt;).super_method.source_location</code>: where is defined certain method with the same name in one of the ancestors.</li>
</ul>
</li>
<li>Metaprogramming:<ul>
<li><code>define_method</code></li>
<li><a target="_blank" href="https://thoughtbot.com/blog/always-define-respond-to-missing-when-overriding">Always define respond_to_missing? when overriding method_missing</a></li>
<li><code>send</code> vs <code>public_send</code></li>
<li>Monkeypatching</li>
</ul>
</li>
<li><a target="_blank" href="https://www.exceptionalcreatures.com/guides/what-are-ruby-exceptions.html">Exceptions</a>:<ul>
<li><a target="_blank" href="https://www.exceptionalcreatures.com/guides/what-are-ruby-exceptions.html#the-class-hierarchy">The class hierarchy</a></li>
</ul>
</li>
<li><a target="_blank" href="https://docs.ruby-lang.org/en/3.4/syntax/pattern_matching_rdoc.html">Pattern matching</a></li>
<li><a target="_blank" href="https://dev.to/baweaver/ruby-3-1-shorthand-hash-syntax-first-impressions-19op">Shorthand hash syntax</a></li>
<li><a target="_blank" href="https://thoughtbot.com/blog/ruby-safe-navigation">Safe navigation operator (&amp;)</a></li>
<li>Strings (<code>to_s</code>) vs symbols (<code>to_sym</code>):<ul>
<li><a target="_blank" href="https://ruby-doc.org/3.3.3/syntax/comments_rdoc.html#label-frozen_string_literal+Directive">Frozen strings</a></li>
</ul>
</li>
<li>Classes vs modules</li>
<li>Lambdas vs procs vs blocks</li>
<li>Memoization:<ul>
<li><a target="_blank" href="https://dev.to/codeandclay/a-memoization-gotcha-14c6">Beware booleans</a></li>
</ul>
</li>
<li>Logical operators:<ul>
<li><code>and</code> vs <code>&amp;&amp;</code></li>
<li><code>or</code> vs <code>||</code></li>
<li><code>not</code> vs <code>!</code></li>
</ul>
</li>
<li>Namespaces</li>
<li>Inheritance</li>
<li>Constants lookup</li>
<li><code>super</code></li>
<li><code>self</code></li>
<li>Attribute macros:<ul>
<li><code>attr_reader</code> vs <code>attr_writer</code> vs <code>attr_accessor</code></li>
</ul>
</li>
<li>Variables:<ul>
<li>local vs global vs instance variables vs class instance variables vs class variables</li>
</ul>
</li>
<li>Methods:<ul>
<li>Visibility: <strong>public</strong> vs <code>protected</code> vs <code>private</code></li>
<li><a target="_blank" href="https://allaboutcoding.ghinda.com/endless-method-a-quick-intro">Endless</a></li>
</ul>
</li>
<li>Ranges:<ul>
<li><a target="_blank" href="https://docs.ruby-lang.org/en/3.4/Range.html#class-Range-label-Beginless+Ranges">Beginless</a> and <a target="_blank" href="https://docs.ruby-lang.org/en/3.4/Range.html#class-Range-label-Endless+Ranges">endless</a></li>
</ul>
</li>
<li>Regular expressions:<ul>
<li><a target="_blank" href="https://rubular.com/">Rubular Playground</a></li>
</ul>
</li>
<li>Documentation:<ul>
<li><a target="_blank" href="https://rubydoc.info/gems/yard/file/docs/GettingStarted.md">YARD</a></li>
</ul>
</li>
</ul>
<h3 id="heading-frameworks">Frameworks</h3>
<ul>
<li><a target="_blank" href="https://rubyonrails.org/">Ruby on Rails</a>:<ul>
<li><a target="_blank" href="https://rubyonrails.org/docs">Documentation</a></li>
<li><a target="_blank" href="https://guides.rubyonrails.org/">Guides</a></li>
<li><a target="_blank" href="https://rubyonrails.org/doctrine">The Rails Doctrine</a></li>
</ul>
</li>
<li><a target="_blank" href="https://hanamirb.org/">Hanami</a>:<ul>
<li><a target="_blank" href="https://docs.hanamirb.org">Documentation</a></li>
<li><a target="_blank" href="https://guides.hanamirb.org/">Guides</a></li>
</ul>
</li>
<li><a target="_blank" href="https://sinatrarb.com/">Sinatra</a>:<ul>
<li><a target="_blank" href="https://sinatrarb.com/documentation.html">Documentation</a></li>
</ul>
</li>
<li><a target="_blank" href="https://padrinorb.com/">Padrino</a>:<ul>
<li><a target="_blank" href="https://www.rubydoc.info/github/padrino/padrino-framework">Documentation</a></li>
<li><a target="_blank" href="https://padrinorb.com/guides/">Guides</a></li>
</ul>
</li>
<li><a target="_blank" href="https://brutrb.com/">BrutRB</a>:<ul>
<li><a target="_blank" href="https://brutrb.com/getting-started.html">Documentation</a></li>
</ul>
</li>
</ul>
<h3 id="heading-testing">Testing</h3>
<ul>
<li>Frameworks:<ul>
<li><a target="_blank" href="https://github.com/minitest/minitest">Minitest</a> (default):<ul>
<li><a target="_blank" href="https://docs.seattlerb.org/minitest/">Documentation</a></li>
<li><a target="_blank" href="https://github.com/thoughtbot/rails-training-testing-exercise/">Learn how to write tests with Minitest</a></li>
</ul>
</li>
<li><a target="_blank" href="https://rspec.info/">RSpec</a>:<ul>
<li><a target="_blank" href="https://rspec.info/documentation/">Documentation</a></li>
<li><a target="_blank" href="https://rspec.info/blog/">Blog</a></li>
</ul>
</li>
<li><a target="_blank" href="https://cucumber.io/">Cucumber</a>:<ul>
<li><a target="_blank" href="https://cucumber.io/docs/">Documentation</a></li>
<li><a target="_blank" href="https://cucumber.io/learn/">Learn BDD and Cucumber</a></li>
<li><a target="_blank" href="https://cucumber.io/blog/">Blog</a></li>
</ul>
</li>
</ul>
</li>
<li>Articles:<ul>
<li><a target="_blank" href="https://thoughtbot.com/blog/how-to-train-your-senior-developers-in-ruby-on-rails#testing-ruby-for-beginners">Testing Ruby for beginners</a></li>
<li><a target="_blank" href="https://thoughtbot.com/blog/the-case-for-wet-tests">The Case for WET Tests</a></li>
<li><a target="_blank" href="https://thoughtbot.com/blog/mystery-guest">Mystery Guest</a></li>
<li><a target="_blank" href="https://thoughtbot.com/blog/functional-viewpoints-on-testing-objectoriented-code">Testing Objects with a Functional Mindset</a></li>
<li><a target="_blank" href="https://thoughtbot.com/blog/four-phase-test">Four-Phase Test</a></li>
<li><a target="_blank" href="https://thoughtbot.com/blog/write-reliable-asynchronous-integration-tests-with-capybara">Write Reliable, Asynchronous Integration Tests With Capybara</a></li>
<li><a target="_blank" href="https://thoughtbot.com/blog/tags/testing">Thoughtbot</a> (lots of articles)</li>
</ul>
</li>
</ul>
<h3 id="heading-interpreters">Interpreters</h3>
<ul>
<li><a target="_blank" href="https://github.com/ruby/ruby">Matz's Ruby Interpreter a.k.a. Ruby MRI a.k.a. CRuby</a></li>
<li><a target="_blank" href="https://github.com/jruby/jruby">JRuby</a></li>
<li><a target="_blank" href="https://github.com/codicoscepticos/ruby-implementations">Others</a></li>
</ul>
<h3 id="heading-installation">Installation</h3>
<ul>
<li>In a real project, whenever possible, it is recommended to use Docker containers.</li>
<li>Version managers:<ul>
<li><a target="_blank" href="https://rvm.io/rvm/install#any-other-system">RVM</a><ul>
<li><code>.ruby-version</code> (include in the version control)</li>
<li><code>.ruby-gemset</code> (do not include in the version control: Add to <code>.gitignore</code>)</li>
</ul>
</li>
<li><a target="_blank" href="https://asdf-vm.com/guide/getting-started.html#_3-install-asdf">asdf</a></li>
<li><a target="_blank" href="https://rbenv.org/">rbenv</a></li>
<li><a target="_blank" href="https://github.com/postmodern/chruby">chruby</a></li>
</ul>
</li>
<li><a target="_blank" href="https://gorails.com/setup/">GoRails setup</a></li>
</ul>
<h3 id="heading-editors-and-ides">Editors and IDEs</h3>
<p>You will find plugins for Ruby in any editor/IDE.</p>
<ul>
<li><a target="_blank" href="https://www.jetbrains.com/ruby/">RubyMine</a>: the best integration</li>
<li><a target="_blank" href="https://code.visualstudio.com/docs/languages/ruby">VSCode</a><ul>
<li>Add a language server protocol (LSP):<ul>
<li><a target="_blank" href="https://shopify.github.io/ruby-lsp/">Ruby LSP</a></li>
<li><a target="_blank" href="https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-lsp">Extension</a></li>
</ul>
</li>
</ul>
</li>
</ul>
<h3 id="heading-linters">Linters</h3>
<ul>
<li><a target="_blank" href="https://rubocop.org/">RuboCop</a><ul>
<li><a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-linting-in-a-legacy-ruby-project">How to start using a linter in a legacy project</a></li>
</ul>
</li>
<li><a target="_blank" href="https://github.com/standardrb/standard">Standard</a></li>
<li><a target="_blank" href="https://brakemanscanner.org/">Brakeman</a></li>
<li><a target="_blank" href="https://github.com/troessner/reek">Reek</a></li>
</ul>
<h3 id="heading-style-guides">Style guides</h3>
<ul>
<li><a target="_blank" href="https://github.com/rubocop/ruby-style-guide">Ruby</a></li>
<li><a target="_blank" href="https://github.com/rubocop/rails-style-guide">Rails</a></li>
<li><a target="_blank" href="https://github.com/rubocop/rspec-style-guide">RSpec</a></li>
<li><a target="_blank" href="https://ruby.style/">Ruby in Style</a></li>
</ul>
<h3 id="heading-playgrounds">Playgrounds</h3>
<ul>
<li><a target="_blank" href="https://try.ruby-lang.org/">Try Ruby</a></li>
<li><a target="_blank" href="https://ruby.github.io/play-ruby/">Ruby Playground</a></li>
<li><a target="_blank" href="https://ruby-next.github.io/">Ruby Next Playground</a></li>
<li><a target="_blank" href="https://runruby.dev/">RunRuby.dev</a></li>
</ul>
<h3 id="heading-courses">Courses</h3>
<p>The company provide us with access to the courses catalog available on Udemy.</p>
<p>Some interesting courses to start learning Ruby and Rails:</p>
<ul>
<li><a target="_blank" href="https://www.udemy.com/course/learn-to-code-with-ruby-lang/">Learn to Code with Ruby</a></li>
<li><a target="_blank" href="https://www.udemy.com/course/testing-ruby-with-rspec/">Testing Ruby with RSpec</a></li>
<li><a target="_blank" href="https://www.udemy.com/course/the-complete-ruby-on-rails-developer-course/">The Complete Ruby on Rails Developer Course</a></li>
<li><a target="_blank" href="https://www.udemy.com/course/building-instagram-from-scratch-using-ruby-on-rails-7/">How To Build Instagram Clone Using Ruby on Rails 7</a></li>
</ul>
<p>Not yet available for us, but hopefully will be soon:</p>
<ul>
<li><a target="_blank" href="https://www.udemy.com/course/ruby-on-types-write-robust-software-with-rbs">Ruby on Types: Static Typing with Ruby and Ruby on Rails</a></li>
</ul>
<p>Other courses:</p>
<ul>
<li><a target="_blank" href="https://www.rubycademy.com/">RubyCademy</a></li>
<li><a target="_blank" href="https://www.theodinproject.com/paths/full-stack-ruby-on-rails">The Odin Project: Full Stack Ruby on Rails</a>:<ul>
<li><a target="_blank" href="https://www.theodinproject.com/paths/full-stack-ruby-on-rails/courses/ruby">Ruby</a></li>
<li><a target="_blank" href="https://www.theodinproject.com/paths/full-stack-ruby-on-rails/courses/ruby-on-rails">Rails</a></li>
</ul>
</li>
</ul>
<h3 id="heading-videos">Videos</h3>
<ul>
<li><a target="_blank" href="https://www.rubyvideo.dev/">Ruby conferences</a></li>
<li><a target="_blank" href="https://thoughtbot.com/upcase/rails">Upcase</a></li>
<li><a target="_blank" href="https://www.driftingruby.com/">Drifting Ruby</a></li>
<li><a target="_blank" href="https://gorails.com/episodes">GoRails</a></li>
<li><a target="_blank" href="http://railscasts.com/">Railscasts</a> (outdated)</li>
</ul>
<h3 id="heading-books">Books</h3>
<ul>
<li><a target="_blank" href="https://pragprog.com/search/?q=ruby">The Pragmatic Bookshelf</a>: lots of books could be outdated<ul>
<li><a target="_blank" href="https://pragprog.com/titles/ruby5/programming-ruby-3-3-5th-edition/">Programming Ruby 3.3</a></li>
<li><a target="_blank" href="https://pragprog.com/titles/rails7/agile-web-development-with-rails-7/">Agile Web Development with Rails 7</a></li>
</ul>
</li>
<li><a target="_blank" href="https://www.amazon.com/gp/product/0134456475">Practical Object-Oriented Design: An Agile Primer Using Ruby (POODR)</a></li>
<li><a target="_blank" href="https://sandimetz.com/99bottles">99 Bottles of OOP</a></li>
<li><a target="_blank" href="https://books.thoughtbot.com/books/ruby-science.html">Ruby Science</a></li>
<li><a target="_blank" href="https://painlessrails.com/">Painless Rails</a></li>
<li><a target="_blank" href="https://www.railsspeed.com/">The Complete Guide to Rails Performance</a></li>
<li><a target="_blank" href="https://books.thoughtbot.com/books/testing-rails.html">Testing Rails</a></li>
</ul>
<h3 id="heading-newsletters">Newsletters</h3>
<ul>
<li><a target="_blank" href="https://rubyweekly.com/">Ruby Weekly</a></li>
<li><a target="_blank" href="https://newsletter.shortruby.com/">Short Ruby</a></li>
<li><a target="_blank" href="https://rubycentral.org/news/">Ruby Central</a></li>
</ul>
<h3 id="heading-exercises-and-katas">Exercises and katas</h3>
<ul>
<li><a target="_blank" href="https://exercism.org/tracks/ruby">Exercism</a></li>
<li><a target="_blank" href="https://www.codewars.com/kata">Codewars</a></li>
<li><a target="_blank" href="https://leetcode.com/problemset/">Leetcode</a></li>
</ul>
<h3 id="heading-interesting-websites-and-articles">Interesting websites and articles</h3>
<ul>
<li><a target="_blank" href="https://phptoruby.io/">PHP to Ruby</a></li>
<li><a target="_blank" href="https://www.rubyguides.com/ruby-post-index/">RubyGuides</a></li>
<li><a target="_blank" href="https://refactoring.guru/design-patterns">Design Patterns</a>: every pattern includes examples in Ruby</li>
<li><a target="_blank" href="https://gorails.com/guides">GoRails guides</a></li>
<li><a target="_blank" href="https://railshurts.com/">Rails hurts</a></li>
<li><a target="_blank" href="https://github.com/backpackerhh/upgrow-docs">Upgrow: A Better Architecture</a>:<ul>
<li>Proposed by Shopify and somehow inspired by <a target="_blank" href="https://davidmontesdeoca.dev/the-one-with-highlights-of-the-red-book-of-ddd">DDD</a></li>
<li>Bear in mind that this is not a usual approach in a Rails project</li>
</ul>
</li>
<li><a target="_blank" href="https://thoughtbot.com/blog/how-to-train-your-senior-developers-in-ruby-on-rails">How to train senior developers in Ruby on Rails</a></li>
<li><a target="_blank" href="https://about.gitlab.com/blog/2022/07/06/why-were-sticking-with-ruby-on-rails/">GitLab: Why we're sticking with Ruby on Rails</a></li>
<li><a target="_blank" href="https://thoughtbot.com/blog/writing-less-error-prone-code">Writing Less Error-Prone Code</a></li>
</ul>
<h3 id="heading-useful-and-popular-gems">Useful and popular gems</h3>
<ul>
<li><a target="_blank" href="https://rubygems.org/">RubyGems</a>: Find, install, and publish Ruby gems</li>
<li><a target="_blank" href="https://ruby.libhunt.com/">Awesome Ruby</a>: A collection of awesome Ruby gems, tools, frameworks and software</li>
<li><a target="_blank" href="https://www.ruby-toolbox.com/">The Ruby Toolbox</a>: Find actively maintained &amp; popular open source software libraries for the Ruby programming language</li>
</ul>
<p>Some gems are available for any Ruby project and others are only available for Rails:</p>
<ul>
<li>Testing:<ul>
<li><a target="_blank" href="https://github.com/rspec/rspec">RSpec</a></li>
<li><a target="_blank" href="https://github.com/minitest/minitest">Minitest</a></li>
<li><a target="_blank" href="https://github.com/cucumber/cucumber-ruby">Cucumber</a></li>
<li><a target="_blank" href="https://github.com/thoughtbot/factory_bot">FactoryBot</a></li>
<li><a target="_blank" href="https://github.com/teamcapybara/capybara">Capybara</a></li>
<li><a target="_blank" href="https://github.com/vcr/vcr">VCR</a></li>
<li><a target="_blank" href="https://github.com/rswag/rswag">RSwag</a>/<a target="_blank" href="https://github.com/raceful-potato/rspec-swag">rspec-swag</a></li>
<li><a target="_blank" href="https://github.com/bblimke/webmock">Webmock</a></li>
<li><a target="_blank" href="https://github.com/faker-ruby/faker">Faker</a></li>
<li><a target="_blank" href="https://github.com/DatabaseCleaner/database_cleaner">database-cleaner</a></li>
</ul>
</li>
<li>Next generation:<ul>
<li><a target="_blank" href="https://github.com/dry-rb">dry</a> (dependency injection, types, schemas, validations, monads, ...)</li>
</ul>
</li>
<li>Web servers:<ul>
<li><a target="_blank" href="https://github.com/rack/rack">Rack</a> (Interface)</li>
</ul>
</li>
<li>Application servers:<ul>
<li><a target="_blank" href="https://github.com/puma/puma">puma</a></li>
<li><a target="_blank" href="https://github.com/phusion/passenger">passenger</a></li>
</ul>
</li>
<li>CORS:<ul>
<li><a target="_blank" href="https://github.com/cyu/rack-cors">rack-cors</a></li>
</ul>
</li>
<li>Databases:<ul>
<li><a target="_blank" href="https://github.com/ged/ruby-pg">PostgreSQL</a></li>
<li><a target="_blank" href="https://github.com/brianmario/mysql2">MySQL</a></li>
<li><a target="_blank" href="https://github.com/mongodb/mongo-ruby-driver">MongoDB</a></li>
<li><a target="_blank" href="https://github.com/redis/redis-rb">Redis</a></li>
</ul>
</li>
<li>Migrations:<ul>
<li><a target="_blank" href="https://github.com/ankane/strong_migrations">Strong migrations</a></li>
</ul>
</li>
<li>Bulk imports:<ul>
<li><a target="_blank" href="https://github.com/zdennis/activerecord-import">ActiveRecord Import</a></li>
</ul>
</li>
<li>Cloud:<ul>
<li><a target="_blank" href="https://github.com/aws/aws-sdk-ruby">AWS</a></li>
<li><a target="_blank" href="https://github.com/googleapis/google-cloud-ruby">GCP</a></li>
<li><a target="_blank" href="https://github.com/Azure/azure-storage-ruby">Azure</a> (no longer maintained)</li>
</ul>
</li>
<li>Background jobs:<ul>
<li><a target="_blank" href="https://github.com/sidekiq/sidekiq">Sidekiq</a></li>
<li><a target="_blank" href="https://github.com/resque/resque">Resque</a></li>
<li><a target="_blank" href="https://github.com/collectiveidea/delayed_job">Delayed Jobs</a></li>
</ul>
</li>
<li>Events:<ul>
<li><a target="_blank" href="https://github.com/ruby-amqp/bunny">Bunny</a> (RabbitMQ)</li>
<li><a target="_blank" href="https://github.com/ruby-amqp/kicks">Kicks</a> (RabbitMQ)</li>
<li><a target="_blank" href="https://github.com/karafka/karafka">Karafka</a>: (Kafka)</li>
</ul>
</li>
<li>Cron:<ul>
<li><a target="_blank" href="https://github.com/javan/whenever">whenever</a></li>
<li><a target="_blank" href="https://github.com/jmettraux/rufus-scheduler">rufus-scheduler</a></li>
<li><a target="_blank" href="https://github.com/sidekiq-cron/sidekiq-cron">Sidekiq Cron</a></li>
<li><a target="_blank" href="https://github.com/sidekiq-scheduler/sidekiq-scheduler">sidekiq-scheduler</a></li>
<li><a target="_blank" href="https://github.com/resque/resque-scheduler">resque-scheduler</a></li>
</ul>
</li>
<li>WebSocket:<ul>
<li><a target="_blank" href="https://github.com/anycable/anycable">AnyCable</a></li>
</ul>
</li>
<li>Uploads:<ul>
<li><a target="_blank" href="https://github.com/shrinerb/shrine">Shrine</a></li>
<li><a target="_blank" href="https://github.com/carrierwaveuploader/carrierwave">Carrierwave</a></li>
</ul>
</li>
<li>Caching:<ul>
<li><a target="_blank" href="https://github.com/petergoldstein/dalli">Dalli</a> (Memcached)</li>
</ul>
</li>
<li>Full text search:<ul>
<li><a target="_blank" href="https://github.com/elastic/elasticsearch-ruby">Elasticsearch</a></li>
<li><a target="_blank" href="https://github.com/opensearch-project/opensearch-ruby">OpenSearch</a></li>
<li><a target="_blank" href="https://github.com/algolia/algoliasearch-client-ruby">Algolia</a></li>
</ul>
</li>
<li>HTTP client:<ul>
<li><a target="_blank" href="https://github.com/lostisland/faraday">Faraday</a></li>
<li><a target="_blank" href="https://github.com/jnunemaker/httparty">httparty</a></li>
</ul>
</li>
<li>APIs:<ul>
<li><a target="_blank" href="https://github.com/rmosolgo/graphql-ruby">GraphQL</a></li>
<li><a target="_blank" href="https://github.com/jsonapi-serializer/jsonapi-serializer">JSON:API serializer</a></li>
<li><a target="_blank" href="https://github.com/rails-api/active_model_serializers">ActiveModel serializers</a></li>
<li><a target="_blank" href="https://github.com/jwt/ruby-jwt">JWT</a></li>
</ul>
</li>
<li>Deployment:<ul>
<li><a target="_blank" href="https://github.com/capistrano/capistrano">Capistrano</a></li>
</ul>
</li>
<li>Geocoding:<ul>
<li><a target="_blank" href="https://github.com/alexreisner/geocoder">Geocoder</a></li>
</ul>
</li>
<li>Feature flags:<ul>
<li><a target="_blank" href="https://github.com/flippercloud/flipper">Flipper</a></li>
</ul>
</li>
<li>Authentication:<ul>
<li><a target="_blank" href="https://github.com/heartcombo/devise">Devise</a></li>
<li><a target="_blank" href="https://github.com/doorkeeper-gem/doorkeeper">Doorkeeper</a></li>
<li><a target="_blank" href="https://github.com/omniauth/omniauth">Omniauth</a>: Standardized Multi-Provider Authentication</li>
<li><a target="_blank" href="https://github.com/thoughtbot/clearance">Clearance</a></li>
</ul>
</li>
<li>Authorization:<ul>
<li><a target="_blank" href="https://github.com/varvet/pundit">Pundit</a></li>
<li><a target="_blank" href="https://github.com/cancancommunity/cancancan">CanCanCan</a></li>
<li><a target="_blank" href="https://github.com/palkan/action_policy">ActionPolicy</a></li>
</ul>
</li>
<li>Mobile:<ul>
<li>Hotwire Native: <a target="_blank" href="https://github.com/hotwired/hotwire-native-android">Android</a>/<a target="_blank" href="https://github.com/hotwired/hotwire-native-ios">iOS</a></li>
</ul>
</li>
<li>Templates:<ul>
<li><a target="_blank" href="https://github.com/ruby/erb">ERB</a></li>
<li><a target="_blank" href="https://github.com/haml/haml">Haml</a></li>
<li><a target="_blank" href="https://github.com/slim-template/slim">Slim</a></li>
</ul>
</li>
<li>Assets:<ul>
<li><a target="_blank" href="https://github.com/sass-contrib/sass-embedded-host-ruby">Sass</a></li>
<li><a target="_blank" href="https://github.com/rails/tailwindcss-rails">Tailwind</a></li>
<li><a target="_blank" href="https://github.com/twbs/bootstrap-rubygem">Bootstrap</a></li>
<li><a target="_blank" href="https://github.com/reactjs/react-rails">React</a></li>
<li><a target="_blank" href="https://github.com/bkuhlmann/htmx">htmx</a></li>
</ul>
</li>
<li>Interactivity:<ul>
<li><a target="_blank" href="https://github.com/hotwired/stimulus-rails">Stimulus</a></li>
<li><a target="_blank" href="https://github.com/hotwired/turbo-rails">Turbo</a></li>
</ul>
</li>
<li>Views:<ul>
<li><a target="_blank" href="https://viewcomponent.org/">View Component</a></li>
</ul>
</li>
<li>Forms:<ul>
<li><a target="_blank" href="https://github.com/heartcombo/simple_form">Simple Form</a></li>
</ul>
</li>
<li>Pagination:<ul>
<li><a target="_blank" href="https://github.com/kaminari/kaminari">Kaminari</a></li>
<li><a target="_blank" href="https://github.com/mislav/will_paginate">will_paginate</a></li>
<li><a target="_blank" href="https://github.com/ddnexus/pagy">pagy</a></li>
</ul>
</li>
<li>State machines:<ul>
<li><a target="_blank" href="https://github.com/state-machines/state_machines">State Machines</a></li>
<li><a target="_blank" href="https://github.com/aasm/aasm">AASM</a></li>
</ul>
</li>
<li>Videos:<ul>
<li><a target="_blank" href="https://github.com/sethdeckard/m3u8">m3u8</a></li>
</ul>
</li>
<li>Money:<ul>
<li><a target="_blank" href="https://github.com/RubyMoney/money">money</a></li>
</ul>
</li>
<li>Audit changes:<ul>
<li><a target="_blank" href="https://github.com/collectiveidea/audited">audited</a></li>
</ul>
</li>
<li>Dependency updates:<ul>
<li><a target="_blank" href="https://github.com/renovatebot/renovate">Renovate</a></li>
</ul>
</li>
</ul>
<h3 id="heading-community">Community</h3>
<p>Last but not least, get involved in the community the way you prefer. Follow people in social networks, read as much as you can about the language, its frameworks and tools, and even collaborate in open source projects if you feel like it.</p>
<p>In your city probably there is a Ruby group that gathers from time to time, usually monthly, to talk about the language and related stuff. For instance, in Madrid, where I am currently living, we have <a target="_blank" href="https://www.madridrb.com/">Madrid.rb</a>.</p>
<p>Recently, I found <a target="_blank" href="https://rubyfriends.app/">RubyFriends</a>, an app that allows people to reconnect after a conference. A great idea!</p>
<hr />
<p>That training for me was an enriching experience that took about 4 weeks. My colleagues approached the training as a challenge and at all times I felt that they were really interested in learning the language and its ecosystem, and that they wanted to put into practice what they were learning.</p>
<p>My boss told me that they were very happy with the training and I can confirm the guide I created will be used for future trainings at SNGULAR.</p>
<p>I would also like to take this opportunity to thank my mentor, <a target="_blank" href="https://www.linkedin.com/in/eddyjosafat">Eddy Josafat</a>, for coming up with the brilliant idea of using this language in which he saw a promising future. He was not wrong.</p>
<p>By the way, this is a list that I will keep updating over time. Is there anything you miss in this list? If so, leave it in the comments, please.</p>
<p>Thank you for reading and see you in the next one!</p>
<hr />
<div class="hn-embed-widget" id="buy-me-a-coffee"></div>]]></content:encoded></item><item><title><![CDATA[The one about using factories with entities in Sinatra]]></title><description><![CDATA[I mentioned in a previous post that in the project I am currently working on we do not use Rails in most of the applications. There are some exceptions in certain legacy applications that represent the core of the project, from which we have been pro...]]></description><link>https://davidmontesdeoca.dev/the-one-about-using-factories-with-entities-in-sinatra</link><guid isPermaLink="true">https://davidmontesdeoca.dev/the-one-about-using-factories-with-entities-in-sinatra</guid><category><![CDATA[sinatrarb]]></category><category><![CDATA[Ruby]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[sinatra]]></category><category><![CDATA[FactoryBot]]></category><category><![CDATA[Testing]]></category><category><![CDATA[#rspec]]></category><dc:creator><![CDATA[David Montesdeoca]]></dc:creator><pubDate>Mon, 23 Dec 2024 09:53:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1734812714751/a2e4b95b-09b5-4e3e-b915-9a523cc9b668.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I mentioned in a <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-how-things-work-in-an-american-fintech">previous post</a> that in the project I am currently working on we do not use <a target="_blank" href="https://rubyonrails.org/">Rails</a> in most of the applications. There are some exceptions in certain legacy applications that represent the core of the project, from which we have been progressively extracting functionalities and creating new microservices with <a target="_blank" href="https://sinatrarb.com/">Sinatra</a>.</p>
<p>In the case of my team, part of our focus so far has been on the new payouts application that I <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-linting-in-a-legacy-ruby-project">also mentioned in another post</a>, which allows us to automate certain payments.</p>
<p>In that application, <a target="_blank" href="https://sequel.jeremyevans.net/">Sequel</a> provides the ORM layer for mapping database records to Ruby objects. We do not use <a target="_blank" href="https://guides.rubyonrails.org/active_record_basics.html">models like in Rails</a> inheriting from <code>ActiveRecord</code>, but entities that inherit from the <a target="_blank" href="https://docs.ruby-lang.org/en/3.2/Data.html">Data class</a>, added to the Ruby core in version 3.2.</p>
<pre><code class="lang-ruby">BankAccount &lt; Data.define(<span class="hljs-symbol">:account_code</span>, <span class="hljs-symbol">:bank</span>, <span class="hljs-symbol">:currency_code</span>, <span class="hljs-symbol">:country</span>)
</code></pre>
<p>When I joined the team, we used factories in tests with <a target="_blank" href="https://github.com/thoughtbot/factory_bot">FactoryBot</a>:</p>
<pre><code class="lang-ruby">FactoryBot.define <span class="hljs-keyword">do</span>
  factory <span class="hljs-symbol">:bank_account</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: <span class="hljs-title">BankAccount</span> <span class="hljs-title">do</span></span>
    account_code { <span class="hljs-string">"CITI-00000001-USD"</span> }
    bank { <span class="hljs-string">"citibank"</span> }
    currency_code { <span class="hljs-string">"USD"</span> }
    country { <span class="hljs-string">"US"</span> }
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>However, those factories were only used to generate instances of the entities:</p>
<pre><code class="lang-ruby">bank_account = FactoryBot.build(<span class="hljs-symbol">:bank_account</span>, <span class="hljs-symbol">account_code:</span> <span class="hljs-string">"CITI-00000001-EUR"</span>, <span class="hljs-symbol">currency_code:</span> <span class="hljs-string">"EUR"</span>, <span class="hljs-symbol">country:</span> <span class="hljs-string">"ES"</span>)
</code></pre>
<p>I was quite surprised that those factories were not used to create the necessary records in the database for every single test example.</p>
<p>The records were defined with the required values in a module:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">Test</span></span>
  <span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">Helpers</span></span>
    <span class="hljs-class"><span class="hljs-keyword">module</span> <span class="hljs-title">BankAccountStorage</span></span>
      <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">seed_bank_account_storage</span></span>
        Database.connection <span class="hljs-keyword">do</span> <span class="hljs-params">|db|</span>
          db[<span class="hljs-symbol">:bank_accounts</span>].insert(<span class="hljs-symbol">account_code:</span> <span class="hljs-string">"CITI-123456789-USD"</span>, <span class="hljs-symbol">bank:</span> <span class="hljs-string">"citibank"</span>, <span class="hljs-symbol">currency_code:</span> <span class="hljs-string">"USD"</span>, <span class="hljs-symbol">country:</span> <span class="hljs-string">"US"</span>)
          db[<span class="hljs-symbol">:bank_accounts</span>].insert(<span class="hljs-symbol">account_code:</span> <span class="hljs-string">"CITI-123456789-EUR"</span>, <span class="hljs-symbol">bank:</span> <span class="hljs-string">"citibank"</span>, <span class="hljs-symbol">currency_code:</span> <span class="hljs-string">"EUR"</span>, <span class="hljs-symbol">country:</span> <span class="hljs-string">"ES"</span>)
          db[<span class="hljs-symbol">:bank_accounts</span>].insert(<span class="hljs-symbol">account_code:</span> <span class="hljs-string">"CITI-123456789-GBP"</span>, <span class="hljs-symbol">bank:</span> <span class="hljs-string">"citibank"</span>, <span class="hljs-symbol">currency_code:</span> <span class="hljs-string">"GBP"</span>, <span class="hljs-symbol">country:</span> <span class="hljs-string">"GB"</span>)
          <span class="hljs-comment"># ...more records...</span>
        <span class="hljs-keyword">end</span>
      <span class="hljs-keyword">end</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<blockquote>
<p><code>Database.connection</code> is just a custom wrapper around a Sequel connection.</p>
</blockquote>
<p>The module was then included in the corresponding test file:</p>
<pre><code class="lang-ruby"><span class="hljs-keyword">require</span> <span class="hljs-string">"spec_helper"</span>
require_relative <span class="hljs-string">"../test/helpers/bank_account_storage"</span>

describe <span class="hljs-string">"POST /payments/initiate"</span> <span class="hljs-keyword">do</span>
  <span class="hljs-keyword">include</span> Test::Helpers::BankAccountStorage

  before <span class="hljs-keyword">do</span>
    seed_bank_account_storage
  <span class="hljs-keyword">end</span>

  context <span class="hljs-string">"when paying in USD"</span> <span class="hljs-keyword">do</span>
  <span class="hljs-comment"># ...</span>
</code></pre>
<p>The way I see it, doing that is not maintainable and would not scale well, for several reasons:</p>
<ul>
<li><p>We are generating more records than needed for each example. Therefore, each test is slower than necessary.</p>
</li>
<li><p>We do not have control over the attributes of each of the records created.</p>
</li>
<li><p>We are adding indirection within each test file where that module is used, because we are asserting over values that are not defined neither in the test example nor in the test file itself.</p>
</li>
</ul>
<p>I would even prefer to use <strong>fixtures</strong> rather than keep creating records that way, but only if I knew that the code would never change. And we all know that rarely happens.</p>
<p>When it was my turn to refactor a part of that application, I decided it was time to change that approach as a previous step to introducing new changes.</p>
<p>Of course, the first step was to delete those modules and start persisting records in the database with FactoryBot:</p>
<pre><code class="lang-ruby">bank_account = FactoryBot.create(<span class="hljs-symbol">account_code:</span> <span class="hljs-string">"CITI-123456789-USD"</span>, <span class="hljs-symbol">bank:</span> <span class="hljs-string">"citibank"</span>, <span class="hljs-symbol">currency_code:</span> <span class="hljs-string">"USD"</span>, <span class="hljs-symbol">country:</span> <span class="hljs-string">"US"</span>)
</code></pre>
<p>However, when I ran the test suite, I found errors like the following:</p>
<pre><code class="lang-plaintext">Failure/Error: bank_account = FactoryBot.create(account_code: "CITI-123456789-USD", bank: "citibank", currency_code: "USD", country: "US")

NoMethodError:
undefined method `save!' for an instance of BankAccount
# /usr/local/bundle/gems/factory_bot-6.4.6/lib/factory_bot/evaluation.rb:15:in `create'
# /usr/local/bundle/gems/factory_bot-6.4.6/lib/factory_bot/strategy/create.rb:12:in `block in result'
# /usr/local/bundle/gems/factory_bot-6.4.6/lib/factory_bot/strategy/create.rb:9:in `result'
# /usr/local/bundle/gems/factory_bot-6.4.6/lib/factory_bot/factory.rb:43:in `run'
# /usr/local/bundle/gems/factory_bot-6.4.6/lib/factory_bot/factory_runner.rb:29:in `block in run'
# /usr/local/bundle/gems/factory_bot-6.4.6/lib/factory_bot/factory_runner.rb:28:in `run'
# /usr/local/bundle/gems/factory_bot-6.4.6/lib/factory_bot/strategy_syntax_method_registrar.rb:28:in `block in define_singular_strategy_method'
# ...
</code></pre>
<p>Of course, I have always used FactoryBot in Rails applications where each factory is associated with models inheriting from <code>ActiveRecord</code>.</p>
<p>Our entities do not implement the <code>save!</code> method, which is the <a target="_blank" href="https://thoughtbot.github.io/factory_bot/ref/build-and-create.html#to_create">default implementation</a> of the <code>to_create</code> method:</p>
<pre><code class="lang-ruby">to_create { <span class="hljs-params">|obj, context|</span> obj.save! }
</code></pre>
<p>Therefore, I had to <a target="_blank" href="https://github.com/thoughtbot/factory_bot/blob/main/GETTING_STARTED.md#custom-methods-to-persist-objects">override that behavior</a> for all our factories.</p>
<p>In this case, I did it in the FactoryBot support file:</p>
<pre><code class="lang-ruby"><span class="hljs-keyword">require</span> <span class="hljs-string">"factory_bot"</span>

RSpec.configure <span class="hljs-keyword">do</span> <span class="hljs-params">|config|</span>
  config.before(<span class="hljs-symbol">:suite</span>) <span class="hljs-keyword">do</span>
    FactoryBot.find_definitions
    FactoryBot.define <span class="hljs-keyword">do</span>
      initialize_with { new(**attributes) }

      to_create <span class="hljs-keyword">do</span> <span class="hljs-params">|obj, context|</span>
        Database.connection <span class="hljs-keyword">do</span> <span class="hljs-params">|db|</span>
          <span class="hljs-keyword">if</span> !context.respond_to?(<span class="hljs-symbol">:table_name</span>)
            raise NotImplementedError,
              <span class="hljs-string">"define a transient table_name attribute inside the factory with the name of the database table"</span>
          <span class="hljs-keyword">end</span>

          db[context.table_name.to_sym].insert(obj.to_h)
        <span class="hljs-keyword">end</span>
      <span class="hljs-keyword">end</span>
    <span class="hljs-keyword">end</span>
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>In the factory we define the transient attribute <code>table_name</code>:</p>
<pre><code class="lang-ruby">FactoryBot.define <span class="hljs-keyword">do</span>
  factory <span class="hljs-symbol">:bank_account</span>, <span class="hljs-class"><span class="hljs-keyword">class</span>: <span class="hljs-title">BankAccount</span> <span class="hljs-title">do</span></span>
    transient <span class="hljs-keyword">do</span>
      table_name { BankAccountRepository::TABLE_NAME }
    <span class="hljs-keyword">end</span>

    account_code { <span class="hljs-string">"CITI-00000001-USD"</span> }
    bank { <span class="hljs-string">"citibank"</span> }
    currency_code { <span class="hljs-string">"USD"</span> }
    country { <span class="hljs-string">"US"</span> }
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Since it is related to persistence, I decided to define the constant with the name of the table in the corresponding repository:</p>
<pre><code class="lang-ruby"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">BankAccountRepository</span></span>
  TABLE_NAME = <span class="hljs-symbol">:bank_accounts</span>
  <span class="hljs-comment"># ...</span>
</code></pre>
<p>And that is all. With those simple changes we already have at our disposal all the power that <code>FactoryBot.create</code> offers.</p>
<p>It is important that the error message that would appear in case of using the <em>create</em> strategy without having the transient attribute <code>table_name</code> defined, allow anyone to fix that error very quickly.</p>
<p>I must say that the team welcomed the new changes with open arms.</p>
<p>Thank you for reading and see you in the next one!</p>
<hr />
<div class="hn-embed-widget" id="buy-me-a-coffee"></div>]]></content:encoded></item><item><title><![CDATA[The one about how things work in an American fintech]]></title><description><![CDATA[This is my first time working for any American company. In many ways things work differently here than with other companies I have previously worked for, so I will tell you here about the ins and outs of this American fintech I am working for as a co...]]></description><link>https://davidmontesdeoca.dev/the-one-about-how-things-work-in-an-american-fintech</link><guid isPermaLink="true">https://davidmontesdeoca.dev/the-one-about-how-things-work-in-an-american-fintech</guid><category><![CDATA[fintech]]></category><category><![CDATA[fintech software development]]></category><category><![CDATA[software development]]></category><category><![CDATA[Software Engineering]]></category><category><![CDATA[agile]]></category><dc:creator><![CDATA[David Montesdeoca]]></dc:creator><pubDate>Fri, 29 Nov 2024 17:23:54 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1739389093975/346f6441-52ea-41d0-9be0-5ae4c24563cd.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>This is my first time working for any American company. In many ways things work differently here than with other companies I have previously worked for, so I will tell you here about the ins and outs of this American fintech I am working for as a contractor.</p>
<h2 id="heading-before-the-start">Before the start</h2>
<p>Before I started I had to go through a <strong>background screening</strong> process with their provider, <a target="_blank" href="https://www.orangetreescreening.com/">Orange Tree</a>. That process took several weeks but finally they gave the green light.</p>
<p>A couple of days before starting, I received an email in my personal email account with details for configuring the laptop received a few days before. Of course, it was a <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-working-with-macos-being-a-linux-user">macOS laptop</a>. If you are a contractor, you do not get a new laptop.</p>
<h2 id="heading-onboarding">Onboarding</h2>
<p>The first day was very simple. I joined a call with the IT people along with other people starting at the company that very same day as well so they can install the <strong>software that is able to remotely control our computers</strong> if needed. According to the IT people, you need to explicitly give permissions before they are able to do anything in the computer.</p>
<p>After that, I met with the engineering manager to tell me about how the project works, how it is organized, what are the responsibilities of the team and how the onboarding would be, among other stuff.</p>
<p>Then I was assigned a board with a series of tasks that I had to complete during the onboarding, like meeting people from different teams, roles and hierarchy levels in the company; reading documentation, completing the first task, doing a code review or deploying to production.</p>
<p>That first day I was already present at the daily meeting so I could meet my new teammates.</p>
<p>The next day I joined my <strong>onboarding buddy</strong> to set up the various applications we work on and to tell me what the day-to-day work is like. We spent a big part of my first few weeks doing <strong>pair programming</strong>.</p>
<p>The onboarding was completed in just over a month.</p>
<h2 id="heading-the-company">The company</h2>
<p>The headquarters of the company are in Boston.</p>
<p>The company has around 1,300 employees around the world, although most of the engineering team is based in Spain.</p>
<p>They recently announced that around 3% of the workforce would be affected by lay offs, with the engineering team not being affected at all right now.</p>
<p>Communication within the company is always in English, unless everyone present at a meeting speaks Spanish (or any other language).</p>
<p>At the company level there are some recurring meetings:</p>
<ul>
<li><strong>All Company</strong>: a 1-hour to 1.5-hour meeting where the CEO and other senior managers of the company tell how the business is doing and upcoming objectives. It is held once a month.</li>
<li><strong>All Hands Engineering</strong>: very similar to <em>All Company</em> meeting, but only for members of the engineering area. Here they mainly talk about changes already implemented in the different teams and upcoming projects or changes that we are about to implement. It is held once a month.</li>
</ul>
<p>Everybody is required to log however many hours they are required to work each week, included contractors like me.</p>
<h2 id="heading-the-project">The project</h2>
<p>In the engineering area there are multiple teams, each with governance over multiple applications.</p>
<p>However, this is not a strict ownership, because any developer can add or modify code in any of the repositories they have access to, as long as the owner team is tagged in the MR so that they can review it as well.</p>
<p>Such a review is usually optional, unless any of the changed files are included in the <a target="_blank" href="https://docs.gitlab.com/ee/user/project/codeowners/">CODEOWNERS file</a>.</p>
<p>Unfortunately, there is <strong>no standard way to do anything in the engineering team</strong>. In the documentation I had to read as part of the onboarding I found a style guide with details on how to write code, but in practice I can confirm that that style guide is not being followed by any of the existing teams.</p>
<p>That means that it is very likely that when we change code owned by other team we will have to do things differently than we would do it in our own code. My house, my rules!</p>
<p>In a daily basis, we work with money in one way or another, so <strong>security</strong> is critical. The company must comply with certain regulations, standards and laws, including anti-money laundering (AML), fraud prevention, and know your customer (KYC) procedures.</p>
<p>Periodically we must complete courses related to these topics to stay up to date.</p>
<p>Therefore, access to production applications, databases or console is totally restricted for developers.</p>
<p><strong>Observability</strong> in cases like this is essential, so we make extensive use of logs with tools like <a target="_blank" href="https://www.honeycomb.io/">Honeycomb</a> and <a target="_blank" href="https://www.sumologic.com/">Sumo Logic</a>, generating those logs through an internal gem that provides an <a target="_blank" href="https://opentelemetry.io/">OpenTelemetry</a> wrapper.</p>
<p>The <strong>error tracking</strong> of all applications is done with <a target="_blank" href="https://sentry.io/welcome/">Sentry</a>.</p>
<p>Sometimes, due to an error or simply because we need to apply a change or enable a feature flag in production, we have to open a support ticket on <a target="_blank" href="https://www.atlassian.com/software/jira">Jira</a> so that the support team can take care of it, usually by executing a Rake task.</p>
<p>Access to all applications is handled by <a target="_blank" href="https://www.okta.com/">Okta</a>, usually allowing to authenticate with biometrics (e.g. fingerprint). For certain applications, 2-factor authentication (<a target="_blank" href="https://2fas.com/">2FA</a>) is required, especially for those that are only accessible through a <a target="_blank" href="https://en.wikipedia.org/wiki/Virtual_private_network">VPN</a>.</p>
<p>At the code level, security is also taken into account, since the CI pipeline executes a step in each repository that checks for vulnerabilities either in our own code or in third-party code. At the moment it only shows warnings with all the vulnerabilities found, but soon it will cause our pipelines to fail.</p>
<p>In addition to the applications already mentioned, we work in a daily basis with the following applications:</p>
<ul>
<li><a target="_blank" href="https://workspace.google.com/">Google Suite</a> (GMail, Drive, Meet, Presentations, Spreadsheets, Docs).</li>
<li><a target="_blank" href="https://www.atlassian.com/software/confluence">Confluence</a> → Documentation.</li>
<li><a target="_blank" href="https://www.atlassian.com/software/jira">Jira</a> → Project tracking and support</li>
<li><a target="_blank" href="https://slack.com/">Slack</a> → Instant messaging.</li>
<li><a target="_blank" href="https://about.gitlab.com/">GitLab</a> → Coding and CI/CD.</li>
<li><a target="_blank" href="https://www.postman.com/">Postman</a> → API development.</li>
</ul>
<p>The <a target="_blank" href="https://aws.amazon.com/what-is/sre">SRE</a> team has created a series of internal tools that integrate into our daily work in a very simple way:</p>
<ul>
<li><p>A tool similar to <a target="_blank" href="https://kubernetes.io/">Kubernetes</a> that allows us to automate deployments, scaling and manage applications with containers.</p>
<p>They have everything perfectly organized to generate CI/CD pipelines with <a target="_blank" href="https://aws.amazon.com/what-is/iac/">IaC</a>, in each of the environments we have available to test our applications, before promoting the code to production.</p>
</li>
<li><p>A CLI tool similar to <a target="_blank" href="https://aws.amazon.com/cli/">AWS CLI</a> that allows to execute commands in those pre-production environments, simply by specifying the environment, the application and the command to be executed.</p>
</li>
<li>A tool that allows to manage the creation, modification and deletion of records of any of the entities of our domain in pre-production environments. It works as a test factory, generating any type of dependency that may be necessary along the way. This tool replaces a Slack bot that has been used until now and that will be deprecated soon.</li>
</ul>
<p>At the engineering team level there are some recurring meetings:</p>
<ul>
<li><strong>Dev Learning</strong>: a 15-minute to 1-hour meeting where one of our colleagues talks about a technical topic. It is held every Friday.</li>
<li><strong>Game Days</strong>: a 1.5-hour meeting where first every team shares their <a target="_blank" href="https://en.wikipedia.org/wiki/Postmortem_documentation">post-mortems</a> since the previous meeting, and later we are presented with a series of exercises prepared by other members of the engineering team for us to solve, organized in groups and with time constraints. The challenges usually must be solved without using the tools mentioned above. It is held once a month.</li>
</ul>
<p>We have dozens of internal gems hosted on <a target="_blank" href="https://www.buildkite.com/platform/package-registries/">Buildkite Package Registries</a> (formerly <a target="_blank" href="https://packagecloud.io/">Packagecloud</a>). Some examples of those gems, in addition to the OpenTelemetry wrapper mentioned above, allow us to integrate with banks we work with, such as Citibank or Bank of America. Among other things, it allows us to test new changes directly against their sandbox before releasing a new version of the gem.</p>
<h2 id="heading-my-team">My team</h2>
<p>It consists of an engineering manager (EM), a project manager (PM), a QA engineer and 4 software engineers, including myself. Everybody is based in Spain, except for one teammate who lives in Canada.</p>
<p>We have a flexible schedule. The only requirements are getting the job done and attending team meetings.</p>
<p>The team belongs to the <strong>Transactional</strong> vertical and is in charge of managing funds collection and payouts to many countries and in many currencies.</p>
<p>It is pretty common that we add new <strong>payment corridors</strong>, which is simply adding support for payments to specific countries in one or more currencies if we do not already have it in place.</p>
<p>Probably the <strong>automatic matching</strong> of received transactions and expected payments, either debits or credits, is the most important function we have, thus relieving the manual work of the operators who deal with all those transactions.</p>
<p>Some of the clients we work with are Citibank, Bank of America, J.P. Morgan, Deutsche Bank or Cross River Bank.</p>
<p>Most of our applications are pure backend applications, although we also have one <a target="_blank" href="https://developer.mozilla.org/en-US/docs/Glossary/SPA">SPA</a> developed with <strong>React</strong>. At the moment, only one of my teammates works with that application in case adding a new feature or fixing a bug is needed.</p>
<p>Regarding our backend applications, they are <strong>microservices</strong> developed in <strong>Ruby</strong> with <a target="_blank" href="https://sinatrarb.com/">Sinatra</a>. As far as I know, most people in this company are not fans of <a target="_blank" href="https://rubyonrails.org/">Rails</a>.</p>
<p>The gems we develop are of course developed in pure Ruby as well.</p>
<p>We have an event-driven architecture (<a target="_blank" href="https://aws.amazon.com/what-is/eda/">EDA</a>) in place, where the communication between microservices is usually asynchronous with events published to and consumed from <a target="_blank" href="https://www.rabbitmq.com/">RabbitMQ</a>.</p>
<p>Depending on the application, we also expose and consume data from <a target="_blank" href="https://blog.postman.com/rest-api-examples/">REST APIs</a> endpoints whose response is formatted in <a target="_blank" href="https://www.json.org/json-en.html">JSON</a>.</p>
<p>Each application has its own technology stack, but it is common to work with <a target="_blank" href="https://www.docker.com/">Docker</a>, <a target="_blank" href="https://docs.docker.com/compose/">docker compose</a>, <a target="_blank" href="https://aws.amazon.com/">AWS</a>, <a target="_blank" href="https://dry-rb.org/">Dry gems</a>, <a target="_blank" href="https://www.mysql.com/">MySQL</a>, <a target="_blank" href="https://www.elastic.co/elasticsearch">Elasticsearch</a> and <a target="_blank" href="https://redis.io/">Redis</a>, among others.</p>
<p>As I mentioned previously at the company level, the team did not follow a style guide for writing code when I joined. I am currently in the process of <a target="_blank" href="https://blog.davidmp.es/the-one-about-linting-in-a-legacy-ruby-project">adding RuboCop as a linter</a> to all applications of our governance.</p>
<p>In the same way, the way applications are tested is quite diverse, so I am trying to define a style guide for that as well. In the newest application we have developed I was already able to implement a new testing approach successfully, but I am aware that it will not be that easy to do it for the rest, among other things, because I find some resistance to change from the team.</p>
<p>Either way, what is common is that we use <a target="_blank" href="https://rspec.info/">RSpec</a> as a testing framework and <a target="_blank" href="https://github.com/thoughtbot/factory_bot">FactoryBot</a> as a tool to create database records in our test suite. I also introduced <a target="_blank" href="https://github.com/graceful-potato/rspec-swag">rspec-swag</a>, a <a target="_blank" href="https://github.com/rswag/rswag">rswag</a> fork that can be used with any Rack-compatible framework, to test our APIs and automatically generate the documentation of our endpoints with <a target="_blank" href="https://swagger.io/tools/swagger-editor/">Swagger</a>, in compliance with the <a target="_blank" href="https://swagger.io/specification/">OpenAPI</a> specification.</p>
<p>We follow an <a target="_blank" href="https://www.atlassian.com/agile">agile methodology</a>, more similar to <a target="_blank" href="https://www.atlassian.com/agile/kanban">Kanban</a> than to <a target="_blank" href="https://www.atlassian.com/agile/scrum">Scrum</a>, to develop our applications.</p>
<p>In the <strong>sprint board</strong> we have different rows, which represent each of the team's priorities. These priorities vary in each sprint, which lasts 2 weeks.</p>
<p>Regarding the development cycle, our sprint board has the following columns:</p>
<ul>
<li><strong>To Do</strong>: A task is created and is ready to be refined.</li>
<li><strong>Ready for Dev</strong> (Selected for Development): A task is already refined and has been included in the current sprint.</li>
<li><strong>In Dev</strong>: A developer is currently working on a task or the MR is already created and they are checking that it works as expected in one of the pre-production environments.</li>
<li><strong>Ready for QA</strong>: The developer finished working on a task and the CI pipeline must pass before moving on. The developer checked in one of the pre-production environments that everything works as expected and documented all their tests, with details about the commands executed or screenshots if necessary, so the QA engineer can validate them and add their own tests. At this point having the MR approved is not a requirement.</li>
<li><strong>In QA Review</strong>: The QA engineer is validating the tests made by the developer and adding their owns tests.</li>
<li><strong>QA Complete</strong>: The QA engineer successfully finished testing the task and it is now ready to be deployed. At this point the MR must be approved by at least 2 repository owners before being merged.</li>
<li><strong>Deployed</strong>: The MR was merged and the code deployed to production. The developer could wait until they confirm no errors are raised related to the changes deployed or a Rake task finishes its execution.</li>
<li><strong>Closed</strong>: There is nothing left to do regarding the task.</li>
</ul>
<p>And the workflow is as follows:</p>
<p><img src="https://github.com/user-attachments/assets/95eb636e-e23e-41fd-886b-fc6b6fd9b112" alt="American fintech development lifecycle workflow" /></p>
<p>Each developer is responsible for moving their tasks around the sprint board, except when the task is in one of the QA-related columns.</p>
<p>At the team level there are some recurring meetings:</p>
<ul>
<li><strong>Daily</strong>: a 15-minute meeting where each member of the team explains what they have been working on since the previous day and what they will continue working on. In case anything is blocking us, we mention it here as well. Some members of the support team usually join us a couple of days each week. At the end of their turn, each person nominates the next person who should speak.</li>
<li><strong>Refinement</strong>: a 30-minute to 1-hour meeting from a product point of view, where any doubts the team may have about certain tasks are solved. The "definition of done" is defined here, but without going into technical details. Tasks are rarely estimated, except when we want to know from the beginning if all of us think the task should be divided into multiple tasks or not. We use the Fibonacci sequence, between 3 and 13 points, where 3 points represents 1 day of work approximately. It is held once in each sprint.</li>
<li><strong>Split</strong>: a 1-hour meeting from a technical point of view, where we solve any doubts the team may have about how to implement certain changes and we write it down in the task itself. If necessary, we create a <a target="_blank" href="https://agilemania.com/what-is-a-spike-in-agile">spike</a> and discuss it again in a later meeting. This is held once in each sprint.</li>
<li><strong>Code Review</strong>: a 1-hour meeting where we review the concerns board, where each team member can add their concerns to solve technical debt. In each task we can define what we are concerned about and the solution we propose to solve it. This is held once in each sprint.</li>
<li><strong>Sprint Planning</strong>: a 30-minute meeting where the PM checks the unfinished tasks from the previous sprint and prioritizes the tasks to be included in the next sprint. The previous team's velocity is not taken into account, but the team's opinion is taken into account when it comes to include a task in the sprint or not. It is held on the first Monday of each sprint.</li>
<li><strong>Retrospective</strong>: a 1.5-hour meeting where each team member can add a list of things they think went well and what things we can improve since the previous retrospective, although we usually only talk about what did not go so well. Then everyone chooses the 3 topics that interest them the most and we spend the rest of the session trying to find possible solutions to the 3 most upvoted topics. A person is assigned to follow up on each of those topics. The meeting is lead by a facilitator from another team. Normally this is held once a month, although it sometimes takes longer.</li>
<li><strong>Game Days</strong>: a 30-minute meeting where the whole team spends time together, bonding with each other. We usually play online Pictionary or games like that. It is held on the second Monday of each sprint.</li>
<li><strong>1 to 1</strong>: a 30-minute meeting with each permanent full-time worker or a 15-minute meeting with each contractor with the EM to talk and periodically check on how things are going on both sides.</li>
<li><strong>Coffee Chat</strong>: a 15-minute meeting before the daily meeting on Fridays where we usually talk about our plans for the weekend.</li>
</ul>
<p>As you can see, we do not have <em>sprint review</em> or <em>sprint retrospective</em>.</p>
<p>Finally, on rotating basis, each week a developer of the team is the <strong>minion</strong>.</p>
<p>The highest priority of that person during that week is to help resolve support tickets that have been assigned to the team. If the solution is simple, they can create a task and implement the changes directly. If the solution is a bit more complex, they can create a new task and share it with the rest of the team to give more visibility. If it is urgent, that task is added directly to the sprint board.</p>
<p>In addition, the minion shares their screen with the sprint board on the daily meeting and takes notes in technical meetings.</p>
<p>The minion also has a dedicated row on the sprint board with tasks related to support tickets or technical debt tasks that are created from a concern.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>There was a lot more to tell than I had initially thought. In many ways I am sure this American fintech works very similarly to any other company you know or have worked for. Still, I think it is interesting to share how they work and how they are organized, but keeping the company name anonymous, for obvious reasons.</p>
<p>I hope to talk about my experience working with them soon. So stay tuned if you are interested.</p>
<p>Thank you for reading and see you in the next one!</p>
<hr />
<div class="hn-embed-widget" id="buy-me-a-coffee"></div>]]></content:encoded></item><item><title><![CDATA[The one about linting in a legacy Ruby project]]></title><description><![CDATA[Previously, I mentioned that I recently started working on a new project. New for me, of course.
The platform of this American fintech consists of many applications developed in multiple programming languages, but mainly in Ruby. There are several de...]]></description><link>https://davidmontesdeoca.dev/the-one-about-linting-in-a-legacy-ruby-project</link><guid isPermaLink="true">https://davidmontesdeoca.dev/the-one-about-linting-in-a-legacy-ruby-project</guid><category><![CDATA[standardrb]]></category><category><![CDATA[legacy-applications]]></category><category><![CDATA[Ruby]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Rubocop]]></category><category><![CDATA[standard]]></category><category><![CDATA[Linter]]></category><category><![CDATA[legacy]]></category><category><![CDATA[git-hooks]]></category><category><![CDATA[CI/CD pipelines]]></category><category><![CDATA[ci-cd]]></category><dc:creator><![CDATA[David Montesdeoca]]></dc:creator><pubDate>Mon, 28 Oct 2024 07:06:37 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1729268982009/0a5f70da-23a7-4955-b141-d70f89c32591.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Previously, I mentioned that I recently started working on a <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-my-new-job">new project</a>. New for me, of course.</p>
<p>The platform of this American fintech consists of many applications developed in multiple programming languages, but mainly in Ruby. There are several development teams, each with governance over different parts of the project. As you can imagine, over more than a decade literally several hundred developers have worked on the project and each and every one of us has had our say and preferences when it comes to writing code.</p>
<p><strong>There is no defined standard either at the company level or within each team</strong>. It is true that there are certain applications that are currently being developed in different teams that very vaguely we try to take as a reference for new developments, mainly to adopt good practices.</p>
<p>It is relevant to mention that my team is relatively new, formed just this year, and that the code in each of the applications over which we have governance is quite different from each other. Generally, they are <strong>legacy applications</strong>, where adopting a new approach to doing anything is not that simple and even generates panic among people who have been in the company the longest. Luckily, the team has recently created a <em>payouts application</em> where we have more freedom to innovate.</p>
<p>It is precisely in this kind of context where I think that <strong>using a linter is the best option</strong>.</p>
<p>Whenever starting a new application, my first commits are usually related to the configuration of the testing tool and the linter. However, these applications were not set up correctly from the get-go, so adding a linter definitely will bring a number of challenges.</p>
<p>The first time I talked to the rest of the team about the possibility of adding a linter to our applications to enforce a coding style, <strong>the reaction was mostly negative</strong>. The main argument was that other teams do not usually use it and that in some of their previous teams within this company they already tried to use it and it did not work. In addition, some of my teammates mentioned that if they had not been using it until now it is because we do not need it and that we should not enforce its use.</p>
<p>The way I see it, those are not solid arguments for not starting to use it, but I did not want to push it any further because I am a newcomer to the team and I knew the time to bring it up again would come.</p>
<p>A few days later, my onboarding buddy included a change in a MR where she added whitespaces after <code>{</code> and before <code>}</code> in an existing hash. One of the developers whose opinion is most valued within the company, who is not part of my team, reviewed that code and suggested that <strong>to avoid such changes in the future we could use a linter</strong>.</p>
<p>Of course, his opinion was taken into account, especially as it was meant to be introduced in the payouts application, where, as I said earlier, there is much more flexibility with the things we can do.</p>
<p>So for the next task I paired with my onboarding buddy and <strong>we added the linter as a previous step to other changes we had to implement</strong>. We decided to use <a target="_blank" href="https://github.com/rubocop/rubocop">RuboCop</a>, as it is the most used linter in the Ruby community and the linter I have been working with for a long time.</p>
<p>In the unlikely scenario that you are not familiar with this tool, the official documentation states the following:</p>
<blockquote>
<p>RuboCop is a Ruby <strong>static code analyzer</strong> (a.k.a. linter) and <strong>code formatter</strong>. Out of the box it will enforce many of the guidelines outlined in the community <a target="_blank" href="https://github.com/rubocop/ruby-style-guide">Ruby Style Guide</a>. Apart from reporting the problems discovered in your code, RuboCop can also automatically fix many of them for you.</p>
<p>RuboCop is extremely flexible and most aspects of its behavior can be tweaked via various configuration options.</p>
</blockquote>
<p>To start, once installed, we added the basic configuration in the <code>.rubocop.yml</code> file, located in the root of the application:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">require:</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">rubocop-performance</span>
  <span class="hljs-bullet">-</span> <span class="hljs-string">rubocop-rspec</span>

<span class="hljs-attr">AllCops:</span>
  <span class="hljs-attr">SuggestExtensions:</span> <span class="hljs-literal">false</span>
  <span class="hljs-attr">NewCops:</span> <span class="hljs-string">enable</span>
  <span class="hljs-attr">TargetRubyVersion:</span> <span class="hljs-number">3.3</span>

<span class="hljs-attr">Style/Documentation:</span>
  <span class="hljs-attr">Enabled:</span> <span class="hljs-literal">false</span>
</code></pre>
<p>Note that there is no configuration related to Rails because we usually do not work with that framework.</p>
<p>I added the following <a target="_blank" href="https://makefiletutorial.com/">make</a> commands to run the linter inside the Docker container where the application runs:</p>
<pre><code class="lang-Makefile"><span class="hljs-section">lint:</span>
  docker compose exec &lt;service&gt; bundle exec rubocop <span class="hljs-variable">$(<span class="hljs-built_in">if</span> <span class="hljs-variable">$(COP)</span>,--only <span class="hljs-variable">$(COP)</span>)</span>

<span class="hljs-section">lint-safe-fix:</span>
  docker compose exec &lt;service&gt; bundle exec rubocop -a <span class="hljs-variable">$(<span class="hljs-built_in">if</span> <span class="hljs-variable">$(COP)</span>,--only <span class="hljs-variable">$(COP)</span>)</span>

<span class="hljs-section">lint-unsafe-fix:</span>
  docker compose exec &lt;service&gt; bundle exec rubocop -A <span class="hljs-variable">$(<span class="hljs-built_in">if</span> <span class="hljs-variable">$(COP)</span>,--only <span class="hljs-variable">$(COP)</span>)</span>

<span class="hljs-section">lint-group-offenses:</span>
  docker compose exec &lt;service&gt; bundle exec rubocop --format offenses

<span class="hljs-section">lint-generate-todos:</span>
  docker compose exec &lt;service&gt; bundle exec rubocop --auto-gen-config --auto-gen-only-exclude --no-exclude-limit
</code></pre>
<p>Next, we ran the linter to check how many offenses exist in the codebase:</p>
<pre><code class="lang-bash">$ make lint
</code></pre>
<p>Get a list of all offenses grouped by cop:</p>
<pre><code class="lang-bash">$ make lint-group-offenses
</code></pre>
<p>Usually most offenses are related to quotes (or string literals):</p>
<pre><code class="lang-yaml"><span class="hljs-attr">Style/StringLiterals:</span>
  <span class="hljs-attr">Enabled:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">EnforcedStyle:</span> <span class="hljs-string">double_quotes</span>
</code></pre>
<p>Here you can choose the option you prefer, single or double quotes, and run the command that automatically solves those offenses:</p>
<pre><code class="lang-bash">$ make lint-unsafe-fix COP=Style/StringLiterals
</code></pre>
<blockquote>
<p>Make sure you have a good test suite before you start applying automatic changes with the linter.</p>
</blockquote>
<p>Since payouts application is still small, we checked each type of offense one by one.</p>
<p>The next cops I usually configure are related to conditionals:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">Style/IfUnlessModifier:</span>
  <span class="hljs-attr">Enabled:</span> <span class="hljs-literal">false</span>

<span class="hljs-attr">Style/NegatedIf:</span>
  <span class="hljs-attr">Enabled:</span> <span class="hljs-literal">false</span>

<span class="hljs-attr">Style/NegatedUnless:</span>
  <span class="hljs-attr">Enabled:</span> <span class="hljs-literal">true</span>

<span class="hljs-attr">Style/UnlessElse:</span>
  <span class="hljs-attr">Enabled:</span> <span class="hljs-literal">true</span>

<span class="hljs-attr">Style/IfUnlessModifierOfIfUnless:</span>
  <span class="hljs-attr">Enabled:</span> <span class="hljs-literal">true</span>

<span class="hljs-attr">Style/InvertibleUnlessCondition:</span>
  <span class="hljs-attr">Enabled:</span> <span class="hljs-literal">true</span>
</code></pre>
<p>For more details about that configuration, take a look to <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-conditionals-in-ruby">this other post</a>.</p>
<p>Of course, this is a personal choice and I expected to open a discussion with my teammates in the MR later on regarding the final configuration.</p>
<p>We then repeated the same process of checking the cop with most offenses, configured it as desired and automatically corrected offenses where possible. Not all cops allow to autocorrect offenses.</p>
<p>For those cops I find any offenses, I like to <strong>be explicit even if it is to configure the default option of a cop</strong>.</p>
<p>At least initially, I also like to have a single place where those files that contain some offenses are temporarily being ignored. Therefore, I include in the <code>.rubocop.yml</code> file <a target="_blank" href="https://docs.rubocop.org/rubocop/configuration.html#cop-specific-include-and-exclude">which files are being excluded for each cop</a>:</p>
<pre><code class="lang-yaml"><span class="hljs-attr">Metrics/AbcSize:</span>
  <span class="hljs-attr">Enabled:</span> <span class="hljs-literal">true</span>
  <span class="hljs-attr">CountRepeatedAttributes:</span> <span class="hljs-literal">false</span>
  <span class="hljs-attr">Exclude:</span>
    <span class="hljs-bullet">-</span> <span class="hljs-string">app/commands/initiate_payment.rb</span>
</code></pre>
<p>Another option, if you want to be even more fine-grained, is to disable <a target="_blank" href="https://docs.rubocop.org/rubocop/configuration.html#disabling-cops-within-source-code">cops inline</a>, only in certain parts of a file:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># rubocop:disable Layout/LineLength, Style/StringLiterals</span>
[...]
<span class="hljs-comment"># rubocop:enable Layout/LineLength, Style/StringLiterals</span>
</code></pre>
<p>However, I agree with <a target="_blank" href="https://docs.gitlab.com/ee/development/rubocop_development_guide.html#disabling-rules-inline">GitLab</a> on this issue, especially in legacy applications.</p>
<p>Once all offenses have been solved or explicitly disabled, we ensure that no new offenses will be added to the code if the developer adding new changes remembers to run the linter, because we are not enforcing its use neither on the machine of every developer nor in the CI pipeline.</p>
<p><strong>What is the point of configuring a linter if you do not enforce its use?</strong></p>
<p>Thus, in a follow-up task where I refactored much of the code for the payouts application, I added the configuration for the CI pipeline, knowing that it would raise some questions among my teammates once the MR was reviewed. And so it did.</p>
<p>We met and agreed that those changes would be removed from that MR and we would have a dedicated meeting to talk about it later.</p>
<p>A few weeks later we met again and it was up to me to try to convince my team about why it is necessary to have a linter configured and also to enforce its use, especially in the CI pipeline.</p>
<p>My main selling point was that RuboCop provides an initial configuration based on the <a target="_blank" href="https://github.com/rubocop/ruby-style-guide">Ruby style guide</a>, in addition to other community-relevant style guides such as <a target="_blank" href="https://github.com/rubocop/rspec-style-guide">RSpec</a> or <a target="_blank" href="https://github.com/rubocop/rails-style-guide">Rails</a>, with corresponding dependencies added.</p>
<p>Other relevant arguments largely match those mentioned by <a target="_blank" href="https://evilmartians.com/chronicles/rubocoping-with-legacy-bring-your-ruby-code-up-to-standard#style-matters">Evil Martians</a>:</p>
<ul>
<li>Consistent code style, avoiding discussions based on preferences of an individual.</li>
<li>Onboarding new engineers becomes much easier when the code style is standardized.</li>
<li>Linters help detect and squash bugs in a timely fashion.</li>
</ul>
<p>I must say that <strong>there was no outright refusal from the team</strong>, but they were reluctant about using a linter.</p>
<p>Their arguments against it include the following points, in addition to some previously mentioned:</p>
<ul>
<li>In the payouts application we can use it, not in the rest of applications.</li>
<li>We should not use a linter in all our applications because not all teams use a linter and we do not know if next year the governance will change.</li>
<li>Do not refactor any of the code if it is not strictly necessary, for things like the cyclomatic complexity of a method or a class, because it would be difficult to detect possible errors in a MR that includes changes in (literally) hundreds or even thousands of files.</li>
<li>Enforce the use of the linter on the machine of every developer with a <a target="_blank" href="https://git-scm.com/book/ms/v2/Customizing-Git-Git-Hooks">git hook</a> so it is not necessary to include it in the CI pipeline.</li>
<li>Some people do not care about linters, although if we use it they will adapt.</li>
<li>Do not use <code>.rubocop-todo.yml</code> file for reasons very similar to those shared by <a target="_blank" href="https://evilmartians.com/chronicles/rubocoping-with-legacy-bring-your-ruby-code-up-to-standard#todo-or-not-todo">Evil Martians</a>.</li>
<li>A teammate suggested we could use <a target="_blank" href="https://github.com/standardrb/standard">Standard</a> as a linter to avoid <strong>bikeshedding</strong> by having to choose which rules to follow.</li>
</ul>
<p>Of course, I agree with some of those arguments, so my proposal at all times accommodated their arguments, trying to find a point where we all felt comfortable.</p>
<p>In the case of RuboCop vs Standard, I prefer to invest some time initially and directly have the flexibility provided by RuboCop to decide the rules we are gonna follow, adapting to the application at hand, instead of using Standard that under the hood itself relies on RuboCop and <a target="_blank" href="https://evilmartians.com/chronicles/rubocoping-with-legacy-bring-your-ruby-code-up-to-standard#sticking-with-rubocop-or-switching-to-standard-cli">does not allow to overwrite rules</a>. I think that we should adapt the configuration to the application at hand, because not always you can apply the same rules to a greenfield application and to a legacy application. I prefer to <strong>start with RuboCop's default configuration and adjust the configuration of each cop as needed</strong>.</p>
<p>I suggested that I could create a <em>proof of concept</em> adding the linter to one of our larger applications to gauge the effort needed to add it, see what changes needed to be applied and finally decide together if we wanted to go ahead with it. They agreed.</p>
<p>I added the required gems to the <code>Gemfile</code> and added the RuboCop configuration we already had for the payouts application.</p>
<p>I started again getting the list of all offenses grouped by cop:</p>
<pre><code class="lang-bash">$ make lint-group-offenses
</code></pre>
<p>The linter found ~32k offenses. More than half related to quotes.</p>
<p>Then I ran the command that automatically fix all offenses, <a target="_blank" href="https://docs.rubocop.org/rubocop/usage/auto_correct.html">both the safe and unsafe ones</a>:</p>
<pre><code class="lang-bash">$ make lint-unsafe-fix
</code></pre>
<blockquote>
<p>Remember to run the tests every time you change something with the linter. Having a good test suite is the key to success here.</p>
</blockquote>
<p>About 60 tests started to fail. All those errors were related to mutated strings by adding the following line to all files:</p>
<pre><code class="lang-ruby"><span class="hljs-comment"># frozen_string_literal: true</span>
</code></pre>
<p>In some cases it was enough to change <code>String#gsub!</code> with <code>String#gsub</code>. In other cases, the encoding of a string was being changed with <code>String#force_encoding</code>, so we basically had to choose between creating a new unfrozen object with <code>Object#dup</code> and setting the magic comment to <code>false</code>.</p>
<p>Once those changes were made, all tests passed correctly.</p>
<p>However, I only did that to check that it was feasible to start using the linter in that application. What I did next was <strong>undo all the changes I had made to start creating a commit for each of the cops with the most offenses</strong>. Not all of them, because that would take too long.</p>
<p>Some of the cops with the most offenses were the usual suspects:</p>
<ul>
<li><code>Style/StringLiterals</code></li>
<li><code>Style/FrozenStringLiteralComment</code></li>
<li><code>Layout/DotPosition</code></li>
<li><code>Layout/EmptyLineAfterMagicComment</code></li>
<li><code>Layout/LineLength</code></li>
<li><code>Layout/SpaceAfterComma</code></li>
<li>...</li>
</ul>
<p>For every one of them I followed the same process I had already followed for the payouts application.</p>
<ul>
<li>Check the configuration options for the cop on the <a target="_blank" href="https://docs.rubocop.org/rubocop/cops.html">official RuboCop documentation</a>.</li>
<li>Correct the errors automatically for that cop, whenever possible.</li>
<li>Run the tests.</li>
<li>Fix any tests that might be broken.</li>
<li>Create a commit for that cop.</li>
</ul>
<p>As I said before, fixing one by one all offenses would take too long, so all offenses were securely fixed:</p>
<pre><code class="lang-bash">$ make lint-safe-fix
</code></pre>
<p>With <code>git diff</code> I checked all the changes made and if for some reason any of them did not work for me, I would undo all changes and repeat the same process as mentioned before.</p>
<p>After that, all offenses were insucurely fixed and the same process was repeated yet again.</p>
<p>Finally, I generated the file with all the remaining offenses:</p>
<pre><code class="lang-bash">$ make lint-generate-todos
</code></pre>
<p>Exclude the remaining offenses in the <code>.rubocop.yml</code> file after running the linter again.</p>
<p>Also I configured the git hook that would run just before pushing to the remote repository of each application.</p>
<p>Create following <code>config/git-hooks/pre-push</code> file:</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/sh</span>

<span class="hljs-built_in">echo</span> <span class="hljs-string">"Running linter..."</span>

<span class="hljs-keyword">if</span> ! make lint; <span class="hljs-keyword">then</span>
  <span class="hljs-built_in">echo</span> <span class="hljs-string">"Linter detected issues. Push aborted."</span>
  <span class="hljs-built_in">exit</span> 1
<span class="hljs-keyword">fi</span>

<span class="hljs-built_in">echo</span> <span class="hljs-string">"Linter passed. Proceeding with push."</span>
<span class="hljs-built_in">exit</span> 0
</code></pre>
<p>Add <code>config/setup-git-hooks</code> file, that will run when the application is built with Docker:</p>
<pre><code class="lang-bash"><span class="hljs-meta">#!/bin/sh</span>

cp config/git-hooks/* .git/hooks/
chmod +x .git/hooks/*

<span class="hljs-built_in">echo</span> <span class="hljs-string">"Git hooks successfully installed"</span>
</code></pre>
<p>Add execute permission to that file with following command:</p>
<pre><code class="lang-bash">$ chmod +x config/setup-git-hooks
</code></pre>
<p>Bear in mind that git hooks can be skipped:</p>
<pre><code class="lang-bash">$ git push --no-verify
</code></pre>
<blockquote>
<p>Use that option only if you really have a good reason for it.</p>
</blockquote>
<p>And that was the proof of concept. When we met again, I told the team everything I had done, clarified some of the questions they had, and we agreed on the following:</p>
<ul>
<li>We are going to start using the linter in all our applications.</li>
<li>Create a parent task on Jira containing a subtask for every application to which the linter needs to be added.</li>
<li>Use the existing configuration for the payouts application as starting point and in all other cases we use the default RuboCop configuration, unless we have a strong opinion about any of the cops.</li>
<li>Do not add the linter to the CI pipeline step because we have the git hook. I still think it would be the right thing to do, though.</li>
<li>Use the <code>.rubocop-todo.yml</code> file to maintain the list of offenses that are being ignored, keeping in mind that it can be regenerated as those offenses are resolved.</li>
<li>We are all committed to resolve the offenses little by little.</li>
</ul>
<p>That is all I can share for now. Tasks are already created and added to our board, but the hardest part is to find the right time to tackle them.</p>
<p>Nobody said that adding a linter to a legacy application is easy, but I am totally convinced that when we have done it, it will be beneficial for all of us on the team and those yet to come. Hopefully this will be the spark for other teams to think about the possibility of using a linter seriously in all their applications.</p>
<p>In any case, keep in mind that here I am only focusing on a style linter and formatter like RuboCop, but there are other kinds of linters, like <a target="_blank" href="https://github.com/troessner/reek">Reek</a> or <a target="_blank" href="https://brakemanscanner.org/">Brakeman</a>, that are complementary to RuboCop and that I also consider to be very useful in any application.</p>
<p>Before finishing, I would like to mention that it is quite handy to view the warnings and errors produced by RuboCop as you type code or save changes in your editor or IDE instead of having to run the checks through the command line every time, so configure any of the <a target="_blank" href="https://docs.rubocop.org/rubocop/integration_with_other_tools.html">integrations available</a> to improve your workflow.</p>
<p>Thank you for reading and see you in the next one!</p>
<hr />
<div class="hn-embed-widget" id="buy-me-a-coffee"></div>]]></content:encoded></item><item><title><![CDATA[The one about conditionals in Ruby]]></title><description><![CDATA[Conditionals control the flow of execution of your program based on conditions that you define. In Ruby we have conditional statements such as if, else, elsif, unless, case, or the ternary operator.
Here I would like to focus on what I consider to be...]]></description><link>https://davidmontesdeoca.dev/the-one-about-conditionals-in-ruby</link><guid isPermaLink="true">https://davidmontesdeoca.dev/the-one-about-conditionals-in-ruby</guid><category><![CDATA[Ruby]]></category><category><![CDATA[Ruby on Rails]]></category><category><![CDATA[Rubocop]]></category><category><![CDATA[Linter]]></category><category><![CDATA[Conditional statement]]></category><category><![CDATA[Conditionals]]></category><dc:creator><![CDATA[David Montesdeoca]]></dc:creator><pubDate>Sat, 28 Sep 2024 19:57:12 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1727548097607/573dff93-6e49-4330-9d17-abcb020cdca9.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Conditionals control the flow of execution of your program based on conditions that you define. In Ruby we have <strong>conditional statements</strong> such as <code>if</code>, <code>else</code>, <code>elsif</code>, <code>unless</code>, <code>case</code>, or the ternary operator.</p>
<p>Here I would like to focus on what I consider to be in most cases a questionable use of <code>unless</code> and what I think could be a better approach. YMMV.</p>
<p>But first, I will start giving a bit of context about that conditional statement.</p>
<p>As far as I know, <a target="_blank" href="https://www.perl.org/">Perl</a> was the first one that introduced <code>unless</code> to provide a more readable alternative to using negated <code>if</code>.</p>
<p>Although the keyword <code>unless</code> is not available in most programming languages, Ruby already included it in its first version released in 1995. Other languages highly inspired by Ruby, such as <a target="_blank" href="http://crystal-lang.org/">Crystal</a> or <a target="_blank" href="https://elixir-lang.org/">Elixir</a>, include that <strong>control structure</strong> as well.</p>
<p>Its function is the opposite of the <code>if</code> statement, meaning that will <strong>execute a block of code only if a condition is false</strong>. You could say it is <em>syntactic sugar</em> for a negated <code>if</code>.</p>
<blockquote>
<p>Remember that the only <em>falsey</em> values in Ruby, those that evaluate to false, are <code>false</code> and <code>nil</code>.</p>
</blockquote>
<p>I have read a lot about how <code>unless</code> improves readability in cases where you are checking for negative conditions, making the code flow more naturally and even closer to plain English.</p>
<p>And I wonder, is that so?</p>
<p>Most of the people I have worked with come to Ruby from other languages, such as .NET, PHP, Python or Java. I have asked many of them what is their opinion about the use of <code>unless</code> in Ruby and not a single one of them has shared with me a positive experience. Ever.</p>
<p>They usually have expressed confusion towards any code where that statement is used. Of course, you could think it is a small sample to draw many conclusions. However, if you search the web you will find <a target="_blank" href="https://stackoverflow.com/questions/13245001/cant-understand-unless-keyword-in-ruby">other</a> <a target="_blank" href="https://www.reddit.com/r/ruby/comments/ob8f6k/is_it_just_me_or_is_unless_needlessly_confusing/">people</a> as confused as my teammates.</p>
<p>I tend to agree, but I must say it was not always like that. In my first contact with Ruby, I kind of liked using <code>unless</code> whenever I considered it was necessary without much hesitation.</p>
<p>However, nowadays I avoid its use as much as possible. In my opinion, it <strong>adds cognitive load</strong> in most cases. Every single time I see an <code>unless</code> in the code I have to stop and <strong>translate it in my head to "if not"</strong>. Even the easiest condition.</p>
<p>And I have been working with Ruby for almost 15 years now, no wonder it is difficult to grasp for people just learning the language.</p>
<p>In my side projects I completely avoid its use, but in a real job you need to reach an agreement with the rest of the team if you want the codebase to be as consistent as possible.</p>
<p>Although <code>unless</code> is vastly used in the Ruby community, surprisingly I did not find much literature about it, with <a target="_blank" href="https://signalvnoise.com/posts/2699-making-sense-with-rubys-unless">some exception</a>. I am not taking into account the articles about the basic structures of the language, of course.</p>
<p>I would like to highlight that the author of the post linked above considers following code "little gems":</p>
<pre><code class="lang-ruby">i += <span class="hljs-number">1</span> <span class="hljs-keyword">unless</span> i &gt; <span class="hljs-number">10</span>

<span class="hljs-keyword">unless</span> person.present?
  puts <span class="hljs-string">"There's no such person"</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Once again, I wonder if <a target="_blank" href="https://en.wikipedia.org/wiki/Ruby_\(programming_language\)">Ruby</a> was designed with an emphasis on programming productivity and simplicity, why complicate things?</p>
<p>I would write the same code as follows:</p>
<pre><code class="lang-ruby">i += <span class="hljs-number">1</span> <span class="hljs-keyword">if</span> i &lt;= <span class="hljs-number">10</span>

<span class="hljs-keyword">if</span> person.blank?
  puts <span class="hljs-string">"There's no such person"</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Much easier, right? To me it feels more natural and easier to understand. By far.</p>
<p>Whilst I disagree with his examples, I agree with his rules of thumb:</p>
<ul>
<li>Avoid using more than a single logical condition.</li>
<li>Avoid negation as <code>unless</code> is already negative.</li>
<li>Never use an <code>else</code> clause with an <code>unless</code> statement.</li>
</ul>
<p>Next I will show some real-life examples with <code>unless</code> along with the code I would write instead.</p>
<p>Probably the most common example is the use of the truthy value of a given variable:</p>
<pre><code class="lang-ruby"><span class="hljs-keyword">return</span> <span class="hljs-keyword">unless</span> user
</code></pre>
<p>Nobody will ever convince me that previous line is more readable that the following one:</p>
<pre><code class="lang-ruby"><span class="hljs-keyword">return</span> <span class="hljs-keyword">if</span> user.<span class="hljs-literal">nil</span>?
</code></pre>
<p>Other common example is with collections:</p>
<pre><code class="lang-ruby"><span class="hljs-keyword">return</span> <span class="hljs-keyword">unless</span> posts.any?

<span class="hljs-comment"># ...instead of...</span>

<span class="hljs-keyword">return</span> <span class="hljs-keyword">if</span> posts.empty?
</code></pre>
<p>Another common example is with equality:</p>
<pre><code class="lang-ruby"><span class="hljs-keyword">return</span> <span class="hljs-keyword">unless</span> currency == <span class="hljs-string">"EUR"</span>

<span class="hljs-comment"># ...instead of...</span>

<span class="hljs-keyword">return</span> <span class="hljs-keyword">if</span> currency != <span class="hljs-string">"EUR"</span>
</code></pre>
<p>Certainly there is a lot of personal preference here. What is readable for you may not be readable for me. And the other way around.</p>
<p>A good example could be the next one:</p>
<pre><code class="lang-ruby"><span class="hljs-keyword">return</span> <span class="hljs-keyword">unless</span> user.confirmed?

<span class="hljs-comment"># ...instead of...</span>

<span class="hljs-keyword">return</span> <span class="hljs-keyword">if</span> !user.confirmed?
</code></pre>
<p>Personally, I prefer negating the condition in that case, but I think we should always stick to whatever the team decides.</p>
<p>I would even prefer the use of <code>not</code> if what we are looking for is readability. Although I have never done that because the <a target="_blank" href="https://rubystyle.guide/#bang-not-not">Ruby style guide</a> has always discouraged it.</p>
<p>Yet another example would be the use of <code>unless</code> along with <code>else</code>:</p>
<pre><code class="lang-ruby"><span class="hljs-keyword">unless</span> user_signed_in?
  render <span class="hljs-string">"login"</span>
  render <span class="hljs-string">"signup"</span>
<span class="hljs-keyword">else</span>
  render <span class="hljs-string">"profile"</span>
  render <span class="hljs-string">"logout"</span>
<span class="hljs-keyword">end</span>

<span class="hljs-comment"># ...instead of...</span>

<span class="hljs-keyword">if</span> user_signed_in?
  render <span class="hljs-string">"profile"</span>
  render <span class="hljs-string">"logout"</span>
<span class="hljs-keyword">else</span>
  render <span class="hljs-string">"login"</span>
  render <span class="hljs-string">"signup"</span>
<span class="hljs-keyword">end</span>
</code></pre>
<p>Fortunately, it is not so common to find such code.</p>
<p>The one rule I always follow, mentioned above, is to never use more than one logical condition with <code>unless</code>.</p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">user_has_access_to_course?</span></span>
  <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span> <span class="hljs-keyword">unless</span> user.present? &amp;&amp; course.present?

  user.purchased_course?(course)
<span class="hljs-keyword">end</span>

<span class="hljs-comment"># ...instead of...</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">user_has_access_to_course?</span></span>
  <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span> <span class="hljs-keyword">if</span> user.blank? <span class="hljs-params">||</span> course.blank?

  user.purchased_course?(course)
<span class="hljs-keyword">end</span>
</code></pre>
<p>My brain is about to explode every time I run into that kind of conditions.</p>
<p>I also prefer to see any conditional upfront, so I try to avoid <strong>conditional modifiers</strong> as much as possible. Those conditionals are placed after another statement allowing to run the code only when the condition is met.</p>
<p>The obvious exception to that rule are <strong>guard clauses</strong>, mainly used to avoid a condition wrapping the whole body of a method:</p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">url</span></span>
  <span class="hljs-keyword">if</span> course
    course_url(course)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>

<span class="hljs-comment"># ...or...</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">url</span></span>
  course_url(course) <span class="hljs-keyword">if</span> course
<span class="hljs-keyword">end</span>

<span class="hljs-comment"># ...but preferably...</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">url</span></span>
  <span class="hljs-keyword">return</span> <span class="hljs-keyword">if</span> course.<span class="hljs-literal">nil</span>?

  course_url(course)
<span class="hljs-keyword">end</span>
</code></pre>
<p>Keep in mind that the code before the condition is simple in those examples, but I have found on countless ocassions a conditional at the end of a line with a length of 100 characters or more. That is not exactly what I would call readability.</p>
<p>Besides that when there are two possible paths in the code, I do not care if the code is written with a full <code>if</code>/<code>else</code> or a guard clause:</p>
<pre><code class="lang-ruby"><span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">url</span></span>
  <span class="hljs-keyword">if</span> current_user.<span class="hljs-literal">nil</span>?
    public_course_url(course)
  <span class="hljs-keyword">else</span>
    private_course_url(course)
  <span class="hljs-keyword">end</span>
<span class="hljs-keyword">end</span>

<span class="hljs-comment"># ...alternatively...</span>

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">url</span></span>
  <span class="hljs-keyword">return</span> public_course_url(course) <span class="hljs-keyword">if</span> current_user.<span class="hljs-literal">nil</span>?

  private_course_url(course)
<span class="hljs-keyword">end</span>
</code></pre>
<p>But usually my teammates have prefered the guard clause, and I am ok with that option, as long as we are consistent.</p>
<p>As I said before, you have to reach an agreement with your team about how to write the code. In my opinion, a linter is the best possible tool to enforce those agreements.</p>
<p>Every single team I have been part of have always chosen <a target="_blank" href="https://github.com/rubocop/rubocop">RuboCop</a> as linter. Out of the box it will enforce many of the guidelines outlined in the community <a target="_blank" href="https://rubystyle.guide/">Ruby Style Guide</a>.</p>
<p>Next, I would like to highlight some cops that are related to conditionals:</p>
<ul>
<li><a target="_blank" href="https://docs.rubocop.org/rubocop/cops_style.html#styleifunlessmodifier">Style/IfUnlessModifier</a>: Checks for <code>if</code> and <code>unless</code> statements that would fit on one line if written as modifier and checks for conditional modifiers lines that exceed the maximum line length. I always disable it.</li>
<li><a target="_blank" href="https://docs.rubocop.org/rubocop/cops_style.html#stylenegatedif">Style/NegatedIf</a>: Checks for uses of <code>if</code> with a negated condition. Only <code>if</code>s without <code>else</code> are considered. I always disable it.</li>
<li><a target="_blank" href="https://docs.rubocop.org/rubocop/cops_style.html#styleinvertibleunlesscondition">Style/InvertibleUnlessCondition</a>: Checks for usages of <code>unless</code> which can be replaced by <code>if</code> with inverted condition. I always enable it.</li>
<li><a target="_blank" href="https://docs.rubocop.org/rubocop/cops_style.html#stylenegatedunless">Style/NegatedUnless</a>: Checks for uses of <code>unless</code> with a negated condition. Only <code>unless</code> without <code>else</code> are considered. I always enable it.</li>
<li><a target="_blank" href="https://docs.rubocop.org/rubocop/cops_style.html#styleunlesselse">Style/UnlessElse</a>: Looks for <code>unless</code> expressions with <code>else</code> clauses. I always enable it.</li>
<li><a target="_blank" href="https://docs.rubocop.org/rubocop/cops_style.html#styleifunlessmodifierofifunless">Style/IfUnlessModifierOfIfUnless</a>: Checks for <code>if</code> and <code>unless</code> statements used as modifiers of other conditional statements. I always enable it.</li>
</ul>
<p>The related rules from the style guide are the following:</p>
<ul>
<li><a target="_blank" href="https://rubystyle.guide/#if-as-a-modifier">if as a modifier</a></li>
<li><a target="_blank" href="https://rubystyle.guide/#unless-for-negatives">if vs unless</a></li>
<li><a target="_blank" href="https://rubystyle.guide/#no-else-with-unless">Using else with unless</a></li>
<li><a target="_blank" href="https://github.com/rubocop/ruby-style-guide#multi-line-if-modifiers">Multi-line if modifiers</a></li>
<li><a target="_blank" href="https://rubystyle.guide/#no-nested-modifiers">Nested modifiers</a></li>
</ul>
<p>Hopefully I have given to you some arguments why <code>unless</code> is not the best option to create readable code, but that is something very subjective, so the take away is to write conditionals in a consistent way, preferably following a style guide defined by the community or your own team.</p>
<p>Thank you for reading and see you in the next one!</p>
<hr />
<div class="hn-embed-widget" id="buy-me-a-coffee"></div>]]></content:encoded></item><item><title><![CDATA[The one about working with macOS being a Linux user]]></title><description><![CDATA[I will start with an unpopular opinion: I hate macOS and I hate Apple.
TL;DR
Next I will explain my reasons behind that unpopular opinion.
In case you are not interested in my opinion or simply want to see what I did as a Linux user to (kind of) happ...]]></description><link>https://davidmontesdeoca.dev/the-one-about-working-with-macos-being-a-linux-user</link><guid isPermaLink="true">https://davidmontesdeoca.dev/the-one-about-working-with-macos-being-a-linux-user</guid><category><![CDATA[Linux]]></category><category><![CDATA[Ubuntu]]></category><category><![CDATA[macOS]]></category><category><![CDATA[software development]]></category><category><![CDATA[VS Code]]></category><dc:creator><![CDATA[David Montesdeoca]]></dc:creator><pubDate>Thu, 29 Aug 2024 18:27:49 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/b18TRXc8UPQ/upload/1047f0401b548ebe628ca4534e6783a0.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I will start with an unpopular opinion: <strong>I hate macOS and I hate Apple</strong>.</p>
<h2 id="heading-tldr">TL;DR</h2>
<p>Next I will explain <a class="post-section-overview" href="#heading-my-reasons">my reasons</a> behind that unpopular opinion.</p>
<p>In case you are not interested in my opinion or simply want to see what I did as a Linux user to (kind of) happily work with macOS, you can jump directly to the part where I share <a class="post-section-overview" href="#heading-my-configuration">my configuration</a>.</p>
<h2 id="heading-my-reasons">My reasons</h2>
<p>The way I see it, Apple is a company that simply sells overpriced products, although usually are products with very good quality. I'll give them that.</p>
<p>Honestly, <strong>I will never understand what all the fuss is about</strong>, especially in the tech industry.</p>
<p>As a software developer, I don't get why so many tech companies force you to work with macOS, instead of asking you what operating system do you prefer to be more productive.</p>
<p>In fact, lots of them include working with a MacBook in their job offers as if it were a perk. Seriously? I guess they think it's really cool to have a bitten apple on the lid of your laptop. However, most of the time <strong>for half the price you could get a laptop with similar features or even better</strong>, depending on the laptop we are talking about.</p>
<p>In my previous post, I talked about <a target="_blank" href="https://davidmontesdeoca.dev/the-one-about-my-new-job">my new job</a>. The consulting firm allowed me to choose between Windows and macOS. I chose Windows simply because I wanted to install Ubuntu and have a dual boot. Finally I didn't do it because they don't have support for Linux, but mainly because I don't need that laptop at all. I used it for a few weeks but right now it's kept in a drawer, where it will remain until the day I have to return it.</p>
<p>On the other hand, the end client I'm actually working on, an American fintech company, didn't give me the opportunity to choose. They send me a relatively old <em>MacBook Air with the M1 chip</em>. So everybody in the company works with macOS, whether they like it or not.</p>
<p>I get that for IT and security teams (probably among others) is easier that everybody uses the same operating system. I also get that not every tool is available for Linux systems. That being said, I think that there is an <a target="_blank" href="https://osssoftware.org/">open source</a> alternative for most of them.</p>
<p>Anyway, during my first day with my new team I shared my feelings about macOS and everybody said that at least it isn't Windows. I used to think that way too, but after using the Windows laptop I mentioned before for a few weeks with <a target="_blank" href="https://ubuntu.com/desktop/wsl">WSL</a> I must say that the experience was better than I expected.</p>
<p>And don't get me wrong, <strong>I hate Windows</strong> too. But for different reasons.</p>
<p>I know, I know. I'm just another hater...</p>
<p>If you've reached this point, you might ask yourself why so much animosity. And that would be a fair question.</p>
<p>Focusing here exclusively on macOS, it's mainly because of what I consider an awful <strong>user experience</strong> (<a target="_blank" href="https://en.wikipedia.org/wiki/User_experience">UX</a>).</p>
<p>I have previous experience working with macOS in a few companies, so I know what I'm talking about. The frustration I've felt every day working with macOS is something I have rarely felt working with Linux for almost 20 years.</p>
<p>I use a lot of shortcuts and I find quite frustrating in macOS that most of them don't work or do something completely different to what I expect.</p>
<p>The obvious exception in Linux is <code>ctrl+c</code> and <code>ctrl+v</code> in the terminal, where is required to press <code>shift</code> key to make it work as expected. Definitely that was really confusing for me at the beginning.</p>
<p>Although almost every Linux distribution has its own desktop environment, package manager, etc., I love the feeling working with any of them. Everything works just fine from the start. Every shortcut works as I would expect. No frustration at all. And I think that is priceless.</p>
<p>Of course it's not perfect. Among other things, the support for some drivers in Linux is sometimes far from ideal, but I'm talking here about the overall user experience.</p>
<p>Although macOS came first (with another name), and <a target="_blank" href="https://www.quora.com/Why-do-Apple-Macs-have-a-command-key-in-addition-to-regular-PC-modifier-keys-and-why-do-they-insist-on-using-them-instead-of-control#:~:text=Because%20they%E2%80%99re%20for%20different%20things">cmd and ctrl keys had different purposes</a>, Windows was the real game changer during the '90s. For that reason and despite my feelings about that operating system, I'm glad that Linux followed the Windows way without the <code>cmd</code> key, allowing a smoother experience for any kind of user that wanted to try a quite different operating system. And that's precisely the main reason I hate macOS: <strong>Almost nothing works as I would expect from the start</strong>.</p>
<p>And I haven't even mentioned Finder yet. What the hell is wrong with that file manager? Well, let's keep it there...</p>
<p>In my previous experiences working with macOS I assumed I would have to deal with the frustration. So every time I used a shortcut with <code>ctrl</code> that didn't work, I used exactly the same combination with <code>cmd</code>. If you are something like me, that would happen constantly. You can't be really productive that way, always thinking about the next shortcut.</p>
<p>But you can't simply remap <code>ctrl</code> and <code>cmd</code> keys between them, because some shortcuts are expected to work with <code>ctrl</code> anyway.</p>
<p>So this time I decided that to be more productive from the beginning I needed to work as close as possible to how I work in Linux.</p>
<h2 id="heading-my-configuration">My configuration</h2>
<p>The macOS version is <strong>Sonoma 14.6.1</strong>.</p>
<p>The laptop with macOS comes with a U.S. keyboard layout. I didn't apply any remapping to it.</p>
<p>Recently I acquired a wireless keyboard with Spanish layout (<a target="_blank" href="https://www.logitech.com/en-us/products/keyboards/k860-split-ergonomic.920-009166.html">Logitech ERGO K860</a>) and a vertical mouse (<a target="_blank" href="https://www.logitech.com/en-us/products/mice/mx-vertical-ergonomic-mouse.910-005447.html">Logitech MX Vertical</a>), both connected via Bluetooth. As usual, everything works just fine without any configuration in Windows and Linux.</p>
<h3 id="heading-keyboard">Keyboard</h3>
<p>Go to <code>System Settings</code> -&gt; <code>Keyboard</code>.</p>
<p><img src="https://github.com/user-attachments/assets/e9bb1c32-c57c-457c-b2f1-18c0c06e170f" alt="macOS keyboard system settings" class="image--center mx-auto" /></p>
<p>Some combination of keys was constantly changing my input source so I changed following option:</p>
<blockquote>
<p><code>Press fn key to</code> -&gt; <code>Do nothing</code></p>
</blockquote>
<p>In my case, I configured the Spanish input source during system installation, but if I hadn't, I would had to add it as follows:</p>
<blockquote>
<p><code>Text Input</code> -&gt; <code>Input sources</code> -&gt; <code>Edit</code> -&gt; At the bottom left, click on <code>+</code> -&gt; Search for Spanish -&gt; <code>Add</code></p>
</blockquote>
<p><img src="https://github.com/user-attachments/assets/888b55d5-36fb-4807-8098-bdce60bb4952" alt="Add new input source in macOS" class="image--center mx-auto" /></p>
<p>A few changes were made to the shortcuts:</p>
<blockquote>
<p><code>Keyboard Shortcuts</code> -&gt; <code>Missing Control</code> -&gt; Unfold "Mission Control"</p>
<p>Uncheck "Move left a space" and "Move right a space"</p>
</blockquote>
<p>Those options are tied by default to <code>ctrl</code> key, but I prefer to use that key combination for navigation.</p>
<p><img src="https://github.com/user-attachments/assets/b1efdb4e-ac9b-4cdb-81ed-4db36f053a4f" alt="Keyboard missing control options" class="image--center mx-auto" /></p>
<blockquote>
<p><code>Keyboard Shortcuts</code> -&gt; <code>Input Sources</code></p>
<p>Uncheck "Select the previous input source" and "Select next source in Input menu"</p>
</blockquote>
<p><img src="https://github.com/user-attachments/assets/ce8a78a4-2526-4aa6-817a-4cf14159719c" alt="Keyboard input sources options" class="image--center mx-auto" /></p>
<blockquote>
<p><code>Keyboard Shortcuts</code> -&gt; <code>Function Keys</code> -&gt; Enable "Use F1, F2, etc. keys as standard function keys"</p>
</blockquote>
<p><img src="https://github.com/user-attachments/assets/f4e4641c-cf2b-4591-869f-b7e25575fce9" alt="Keyboard function keys" class="image--center mx-auto" /></p>
<p>How that behaviour is disabled by default simply beats me.</p>
<h3 id="heading-mouse">Mouse</h3>
<p>Go to <code>System Settings</code> -&gt; <code>Mouse</code> -&gt; Disable "Natural scrolling"</p>
<p><img src="https://github.com/user-attachments/assets/1cdd4a2e-2356-49b9-a032-dc58c71a37ba" alt="macOS mouse system settings" /></p>
<p>The default behavior doesn't feel natural at all to me. For that reason, I prefer the so-called <a target="_blank" href="https://bootcamp.uxdesign.cc/natural-scrolling-mac-vs-reverse-scrolling-windows-e48656275081">reverse scrolling</a>.</p>
<h3 id="heading-karabiner-elements">Karabiner-Elements</h3>
<p>This <a target="_blank" href="https://karabiner-elements.pqrs.org/">great application</a> allows to easily remap keys and shortcuts. Check the <a target="_blank" href="https://karabiner-elements.pqrs.org/docs/getting-started/features/">list of features</a> if you are interested.</p>
<p>I added a <a target="_blank" href="https://karabiner-elements.pqrs.org/docs/manual/operation/profiles/">custom profile</a>, so I can apply my configuration whenever I want.</p>
<p><img src="https://github.com/user-attachments/assets/7517bfd7-fb51-45ce-8c47-f00d137daf4f" alt="Karabiner-Elements app custom profile" class="image--center mx-auto" /></p>
<p>I had to enable <strong>modify events</strong> for both the keyboard and the mouse. Otherwise the application won't be able to do the remapping properly.</p>
<p><img src="https://github.com/user-attachments/assets/f4064bcb-c572-43c9-a5e9-a35883cd5083" alt="Karabiner-Elements app devices" class="image--center mx-auto" /></p>
<p><a target="_blank" href="https://karabiner-elements.pqrs.org/docs/manual/configuration/configure-simple-modifications/">Simple modifications</a> allow to interchange keys.</p>
<p><img src="https://github.com/user-attachments/assets/54032689-fd24-45f4-ac9d-95caf4db54c2" alt="Karabiner-Elements app simple modifications" class="image--center mx-auto" /></p>
<p>I only apply changes to the desired device, in this case to the external keyboard.</p>
<p>Important note:</p>
<blockquote>
<p>System modifier keys configuration in <code>System Settings</code> -&gt; <code>Keyboard</code> -&gt; <code>Keyboard Shortcuts</code> -&gt; <code>Modifier Keys</code> is ignored when you use Karabiner-Elements.</p>
</blockquote>
<p><a target="_blank" href="https://karabiner-elements.pqrs.org/docs/manual/configuration/configure-complex-modifications/">Complex modifications</a> allow to create rules to make some shortcuts available under certain conditions.</p>
<p><img src="https://github.com/user-attachments/assets/3bd72011-793d-43f7-b47e-39c0206e0051" alt="Karabiner-Elements app complex modifications" class="image--center mx-auto" /></p>
<p>Initially I used some <a target="_blank" href="https://ke-complex-modifications.pqrs.org/">predefined rules</a> maintained by the community, but once I started getting how they work, I started creating my own rules.</p>
<p><img src="https://github.com/user-attachments/assets/847ae3d2-8a38-4ac6-b008-7a70e38349ae" alt="Karabiner-Elements app custom rules" class="image--center mx-auto" /></p>
<p>The <a target="_blank" href="https://karabiner-elements.pqrs.org/docs/manual/operation/eventviewer/">event viewer</a> comes in handy to confirm the name of each key and mouse button.</p>
<p>You can see the whole configuration on <a target="_blank" href="https://gist.github.com/backpackerhh/2448998967f178f0114de6c6a3eb37df">this gist</a>.</p>
<p>Some modifications are only applied to specific applications, such as the terminal or the browser, and others are applied to all applications except the ones specified, usually VSCode.</p>
<p><img src="https://github.com/user-attachments/assets/301f42ad-2805-464e-95a9-2b56c7d07ab7" alt="Karabiner-EventViewer app" class="image--center mx-auto" /></p>
<h3 id="heading-terminal">Terminal</h3>
<p>The first thing I did was replacing the default terminal with <a target="_blank" href="https://iterm2.com/">iTerm2</a>.</p>
<p>I added a custom profile in <code>Settings</code> -&gt; <code>Profiles</code> -&gt; At the bottom left, click on <code>+</code>.</p>
<p><img src="https://github.com/user-attachments/assets/d36329fb-2da6-4dbb-a278-cbf42c6e9a80" alt="iTerm2 app custom profile" class="image--center mx-auto" /></p>
<p>Add the desired name and set it as default profile clicking on "Other Actions...", placed at the bottom.</p>
<p>The shell I use is <a target="_blank" href="https://en.wikipedia.org/wiki/Z_shell">ZSH</a>, that is configured through <a target="_blank" href="https://github.com/backpackerhh/dotfiles">my dotfiles</a>.</p>
<p><img src="https://github.com/user-attachments/assets/6020f218-c065-42b0-9996-259d1a068e67" alt="iTerm2 app customized" class="image--center mx-auto" /></p>
<p>The shortcuts I use on this terminal are defined in <a class="post-section-overview" href="#karabiner-elements">Karabiner-Elements</a>, except for the command that allows to recover the last argument provided to the previous command executed, <code>alt+.</code></p>
<p>I couldn't replicate that behavior with that application, so I had to find an alternative. For that I followed the instructions in the <a target="_blank" href="https://stackoverflow.com/a/68051768/1477964">following comment</a> on StackOverflow.</p>
<p><img src="https://github.com/user-attachments/assets/67517958-6a0b-4c25-abb6-a1ef6253a34f" alt="iTerm2 app custom key mapping" class="image--center mx-auto" /></p>
<p>You can see the whole configuration on <a target="_blank" href="https://gist.github.com/backpackerhh/c52965a4df140ef3abef7b9f827c514c">this gist</a>.</p>
<p>Another command I use a lot in the terminal in Linux is <code>ctrl+u</code> that clears the entire current line from the cursor to the beginning of the line. The closest command I found in macOS with that purpose is <code>ctrl+w</code>, cleaning each word from the cursor to the beginning of the line. To get the same result you have to press the same shortcut multiple times. The remapping was made with Karabiner-Elements, of course.</p>
<p>I plan to add all the configuration of this terminal to my dotfiles so I can easily replicate it anywhere I need it.</p>
<h3 id="heading-vscode">VSCode</h3>
<p>The editor (or IDE) is the application where I spend most of my time everyday, so this is the main source of my frustration working with macOS.</p>
<p>I reused my configuration for VSCodium in Linux, stored on <a target="_blank" href="https://github.com/backpackerhh/dotfiles">my dotfiles</a>.</p>
<p>However I had to change the font size following the instructions on <a target="_blank" href="https://stackoverflow.com/a/55324691/1477964">this comment</a>, because everything looks smaller in macOS.</p>
<p>Open the file that contains user configuration. You can access to that file in <code>~/Library/Application Support/Code/User/settings.json</code> or pressing <code>f1</code> and selecting "Open User Settings (JSON)".</p>
<p>The relevant configuration is as follows:</p>
<pre><code>{
  <span class="hljs-string">"editor.fontSize"</span>: <span class="hljs-number">13</span>,
  <span class="hljs-string">"terminal.integrated.fontSize"</span>: <span class="hljs-number">13</span>,
  <span class="hljs-string">"window.zoomLevel"</span>: <span class="hljs-number">0.6</span>
}
</code></pre><p>After that, having all <a target="_blank" href="https://code.visualstudio.com/docs/getstarted/keybindings">keyboard shortcuts</a> configured the way I wanted would have been a daunting task if it were not for <a target="_blank" href="https://github.com/codebling/vs-code-default-keybindings">a wonderful project</a> on GitHub that takes care of people like me.</p>
<p>Open the file that contains keyboard shortcuts. You can access to that file in <code>~/Library/Application Support/Code/User/keybindings.json</code> or pressing <code>f1</code> and selecting "Open Keyboard Shortcuts (JSON)".</p>
<p>At the top of that file I have all the custom shortcuts included in my dotfiles. Right below I pasted the <a target="_blank" href="https://raw.githubusercontent.com/codebling/vs-code-default-keybindings/master/macos.negative.keybindings.json">negative shortcuts from macOS</a> to remove all those shortcuts. Then I pasted the <a target="_blank" href="https://raw.githubusercontent.com/codebling/vs-code-default-keybindings/master/linux.keybindings.json">shortcuts from Linux</a>.</p>
<p>In case of doubt, you can find more details about how to proceed in the <a target="_blank" href="https://github.com/codebling/vs-code-default-keybindings#how-to-use-these-files">README</a> of the project.</p>
<p>I couldn't directly translate all shortcuts from Linux to macOS, so I had to remove some of them from the Linux section, customize them properly, and place them at the top of the file.</p>
<p>Besides, I wanted to remove other shortcuts that I don't need, so I added them right between the customized shortcuts and the negative shortcuts from macOS.</p>
<p>You can see the whole configuration on <a target="_blank" href="https://gist.github.com/backpackerhh/a1af146ed88335ac5b7b16e21b1c4d8a">this gist</a>.</p>
<p>It's worth to mention that in Karabiner-Elements I was remapping some shortcuts from <code>ctrl</code> to <code>cmd</code>, so I was unable to properly configure some shortcuts because VSCode was receiving <code>cmd</code> when certain shortcuts expected <code>ctrl</code>. I became aware of the issue thanks to the <a target="_blank" href="https://code.visualstudio.com/docs/getstarted/keybindings">Record keys</a> functionality. To avoid that I check in every custom rule that the modification does not affect VSCode.</p>
<h3 id="heading-finder">Finder</h3>
<p>IMHO probably the worst application in the whole macOS ecosystem. It goes hand in hand with <strong>Preview</strong> for such dubious honor.</p>
<p>When you press <code>enter</code> on a selected directory or file, then you can rename that element. Why not?</p>
<p>I made some changes in its settings:</p>
<ul>
<li>Include the home directory in the sidebar</li>
<li>Show all filename extensions</li>
<li>Customize the toolbar (search, create new directory, move to trash, how to show items, item grouping, back/forward buttons)</li>
</ul>
<p>Besides, some modifications made in Karabiner-Elements are focused on Finder:</p>
<ul>
<li><code>enter</code> to open a directory or file</li>
<li><code>f2</code> to rename a directory or file</li>
<li><code>ctrl+h</code> to show/hide hidden files</li>
<li><code>delete_forward</code> to move to trash a directory or file</li>
<li><code>delete_or_backspace</code> to go back to the parent directory</li>
</ul>
<p>Other generic modifications, such as <code>ctrl+c</code> and <code>ctrl+v</code>, are applied to Finder as well.</p>
<h3 id="heading-other-configurations">Other configurations</h3>
<p>One of the shortcuts I use the most is <code>alt+tab</code> to navigate between all the open programs. The simplest solution to get that shortcut would be remapping it to <code>cmd+tab</code>, but I still couldn't see all the instances of any given program. At least by default.</p>
<p>That's something else to add to the never ending list of things I will never understand about macOS.</p>
<p>For that reason, I'm using <a target="_blank" href="https://alt-tab-macos.netlify.app/">AltTab</a> application, that does exactly what I need, and even more. It allows a lot of customization to get the desired result.</p>
<p><img src="https://github.com/user-attachments/assets/b9724c54-8104-4360-aa7e-9165e5ee2569" alt="AltTab app" class="image--center mx-auto" /></p>
<p>I also move and resize windows constantly using shortcuts, so I've installed <a target="_blank" href="https://rectangleapp.com/">Rectangle</a> app. Very nice and simple.</p>
<p><img src="https://github.com/user-attachments/assets/f0b416b4-a8e3-4bb2-ae72-7e84594b7a59" alt="Rectangle app" class="image--center mx-auto" /></p>
<p>Another essential application for me is the browser. In the last few years I have been using <a target="_blank" href="https://www.google.com/intl/en_us/chrome/">Google Chrome</a> mainly, so I had to configure the shortcuts to which I am more used to. Again, some modifications made in Karabiner-Elements are focused on the browser:</p>
<ul>
<li><code>ctrl+l</code> to put the focus on the address bar</li>
<li><code>ctrl+d</code> to save a bookmark</li>
<li><code>ctrl+shift+t</code> to reopen a tab</li>
<li>...</li>
</ul>
<p>The work is far from done yet. Among other things, I'd like to be able to do the following with shortcuts:</p>
<ul>
<li>Open a image and navigate between all images in the same directory with arrow keys</li>
<li>Close an image with <code>escape</code></li>
<li>Display a dialog that would allow to suspend/sleep, restart or shutdown the computer</li>
<li>Select a directory or file and get the options available, similar to what you get with a right click of the mouse (or a button like in Linux)</li>
<li>Navigate between spaces</li>
<li>Easily move applications to different spaces</li>
</ul>
<p>Last but not least, I have to update my dotfiles to include all this configuration.</p>
<hr />
<p>Bear in mind that here I'm sharing my opinion and experience with macOS from the <a target="_blank" href="https://dictionary.cambridge.org/dictionary/english/pov">POV</a> of a software developer that has been working and using Linux for most of my life. An operating system that is not as fancy as macOS is, but one that I consider way better and simpler to use. <a target="_blank" href="https://dictionary.cambridge.org/dictionary/english/ymmv">YMMV</a>.</p>
<p>In any case, right now I have to work with macOS and hopefully I'm going to keep improving my UX to be a little bit happier every day (or at least less unhappy).</p>
<p>Are you an advanced macOS user and know another easier way to do what I'm doing? If so, please, share it because I'm quite interested in improving what I'm doing.</p>
<p>Thank you for reading and see you in the next one!</p>
<hr />
<div class="hn-embed-widget" id="buy-me-a-coffee"></div>]]></content:encoded></item></channel></rss>