Skip to main content

Core Concepts

Transport Protocols Deep Dive

Understanding how Hypermodern handles multiple transport protocols is crucial to leveraging its full potential. Each protocol serves different use cases while maintaining API consistency.

HTTP/HTTPS Transport

HTTP transport provides familiar REST semantics with additional Hypermodern optimizations.

Key Features:

  • Standard HTTP methods (GET, POST, PUT, DELETE)
  • RESTful URL patterns
  • HTTP status codes for error handling
  • Content negotiation (JSON fallback, binary primary)
  • Caching headers and ETags

Example Request Flow:

POST /users HTTP/1.1
Host: localhost:8080
Content-Type: application/x-hypermodern-binary
Content-Length: 42

[binary payload]

Response:

HTTP/1.1 201 Created
Content-Type: application/x-hypermodern-binary
Content-Length: 78

[binary user object]

When to Use HTTP:

  • Mobile applications (leverages HTTP caching)
  • Web browsers (fallback compatibility)
  • Third-party integrations
  • Load balancer compatibility
  • CDN integration

WebSocket Transport

WebSocket transport enables real-time, bidirectional communication with persistent connections.

Key Features:

  • Persistent connection with automatic reconnection
  • Bidirectional streaming
  • Real-time push notifications
  • Binary message framing
  • Connection lifecycle management

Message Format:

Frame Type: REQUEST (0x01)
Request ID: 12345
Endpoint: "create_user"
Payload: [binary request data]

Streaming Example:

// Server-side streaming
server.registerStreamingEndpoint<WatchUsersRequest, UserUpdate>(
  'watch_users',
  (request) async* {
    await for (final update in userUpdateStream) {
      yield update;
    }
  },
);

// Client-side consumption
await for (final update in client.stream<UserUpdate>('watch_users', {})) {
  print('User updated: ${update.userId}');
}

When to Use WebSocket:

  • Real-time applications (chat, collaboration)
  • Live dashboards and monitoring
  • Gaming applications
  • Streaming data feeds
  • Interactive web applications

TCP Transport

TCP transport provides direct socket communication with minimal overhead.

Key Features:

  • Direct socket connection
  • Custom binary protocol
  • Minimal framing overhead
  • Maximum throughput
  • Connection pooling

Protocol Format:

[4 bytes: message length]
[4 bytes: request ID]
[1 byte: message type]
[N bytes: endpoint name (null-terminated)]
[M bytes: binary payload]

Performance Characteristics:

  • 30-50% less overhead than HTTP
  • No header parsing
  • Optimal for high-frequency operations
  • Best latency characteristics

When to Use TCP:

  • IoT devices with bandwidth constraints
  • High-frequency trading systems
  • Game servers requiring low latency
  • Microservice communication
  • Performance-critical applications

Unified Routing System

The unified routing system is what makes Hypermodern's multi-protocol approach possible. All protocols share the same business logic through a common abstraction layer.

Request Lifecycle

  1. Protocol Handler receives raw request
  2. Request Parser converts to common format
  3. Middleware Pipeline processes request
  4. Router dispatches to endpoint handler
  5. Handler executes business logic
  6. Response Serializer converts response
  7. Protocol Handler sends response
// This handler serves ALL protocols
server.registerEndpoint<GetUserRequest, User>(
  'get_user',
  (request) async {
    // Business logic here
    return await userService.getUser(request.id);
  },
);

Protocol Abstraction

Each protocol handler implements a common interface:

abstract class ProtocolHandler {
  Future<void> handleRequest(
    String endpoint,
    dynamic request,
    ResponseSender sender,
  );
  
  Stream<dynamic> handleStream(
    String endpoint,
    dynamic request,
  );
}

Middleware Integration

Middleware works consistently across all protocols:

class LoggingMiddleware implements Middleware {
  @override
  Future<dynamic> handle(
    dynamic request,
    Future<dynamic> Function(dynamic) next,
  ) async {
    final stopwatch = Stopwatch()..start();
    
    try {
      final response = await next(request);
      print('Request completed in ${stopwatch.elapsedMilliseconds}ms');
      return response;
    } catch (e) {
      print('Request failed: $e');
      rethrow;
    }
  }
}

Type Safety and Code Generation

Type safety is enforced through comprehensive code generation from JSON schemas.

Schema Definition

Schemas define the contract between client and server:

{
  "models": {
    "user": {
      "id": "int64",
      "name": "string",
      "email": "string",
      "profile": "@user_profile?",
      "tags": ["string"],
      "metadata": "map<string, any>"
    },
    "user_profile": {
      "bio": "string?",
      "avatar_url": "string?",
      "social_links": "map<string, string>"
    }
  }
}

Generated Model Classes

The generator creates comprehensive model classes:

class User {
  final int id;
  final String name;
  final String email;
  final UserProfile? profile;
  final List<String> tags;
  final Map<String, dynamic> metadata;
  
  const User({
    required this.id,
    required this.name,
    required this.email,
    this.profile,
    required this.tags,
    required this.metadata,
  });
  
  // JSON serialization
  Map<String, dynamic> toJson() => {
    'id': id,
    'name': name,
    'email': email,
    if (profile != null) 'profile': profile!.toJson(),
    'tags': tags,
    'metadata': metadata,
  };
  
  factory User.fromJson(Map<String, dynamic> json) => User(
    id: json['id'] as int,
    name: json['name'] as String,
    email: json['email'] as String,
    profile: json['profile'] != null 
        ? UserProfile.fromJson(json['profile']) 
        : null,
    tags: (json['tags'] as List).cast<String>(),
    metadata: json['metadata'] as Map<String, dynamic>,
  );
  
  // Binary serialization (optimized)
  Uint8List toBinary() {
    final writer = BinaryWriter();
    writer.writeInt64(id);
    writer.writeString(name);
    writer.writeString(email);
    writer.writeOptional(profile?.toBinary());
    writer.writeStringList(tags);
    writer.writeMap(metadata);
    return writer.toBytes();
  }
  
  factory User.fromBinary(Uint8List data) {
    final reader = BinaryReader(data);
    return User(
      id: reader.readInt64(),
      name: reader.readString(),
      email: reader.readString(),
      profile: reader.readOptional((data) => UserProfile.fromBinary(data)),
      tags: reader.readStringList(),
      metadata: reader.readMap(),
    );
  }
  
  // Equality and hashing
  @override
  bool operator ==(Object other) =>
      identical(this, other) ||
      other is User &&
          runtimeType == other.runtimeType &&
          id == other.id &&
          name == other.name &&
          email == other.email &&
          profile == other.profile &&
          listEquals(tags, other.tags) &&
          mapEquals(metadata, other.metadata);
  
  @override
  int get hashCode => Object.hash(id, name, email, profile, tags, metadata);
  
  // Copy with method
  User copyWith({
    int? id,
    String? name,
    String? email,
    UserProfile? profile,
    List<String>? tags,
    Map<String, dynamic>? metadata,
  }) => User(
    id: id ?? this.id,
    name: name ?? this.name,
    email: email ?? this.email,
    profile: profile ?? this.profile,
    tags: tags ?? this.tags,
    metadata: metadata ?? this.metadata,
  );
}

Type System Features

Nullable Types:

{
  "optional_field": "string?",
  "required_field": "string"
}

Collections:

{
  "string_list": ["string"],
  "user_list": ["@user"],
  "string_map": "map<string, string>",
  "mixed_map": "map<string, any>"
}

References:

{
  "user_ref": "@user",
  "optional_user": "@user?"
}

Primitive Types:

  • string, int32, int64, float32, float64
  • bool, datetime, bytes
  • any (dynamic type)

Binary Serialization

Binary serialization is a key performance feature of Hypermodern, providing significant advantages over JSON.

Performance Benefits

Size Comparison:

// JSON (78 bytes)
{
  "id": 12345,
  "name": "Alice Johnson",
  "email": "alice@example.com",
  "created_at": "2023-10-15T14:30:00Z"
}
// Binary (42 bytes - 46% smaller)
[8 bytes: id as int64]
[13 bytes: name length + UTF-8 data]
[19 bytes: email length + UTF-8 data]
[8 bytes: timestamp as int64]

Speed Comparison:

  • JSON parsing: ~500 MB/s
  • Binary parsing: ~2000 MB/s (4x faster)
  • Memory usage: 60% less allocation

Binary Format Specification

Primitive Types:

int32:    4 bytes, little-endian
int64:    8 bytes, little-endian
float32:  4 bytes, IEEE 754
float64:  8 bytes, IEEE 754
bool:     1 byte (0x00 or 0x01)

Variable Length Types:

string:   [4 bytes length][UTF-8 data]
bytes:    [4 bytes length][raw data]

Collections:

list:     [4 bytes count][items...]
map:      [4 bytes count][key-value pairs...]

Optional Types:

present:  [1 byte: 0x01][value]
null:     [1 byte: 0x00]

Custom Serialization

You can customize serialization for specific types:

class CustomModel {
  // Custom binary serialization
  Uint8List toBinary() {
    final writer = BinaryWriter();
    // Custom logic here
    return writer.toBytes();
  }
  
  factory CustomModel.fromBinary(Uint8List data) {
    final reader = BinaryReader(data);
    // Custom deserialization logic
    return CustomModel(/* ... */);
  }
}

Backward Compatibility

Binary format supports schema evolution:

Field Addition:

  • New optional fields can be added
  • Readers ignore unknown fields
  • Writers include version information

Field Removal:

  • Deprecated fields are ignored
  • Old readers skip unknown data
  • Graceful degradation

Type Changes:

  • Limited type coercion supported
  • Version-specific handling
  • Migration strategies

Connection Management

Hypermodern provides sophisticated connection management across all protocols.

HTTP Connection Pooling

final client = HypermodernClient.http('http://localhost:8080', 
  connectionPoolSize: 10,
  keepAliveTimeout: Duration(seconds: 30),
  maxRequestsPerConnection: 100,
);

WebSocket Lifecycle

final client = HypermodernClient('ws://localhost:8082');

// Connection events
client.onConnected.listen(() => print('Connected'));
client.onDisconnected.listen(() => print('Disconnected'));
client.onError.listen((error) => print('Error: $error'));

// Automatic reconnection
await client.connect(
  reconnectAttempts: 5,
  reconnectDelay: Duration(seconds: 2),
);

TCP Connection Management

final client = HypermodernClient.tcp('localhost:8081',
  connectionTimeout: Duration(seconds: 5),
  keepAlive: true,
  keepAliveInterval: Duration(seconds: 30),
);

Error Handling

Consistent error handling across protocols:

try {
  final user = await client.request<User>('get_user', request);
} on NotFoundException catch (e) {
  // Handle 404-equivalent errors
} on ValidationException catch (e) {
  // Handle validation errors
} on NetworkException catch (e) {
  // Handle network-level errors
} on TimeoutException catch (e) {
  // Handle timeout errors
}

Performance Characteristics

Understanding the performance implications of each transport helps you make informed decisions.

Latency Comparison

Single Request Latency (localhost):

  • TCP: ~0.1ms
  • WebSocket: ~0.2ms
  • HTTP: ~0.5ms

Network Latency (100ms RTT):

  • TCP: ~100.1ms
  • WebSocket: ~100.2ms
  • HTTP: ~100.5ms

Throughput Comparison

Requests per Second (single connection):

  • TCP: ~50,000 RPS
  • WebSocket: ~30,000 RPS
  • HTTP: ~10,000 RPS

Concurrent Connections:

  • TCP: Limited by file descriptors
  • WebSocket: ~10,000 per server
  • HTTP: ~1,000 per server (with connection pooling)

Memory Usage

Per Connection Memory:

  • TCP: ~2KB
  • WebSocket: ~4KB
  • HTTP: ~8KB (with keep-alive)

Choosing the Right Protocol

Use TCP when:

  • Maximum performance is critical
  • You control both client and server
  • Network conditions are stable
  • You need custom protocol features

Use WebSocket when:

  • You need real-time bidirectional communication
  • Browser compatibility is important
  • You want streaming capabilities
  • Connection persistence is valuable

Use HTTP when:

  • You need maximum compatibility
  • Caching is important
  • You're integrating with existing systems
  • Stateless communication is preferred

What's Next

Now that you understand Hypermodern's core concepts, you're ready to dive into schema-driven development. In the next chapter, we'll explore how to design robust APIs using JSON schemas and leverage the powerful code generation system to create type-safe, maintainable applications.