Skip to content

AntonKluge/gen-dev-25

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

96 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

GenDev 25 - TariffScout ๐Ÿ”

A fast, reliable, and user-friendly tariff comparison platform built for the Check24 GenDev 2025 coding challenge. A live demo is here available

image

๐ŸŽฏ What is TariffScout?

TariffScout lets you find available internet tariffs from different providers in your area, filter, sort and compare them. If you found a suitable selection, you can send them to your friends or family. Features like address autocompletion, a pretty and responsive UI and search histories make it a joy to use.

โœจ Key Features

  • โšก Real-time Comparisons: Fast parallel API calls with caching
  • ๐Ÿ›ก๏ธ Fault Tolerance: Graceful handling of provider API failures
  • ๐ŸŽจ Modern UI/UX: Responsive design with smooth animations
  • ๐Ÿ“Š Smart Filtering: Advanced filtering and sorting capabilities
  • ๐Ÿ“ฑ Mobile-first: Optimized for all screen sizes
  • ๐Ÿ”— Shareable Links: Generate and share comparison links
  • ๐Ÿ” Search History: Save and revisit previous searches

๐Ÿ—๏ธ Architecture

System Overview

The system architecture of TariffScout is split into frontend and backend logic, where the frontend is built with Next.js and the backend is implemented in Scala 3 using the Cats Effect library. The architecture is designed to be modular, scalable, and maintainable, allowing for easy integration of new providers and features.

      โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” 
      โ”‚      User       โ”‚
      โ”‚ (Desktop / Web) โ”‚ 
      โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ 
        โ”‚             |
        โ–ผ             โ–ผ             
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚  Frontend   โ”‚โ—„โ”€โ”€โ–บโ”‚  Backend API  โ”‚โ—„โ”€โ”€โ–บโ”‚  External APIs  โ”‚
โ”‚  (Next.js   โ”‚    โ”‚  (Scala/Cats) โ”‚    โ”‚   (Providers)   โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
       โ”‚                   โ”‚                      
       โ–ผ                   โ–ผ                      
โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”    โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚   Photon    โ”‚    โ”‚ PostgreSQL    โ”‚  
โ”‚ (Adresses)  โ”‚    โ”‚ (Persistence) โ”‚   
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜    

Tech Stack

Frontend:

  • โš›๏ธ Next.js 15 - Allows for server-side rendering and static site generation.
  • ๐ŸŽจ Tailwind CSS - Makes it easier to style components directly and see on first glance how they look.
  • ๐Ÿ“ฆ Zustand - State management for easier frontend logic.
  • ๐Ÿ“ TypeScript - Type safety and developer experience

Backend:

  • ๐Ÿ”ง Scala 3 - Type-safe and functional programming, perfect for large, performant backends while keeping it easy to reason over program flows and architecture.
  • ๐Ÿฑ Cats Effect - Functional effects for lightweight asynchronous programming, perfect for fetching multiple offers simultaneously.
  • ๐ŸŽญ http4s - Http server build on top of cats effect, bringing automatic parsing and minimal boilerplate.
  • ๐Ÿ“ฆ Slick - Type-safe database access, allowing us to write queries in Scala without losing type safety
  • ๐ŸŒŠ FS2 - Functional streams for processing data in a composable way, allowing us to handle large datasets efficiently.

๐Ÿง  Design Philosophy & Technical Decisions

Why Scala 3 for the Backend

The decision to use Scala 3 instead of more conventional choices like Java Spring Boot or Node.js was deliberate:

Type Safety at Compile Time: The tariff comparison domain involves complex data transformations and integration with multiple external APIs. Scala's advanced type system catches entire classes of runtime errors at compile time. For instance, our ConnectionType enumeration ensures that we can never accidentally assign an invalid connection type to an offer, preventing data corruption that could mislead users about their internet options.

Functional Error Handling: External provider APIs are unreliable by nature - they timeout, return malformed data, or go offline entirely. Scala's Either types and cats-effect's IO monad allow us to compose error-prone operations elegantly. Instead of scattered try-catch blocks, we have a clean pipeline where errors are handled consistently across all provider integrations.

Lightweight Concurrency Without Complexity: Tariff comparison requires calling multiple provider APIs simultaneously. Scala's cats-effect provides structured concurrency that's both performant and easy to reason about. Our provider calls execute in parallel automatically, but if one fails, it doesn't crash the entire operation. Additionally, with cats effect we have a green thread model, meaning we can handle thousands of concurrent requests without blocking threads, which is crucial for a responsive user experience.

Structure of the backend

Extensible Architecture: The backend is designed to be modular, with clear separation of concerns. We have a Service layer that handles business logic, a Repository layer for database interactions, and a Provider layer for external API integrations. This structure allows us to easily add new providers or change existing ones without affecting the overall system. New features can be implemented by simply implementing the corresponding interfaces, such as ProviderAdapter and adding it to the corresponding services.

Repository Pattern: We use the repository pattern to abstract database interactions. This allows us to switch between different database implementations (e.g., PostgreSQL, DuckDB) or even holding data in memory without persistence or other services like redis. Some repositories rely on each other, like the SharedRepository, which loads elements from the OfferRepository. The API and service layer build on purely the interfaces defined in the repositories, allowing us to easily swap out implementations or mock them for testing.

Streaming Data Processing: The backend uses FS2 streams to handle the data returned from providers. Starting in the provider, which each returns a stream of Either[Throwable, Offer], where each element is computed by an IO effect. This allows us to just define the fetch logic as an effect, and the stream will handle execution, merging and error handling automatically. The streams are then merged, filtered, cached and further processed in the service layer and finally decoded to nd-json format and streamed to the frontend.

Filtering and Sorting: The backend offers the necessary routes to implement filtering in the backend, but they are not implemented. It was a conscious design decision to keep the filtering and sorting logic in the frontend. Most importantly, this allows us to only call the API once, and not every time the user changes a filter or sorting. On the other hand, some providers offer to directly pass filters with the API call, potentially reducing the load on the provider if the user filters first. Considering the provided filter options (currently connection type and installation service provided), these are not options which a potential user would set directly, but rather play around with the filtering options. If in the future offers options to filter for e.g. "Offers for young people", the filter logic could be partially moved to the backend, allowing us to reduce the number of offers returned by the providers.

๐ŸŒ Photon

What is Photon: We use Photon for the address autocompletion. Photon is an open source address encoder working with OpenStreetMap data and elasticsearch for fast lookups. It provides a REST API that allows us to query addresses based on user input, returning results in a structured format. Photon is run as a separate Docker container, which can be easily integrated into the development environment using Docker Compose.

Why Photon: Photon is a lightweight, fast, and easy-to-use solution for address autocompletion. It provides a simple REST API that can be easily integrated into our Next.js frontend. Additionally, it is open source and can be run locally or in the cloud, making it a flexible choice for our needs. It is also data privacy friendly as it is self hosted and does not require any external APIs like Google Maps, which would require sending user data to a third party. The integration with Photon allows us to provide a smooth user experience when entering addresses, with real-time suggestions.

Frontend Architecture

The frontend is mainly composed of two pages, the start page and the results page. The start page holds next to some static content, the address input field. The field is connected to the Photon API via Next.js server actions, thus not directly exposing the API. Each input by the user is debounced and sent to the Photon API, which returns a list of matching addresses. The user can then select one of the addresses. When it is a full address, i.e. street, house number, zip code and city, the user is redirected to the results page, where the backend is queried for available offers in the area. The results page shows a list of offers, which can be filtered and sorted by the user. The filtering and sorting is done in the frontend, thus the API is only called again when the page is reloaded or the user triggers a refresh. The results page also allows the user to share the current selection with others, by generating a shareable link that can be copied to the clipboard or shared via social media. To create a share link, the frontend sends all offer ids of the currently selected offers, as well as the filters to the backend where they are saved, and a share link is returned. This allows to not only share offers, but also the corresponding filters and addresses.

๐Ÿ›ก๏ธ Reliability & Error Handling

Fault Tolerance Strategy

  • Tolerant API Calls: Use of circuit breakers to handle provider failures, the FS2 streams all contain Either[Throwable, Offer] and filters out the errors.
  • Retry Logic: Exponential backoff for transient failures, with random backoff to avoid simultaneous retries with multiple failing threads.
  • Caching: Short time caching of successful responses to reduce load on providers, can be configured and turned off entirely in the backend.

User Experience During Failures

  • Progressive Loading: Show available results immediately
  • Shows progress indicators: For long-running operations
  • Shows available offers: Even if some providers fail, users can still see partial results

๐Ÿ› ๏ธ Local Development

Prerequisites

  • Node.js 18+
  • Docker & Docker Compose
  • Java 11+ (for Scala backend)
  • sbt (Scala Build Tool)

Quick Start

# Clone the repository
git clone https://github.com/AntonKluge/gen-dev-25.git
cd gen-dev-25

Now modify the docker-compose.yaml file to match the address of your backend, i.e. http://localhost:8080. Then you can start the services using Docker Compose:

```bash
# Start all services with Docker Compose
docker-compose up -d

# Frontend will be available at http://localhost:3000
# Backend API at http://localhost:8080

โš ๏ธ Attention: Photon will need some time to download all relevant data, so please be patient. You can check the logs of the photon container using docker compose logs -f photon to see when it is ready.

Development Setup

To set up the development environment, you need to run both the backend and frontend services. First, we need to create or modify the .env file in the frontend directory to include the fitting backend URL. If you are developing on localhost, you can use the following .env file:

NEXT_PUBLIC_BACKEND_API_URL=http://localhost:8080
PHOTON_API_URL=http://localhost:2322/api

It is important that photon is also running on localhost. Please have a look at the docker-compose.yaml file, for more information on how to run it, or just run docker compose up -d photon, after you added the ports back, to start it in the background. Photon takes a while to download all relevant data, so please be patient.

Additionally we need a PostgreSQL database running. You can use the provided docker-compose.yaml file to start it, or run it manually. If you want to use the docker-compose file, you can run: docker compose up -d postgres, after you added the ports back to the docker-compose.yaml file and modified backend/src/main/scala/repository/clients/PostgresClient.scala and change the database URL to jdbc:postgresql://localhost:5432/gendev. (Normally we would do this with the application.conf file, but that is somehow not working right now.)

Then, you can run the backend and frontend services in separate terminals:

# Backend setup
cd backend
sbt run

# Frontend setup
cd frontend
npm install
npm run dev

Running Tests

The backend tests can be directly run using sbt:

cd backend
sbt test

๐Ÿ”ฎ Future Enhancements

  • Metrics using open telemetry.
  • AI chat, allowing the user to find their new internet provider by chatting with an AI.
  • Switch to DuckDB for better day to day performance of an in-memory database.
  • Remove non shared offers after a certain time, to keep the database clean.

๐Ÿ“ Development Process

Code Quality Standards

  • pre-commit The project ships with a pre-commit config.
  • Testing: The backend is thoroughly tested, with black box API tests and more detailed tests on the service layer.
  • Linting: The frontend uses ESLint and Prettier for code quality and formatting. The backend uses scalafmt for formatting and scalafix for linting as well as scapegoat for static code analysis.

About

My submission for the GenDev 2025 coding challenge: TariffScout!

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published