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
- Protocol Handler receives raw request
- Request Parser converts to common format
- Middleware Pipeline processes request
- Router dispatches to endpoint handler
- Handler executes business logic
- Response Serializer converts response
- 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,float64bool,datetime,bytesany(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.