A Rails 8 e-commerce app built with Hotwire. Started from a plain Ruby domain model project and evolved into this.
I had this plain Ruby project called CoffeeApp - just domain models, services, tests, no framework. The idea was to practice OO design, SOLID principles, hexagonal architecture, all that good stuff.
Then I thought: what if I take these same models and build an actual web app with Rails 8 and Hotwire? Keep the clean architecture but make it interactive and fast.
So here we are.
It's a coffee shop. You browse coffees, add them to cart, checkout. Pretty standard e-commerce stuff, but with some interesting implementation details:
- Real-time cart updates with Turbo Streams (no full page reloads)
- Payment gateway abstraction so you can swap Stripe/PayPal/whatever without touching business logic
- Dependency injection everywhere for easier testing
- All tests follow Sandi Metz principles from POODR
- Tailwind for UI because I'm not a designer
I still have some work to do on the app, I'm planning to add an Admin panel where I can create some dashboards, trends, and such.
Click "Add to Cart" and watch the cart count update instantly. Adjust quantities and see everything recalculate. Remove items with smooth animations. All without reloading the page - that's Hotwire doing its thing.
The payment system is abstracted. Right now it's using a mock gateway, but swapping it for Stripe is just:
CheckoutService.new(order, payment_gateway: StripeGateway.new)No changes to controllers, views, or business logic. That's the hexagonal architecture paying off.
- Rails 8.0.3
- Ruby 3.4.1
- Hotwire (Turbo + Stimulus)
- Tailwind CSS 4
- PostgreSQL
- RSpec with FactoryBot
git clone git@github.com:lbernardelli/CoffeeShopWeb.git
cd CoffeeShopWeb
bundle install
rails db:create db:migrate db:seed
bundle exec rspec # run tests
bin/dev # start serverThen go to http://localhost:3000
I'm using dependency injection heavily, which makes testing way easier. Instead of mocking ActiveRecord or stubbing class methods, services just accept their dependencies:
# In tests:
let(:payment_gateway) { instance_double(PaymentGateway) }
service = CheckoutService.new(order, payment_gateway: payment_gateway)
# Now you can easily verify behavior:
expect(payment_gateway).to receive(:charge).with(amount: order.grand_total)Check TESTING.md for more details on the approach.
This project shows:
- You can keep clean architecture when moving from plain Ruby to Rails
- Hotwire is really good for this kind of app
- Hexagonal architecture makes swapping implementations trivial
- Dependency injection makes testing way less painful
- You don't need React for a modern UX
The original CoffeeApp was about 500 lines of Ruby. This Rails app adds authentication, views, controllers, Hotwire interactions, and a full test suite while keeping the domain logic clean.