← Back to list

I Built a Distributed Image Backend in Dart to Understand System Design

I understood concepts like horizontal scaling and load balancing theoretically.

Waleed Qamar · 2026-05-25 07:16 · 0 claps · 4.3 min read
#flutter #dart #dart-programming-language #design-systems #backend
Open on Medium ↗
Wiki topics: PRD · Product Design 💻 · Programming 🌐 · Web Development 📱 · Mobile Development

I Built a Distributed Image Backend in Dart to Understand System Design

I understood concepts like horizontal scaling and load balancing theoretically.

But I never truly felt why modern systems rely so heavily on distributed architecture.

That changed when I built a mini distributed image backend in Dart.

I started with a single server handling image requests. Under load, CPU usage shot above 90%, response times became unstable, and the system quickly started struggling.

Then I introduced a simple round-robin load balancer and distributed traffic across multiple backend servers.

The difference was immediate.

CPU usage dropped dramatically, traffic became smoother, and for the first time, concepts like horizontal scaling and caching stopped feeling like textbook definitions and started feeling real.

In this article, I’ll walk through how I built:

  • a mini image backend in Dart
  • a custom load balancer
  • an in-memory cache system
  • and a small distributed architecture to understand system design practically

Most system design experiments online use Python, Node.js, or Go.

I intentionally chose Dart.

As a Flutter developer, I wanted to understand backend architecture and distributed systems using the same ecosystem I already work with for app development.

It also forced me to learn networking, async programming, and backend fundamentals at a much deeper level than typical Flutter applications require.

The main goal of the image backend is to store the uploaded image after compressing it. For this project, I used Dart packages shelf, shelf router, mime, and image.

I structured the files in a way that made it easier to scale.

Firstly, let's run the single server and flood traffic to it, and check the metrics

  router.get('/all-images', (Request request) {
    final images = <Map<String, dynamic>>[];

    for (final entity in uploadDir.listSync()) {
      if (entity is File) {
        final filename = p.basename(entity.path);

        final cached = cache.get(filename);

        if (cached != null) {
          images.add(cached);
        } else {
          final imageData = {
            'filename': filename,
            'url': 'http://127.0.0.1:8080/uploads/$filename',
          };

          cache.set(filename, imageData);

          images.add(imageData);
        }
      }
    }

    return Response.ok(
      jsonEncode({
        'success': true,
        'cacheHit': cache.cacheHit,
        'cacheMiss': cache.cacheMiss,
        'totalImages': images.length,
        'images': images,
      }),
      headers: {'Content-Type': 'application/json'},
    );
  });

  // =========================
  // UPLOAD IMAGE
  // =========================
  router.post('/upload-image', (Request request) async {
    final contentType = request.headers['content-type'];

    if (contentType == null || !contentType.contains('multipart/form-data')) {
      return Response(
        400,
        body: jsonEncode({
          'success': false,
          'message': 'Request must be multipart/form-data',
        }),
        headers: {'Content-Type': 'application/json'},
      );
    }

    // Extract boundary
    final boundary = contentType.split('boundary=').last;

    // Create multipart transformer
    final transformer = MimeMultipartTransformer(boundary);

    // Parse incoming request stream
    final parts = transformer.bind(request.read());

    String? savedFilename;

    await for (final part in parts) {
      final headers = part.headers;

      final disposition = headers['content-disposition'];

      if (disposition == null) continue;

      // Extract filename
      final filenameRegex = RegExp(r'filename="(.+)"');
      final match = filenameRegex.firstMatch(disposition);

      if (match == null) continue;

      final originalFilename = p.basename(match.group(1)!);

      final timestamp = DateTime.now().millisecondsSinceEpoch;

      final filename = '${timestamp}_$originalFilename';

      final file = File(p.join(uploadDir.path, filename));

      final sink = file.openWrite();

      // Save uploaded file
      await part.pipe(sink);

      await sink.close();

      // Compress image
      compressImage(file.path);

      final imageData = {
        'filename': filename,
        'url': 'http://127.0.0.1:8080/uploads/$filename',
      };

      // Cache image
      cache.set(filename, imageData);

      savedFilename = filename;
    }

    if (savedFilename == null) {
      return Response(
        400,
        body: jsonEncode({'success': false, 'message': 'No image uploaded'}),
        headers: {'Content-Type': 'application/json'},
      );
    }

    return Response.ok(
      jsonEncode({
        'success': true,
        'filename': savedFilename,
        'url': 'http://127.0.0.1:8080/uploads/$savedFilename',
      }),
      headers: {'Content-Type': 'application/json'},
    );
  });

  router.mount(
    '/uploads/',
    shelf_static.createStaticHandler(uploadDir.path, defaultDocument: null),
  );

  final handler = Pipeline()
      .addMiddleware(corsHeaders())
      .addMiddleware(logRequests())
      .addHandler(router.call);

  await serve(handler, InternetAddress.loopbackIPv4, 8080);

  print('Server running on http://127.0.0.1:8080');

This server.dart file returns all the images in the uploads folder. I used Locust (a Python library) for simulating traffic to the server, and I simultaneously monitored the CPU and RAM usage.

Locust Test Configuration

Locust Test Configuration

CPU and RAM usage spiked upto 90%

In Python, increasing concurrency with Uvicorn workers feels almost effortless:

uvicorn app:app --workers 4

But while rebuilding the same ideas in Dart, I ended up manually running multiple backend instances and distributing traffic through a custom load balancer.

Technically, this moved beyond simple vertical scaling and into horizontal scaling architecture.

Surprisingly, implementing these systems manually helped me understand distributed systems concepts far more deeply than simply increasing worker count.

For horozontal scaling, I ran multiple independent servers on different ports and wrote a custom loab balancer to balance the traffic among these servers using Round Robin method.

Load Balancer:

import 'dart:convert';
import 'dart:io';

final servers = [
  "http://127.0.0.1:8080",
  "http://127.0.0.1:8081",
  "http://127.0.0.1:8082",
];

int current = 0;

String getNextServer() {
  final server = servers[current];
  current = (current + 1) % servers.length;
  return server;
}

void main() async {
  final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 9000);

  print('Load Balancer Running on port 9000');

  await for (HttpRequest request in server) {
    final backend = getNextServer();

    final client = HttpClient();
    final targetUrl = '$backend${request.uri.path}';

    print('Forwarding request to: $targetUrl');
    final backendRequest = await client.getUrl(Uri.parse(targetUrl));
    final backendResponse = await backendRequest.close();

    final responseBody = await utf8.decoder.bind(backendResponse).join();

    request.response
      ..headers.contentType = ContentType.json
      ..write(responseBody);

    await request.response.close();
  }
}

After using the distributed setup, system usage was much optimized and resource utilzation was astronomically better.

TroubleShootings:

First I was using ‘localhost” here:

await serve(handler, InternetAddress.loopbackIPv4, 8080);

Local host by default uses IPV6 address and when communicating with servers, which were running on the IPV4 loopback address, connection was not establishing. Debugging this took me 20 minutes.

Before this project, concepts like load balancing and horizontal scaling felt abstract.

But after building the system myself, debugging networking issues, watching CPU usage collapse after distributing traffic, and observing how caching reduced repeated work, distributed systems stopped feeling theoretical.

They started feeling tangible.

And surprisingly, Dart turned out to be a really interesting way to explore backend architecture beyond Flutter applications.

GITHUB: https://github.com/waleed719/smart_image_backend


메타데이터
post_id
0728cdb6833d
slug
i-built-a-distributed-image-backend-in-dart-to-understand-system-design-0728cdb6833d
url
https://medium.com/@wqamar719/i-built-a-distributed-image-backend-in-dart-to-understand-system-design-0728cdb6833d
canonical_url
https://medium.com/@wqamar719/i-built-a-distributed-image-backend-in-dart-to-understand-system-design-0728cdb6833d
author_url
https://medium.com/@wqamar719
status
ok
fetched_at
2026-06-09 14:34:10