Going Fast with Ruby
The greatest friction in coding lies not in how fast we type, but in how far our thoughts must travel to become code.
The actual bottleneck in software development is between our minds and the code. Development velocity is the speed of forming clear thoughts and translating them into functioning software.
After over a decade of building with Go, Java, and JavaScript, I continually returned to Ruby. While other languages may prioritize simplicity, enterprise stability, or universal access, Ruby makes a different choice: it prioritizes human understanding.
This focus on understanding is evident in the success of our team of three, who built a fintech platform in just 42 days. With me handling the backend and infrastructure alongside a front-end developer and a business developer, we turned our vision into reality faster than anyone expected.
This comes as no surprise to Rubyists. The community has spent decades refining code and the philosophy of collaborative software development. Every gem, pattern, and best practice asks one crucial question: "How can we help teams build better products faster?"
In this article, I'll explain why—contrary to current trends—Ruby remains the undisputed champion of turning ambitious ideas into working products at unprecedented speeds. I'll also share some lessons learned to help maintain this momentum.
The Ruby Advantage in Fintech
In fintech, where every line of code potentially handles someone's money, you might think Ruby's reputation for "developer happiness" would take a backseat to more rigid, type-safe alternatives. But that is not true.
Take, for example, our user onboarding flow. This might require multiple service classes, interfaces, and a complex orchestration layer in Java or Go. In Ruby, we can express the entire business flow in a few readable lines:
def call
ActiveRecord::Base.transaction do
user = find_or_create_user
create_referral(user) if @referee_code
send_sms_with_token(user)
end
end
This isn't just shorter code – it's a precise, executable specification of our business requirements. A new team member can understand our onboarding flow just by reading those three lines. The transaction block ensures data consistency, the conditional referral creation expresses our business logic, and the SMS notification handles user communication without drowning in implementation details.
Let’s look at a section from the test file of this service object:
# ...
context 'when the user does not exist' do
it 'creates a new user' do
described_class.call(phone_number, referee_code)
expect(UserService::Creator).to have_received(:call).with(phone_number)
end
it 'calls the referral service with the correct parameters' do
described_class.call(phone_number, referee_code)
expect(ReferralService::Creator).to have_received(:call).with(referee_code, user.id)
end
it 'calls the token service with the correct parameters' do
described_class.call(phone_number, referee_code)
expect(TokenService::PhoneNumber::Creator).to have_received(:call).with(user)
end
it 'sends an SMS with the token' do
described_class.call(phone_number, referee_code)
expect(SmsService::Sender).to have_received(:call).with(phone_number, message)
end
end
# ...
This is where Ruby challenges conventional wisdom about type safety and compiler checks. While many languages rely on compile-time guarantees, Ruby encourages a different kind of safety through readable, comprehensive tests. There's no hiding behind type checking – your tests must verify behavior, not just interfaces. This isn't a limitation; it's a strength that pushes developers toward TDD or writing tests.
The result? Our test suite serves as both a safety net and living documentation. New team members can understand our system's behavior by reading tests in plain English. We don't need hundreds of lines of test setup or mock configurations – Ruby's elegant syntax and powerful testing tools let us focus on what matters: verifying our business logic works correctly.
The Power of Framework: Rails
"The secret to moving fast isn't finding shortcuts – it's choosing paths where others have already cleared the way."
Rails isn't just a framework; it's a collection of battle-tested patterns that have survived and evolved through countless production deployments. When we chose Rails for our fintech platform, we weren't just picking a technology – we were inheriting years of collective wisdom about building maintainable web applications.
The framework's conventions meant we didn't waste time debating folder structures or naming patterns. Instead, we focused on what made our product unique: the financial logic, security measures, and user experience.
Here are some of the gems that made our project possible, each one saving us countless days of reinventing the wheel while protecting us from potential catastrophes:
Real-time & Background Operations
Sidekiq orchestrates complex background jobs with Redis-backed reliability
Action Cable handles WebSocket connections with simplicity
Quality & Testing
RSpec drives behavior-driven development with readable specs
FactoryBot and Faker generate realistic test data that matches your domain
Rubocop enforces community-standard code style
Performance & Security
Brakeman catches security vulnerabilities before they reach production
Bullet eliminates N+1 queries before they impact performance
Multi-layered caching strategies make things much harder to break
The power of Rails isn't just in what it provides out of the box but in how seamlessly these components work together. Every piece – from Bullet to Sidekiq, RSpec to Action Cable – fits naturally into the framework's convention over configuration philosophy. This integration meant we could focus on building features rather than fighting with infrastructure.
Below is the complete code needed for real-time user updates via WebSockets:
class User < ApplicationRecord
after_update_commit :broadcast_user_update
private
def broadcast_user_update
ActionCable.server.broadcast("user_#{hashed_id}", { event: 'user_updated' })
end
end
class UserChannel < ApplicationCable::Channel
def subscribed
if current_user&.hashed_id == params[:user_id]
stream_from "user_#{params[:user_id]}"
else
reject
end
end
end
The code below isn't a tutorial snippet or a simplified example—it's our entire production caching system using Redis.
def reporting_stats
Rvails.cache.fetch('reporting_stats', expires_in: 1.minute) do
{
user_stats: user_stats,
referral_stats: referral_stats,
conversion_stats: conversion_stats
}
end
end
The Power of Documentation
"Well-architected code is living documentation, but you can't architect well without documenting your thoughts first."
Every feature in our journey began with a simple markdown file. There were no templates or fancy tools—just a README.md in the repository that forced us to think before we typed.
After years of rushed projects, I learned that even with pressing deadlines, starting with pen and paper yields something far more valuable than quick code—clarity.
The process was simple:
# User Onboarding API
Handles new user registration with referral tracking and SMS verification.
## Endpoints
POST /api/v1/xxx
- Creates new user account
- Triggers token SMS
- Processes referral if code provided
Request:
{
"phone_number": "+1234567890",
"referee_code": "XXX-YYY-111" // optional
}
Response:
{
"user_id": "abc123",
"status": "pending_verification"
}
## Business Rules
- Phone numbers must be unique
- Referral codes have a strict format
- SMS tokens expire in 5 minutes
While tests guard your code against bugs, README-driven development protects your architecture from misunderstandings. This practice, combined with our living Postman collection, transformed what could have been 3 AM confusion into 3 PM confidence. Our front-end developer had clarity at their fingertips, and I had the space to dive deep into core challenges.
Embracing Rework Without Fear
"The scariest part of rework isn't the work – it's the first 5 minutes of accepting you need to do it."
This truth hit home hard when, with just one week left until our launch, we discovered a critical limitation in our payment system: it wouldn't process debit cards with how we implemented things. The realization sent a chill down my spine. Rewriting core payment processing logic this close to launch seemed terrifying.
In many ecosystems, this kind of late-stage rework would be a nightmare. But Ruby's approach to abstraction and testing gave us confidence. The key to making this work wasn't just Ruby's elegance – it was how our previous architectural decisions had already established clear boundaries through service objects.
Service Objects: Our payment logic was already encapsulated in service objects, making it easier to add new processors without touching the rest of the codebase.
Clean Interfaces: Ruby's duck typing allowed us to define a consistent interface for different card processors without complex inheritance hierarchies.
Comprehensive Tests: Our existing test suite caught edge cases we might have missed during the rapid changes.
The most important lesson wasn't about the technical implementation but overcoming the psychological barrier to rework.
What I had estimated as days of work dissolved into a five-hour coding sprint. This wasn't luck—it was the power of choosing the right tools.
Outdated Notion: Ruby is Slow
While it's true that Ruby's dynamic typing and interpreted nature leads to lower raw performance on specific benchmarks, this view paints an incomplete picture.
The Ruby ecosystem has significantly improved the language's performance over the years. Projects like TruffleRuby, a high-performance Ruby implementation based on the GraalVM, have also shown that Ruby can be as fast as statically typed languages in certain areas.
I have been building market-making bots in C++ for quite some time, and every iteration takes significant development time. At some point, I gave up and started using Ruby to test new ideas or when implementing a paper as a POC.
I realized that TruffleRuby excels in arithmetic operations, and in particular cases, it can outperform even statically typed languages like Go!
Below is a benchmark of TruffleRuby on arithmetic computations compared to other Ruby implementations:
The nbody benchmark (from CLBG) consists mainly of arithmetic computations and heavily accessing instance variables. JRuby reaches a good speedup of 4.43x. TruffleRuby is about 50x the speed of CRuby 3.1, which is incredible.
This performance boost comes from innovative techniques like just-in-time (JIT) compilation, which allows TruffleRuby to optimize the code at runtime based on the specific workload. As a result, the speed of Ruby is no longer a significant concern for most use cases, especially when you consider the language's unparalleled developer productivity and expressiveness.
It's worth noting that TruffleRuby is compatible with many Ruby gems, including Rails. This further expands the potential use cases for high-performance Ruby in various applications, including performance-sensitive domains like fintech.
Knowing When to Grow
"A great developer knows their code. A great leader knows their strengths - and more importantly, their own limits."
Every project has a deadline, whether explicitly stated or not. A decade of building software teaches you something crucial: the difference between challenging and impossible deadlines isn't in the timeline—it's in understanding the gap between your vision and your current capabilities. Experience helps you spot these gaps early, turning potential bottlenecks into opportunities for strategic decisions.
Our journey illustrates this perfectly. Initially, Erhan Tezakar and I thought we could handle everything as a two-person team—him driving the business while I wrote all the code. With Ruby's productivity boost and our initial momentum, this seemed plausible. We were shipping features rapidly, and our codebases remained stable.
Years of building products teach you to recognize the sweet spot between ambition and reality. The decision was evident when I saw the gap between my front-end work and Erhan's sophisticated UI/UX vision. This wasn't about admitting limitations but knowing when to bring specialized talent to elevate the project.
That same day, we discussed this situation and contacted Melih Ozkalay, the front-end developer. Initially skeptical about our ambitious timeline, he couldn't believe we could build a fintech platform in such a short window. But I knew, with Ruby's productivity and our clear division of responsibilities, it was not just possible—it was achievable. His front-end expertise, my backend focus, and Erhan's product direction created the perfect balance.
Not only did we meet our deadline, but we delivered something that exceeded everyone's expectations—even our own.
The Future is Small Teams
As the AI revolution reshapes the technology landscape, Silicon Valley visionaries like OpenAI CEO Sam Altman predict a profound shift in startups and software development. The AI revolution has already minted dozens of unicorns – startups valued at $1 billion before going public – and now it could create a whole new type of startup: the one-person unicorn.
In this new paradigm, a developer's value is not measured by their ability to churn out code but by their strategic thinking, domain expertise, and collaboration capacity. The future aligns perfectly with Ruby's emphasis on developer productivity. By empowering small, highly collaborative teams to punch above their weight, Ruby can help software ventures to outmaneuver their larger, more established competitors.
As a physicist and scientist by training, I would like to present an utterly unscientific algorithm to calculate decision speed depending on the team size below:
class TeamDynamics
BASE_DECISION_TIME = 15 # minutes for a single person
def decision_velocity(team_size)
# 3 developers -> 3, 10 developers -> 55
channels = team_size * (team_size - 1) / 2
# Small teams (2-5): Nearly linear growth
# Medium teams (6-12): Starts to hurt
# Large teams (13+): Exponential pain
overhead_factor = 1.06 ** channels
# 3 developers: 17 minutes
# 10 developers: 3.4 hours
BASE_DECISION_TIME * overhead_factor
end
end
While the algorithm highlights the exponential challenges of larger teams, the real-world implications can be even more detrimental. As an experienced engineering manager, I've seen firsthand how relying on a select few for decision-making can undermine morale, stifle training, and prevent accurate alignment within the team.
The key is to focus on optimization, automation, and problem-solving before scaling the team. Investing the time to get the processes, tools, and overall alignment right is essential to ensuring that growth doesn't come at the expense of team cohesion and individual empowerment. The future belongs to those who can strike the right balance - leveraging the power of small, collaborative teams while laying the foundation for sustainable, scalable success.
Pairing with LLMs
"AI isn't replacing developers. It's replacing the hours we spend staring at problems we could be discussing."
If LLMs had not been invented, we wouldn’t have been able to build a fintech product in 42 days. Similarly, if we hadn't spent years without them, we wouldn't have been able to do it as well.
When working with large language models, it's essential to approach the interaction as a collaborative effort, not a hierarchical one. The AI is your rubber duck with a Ph.D. in computer science and all the world’s information.
LLMs can propel you forward or lead you astray like any powerful tool. The real skill isn't in using them constantly but in knowing when to lean on them and when to roll up your sleeves and do things the traditional way.
The future tech builders won't just code—they'll orchestrate, combining old-school engineering chops with AI prowess, like jazz masters who blend classical training with improvisation.
Conclusion
In an era where tech stacks grow more complex and teams balloon, our journey demonstrates that small teams with the right tools and clear thinking can achieve remarkable results. Ruby's emphasis on developer happiness isn't just marketing—it's a philosophical stance that translates directly into development velocity. At a time when speed is the ultimate competitive edge, this emphasis on developer happiness becomes more than just a feature—it's a strategic advantage.
By empowering small teams to build quickly, iterate efficiently, and embrace change without fear, Ruby remains the undisputed champion of turning ambitious ideas into reality. Combined with Rails' battle-tested conventions and the amplifying power of modern AI tools, it creates an environment where vision becomes a product at unprecedented speed.
Our 42-day journey building a fintech platform wasn't just about proving Ruby's capabilities—it was about validating a different approach to fintech software development, where clarity of thought and execution speed precede complexity and scale.
If you are a Turkish citizen and want to try our product, here is the link to polira.
Subscribe to my newsletter
Read articles from Mehmet Cetin directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Mehmet Cetin
Mehmet Cetin
As an engineering leader passionate about crafting simple solutions to complex challenges, I bring over a decade of experience building scalable systems and leading diverse teams across Asia and Europe.