OddSockets Java SDK

Official Java SDK for OddSockets real-time messaging platform

Java 11+ Maven Central Thread Safe High Performance Auto Reconnect

Overview & Features

The OddSockets Java SDK provides a robust, enterprise-ready interface for real-time messaging in Java applications, with full support for Spring Boot, Android, and standalone applications.

Java 11+ Support

Compatible with Java 11+ and all modern JVM languages including Kotlin and Scala.

Thread Safe

Fully thread-safe implementation with concurrent collections and proper synchronization.

Spring Boot Ready

Auto-configuration and starter dependencies for seamless Spring Boot integration.

High Performance

Optimized for low latency with efficient WebSocket connections and smart routing.

Cost Effective

No per-message pricing, industry-standard 32KB message limits, transparent pricing.

Automatic Failover

Built-in redundancy and intelligent error handling: the cluster reroutes you if a node goes away and the SDK resubscribes automatically.

Installation

Install from source for now. The Maven Central package is not published yet, so the commands in the tabs below will not resolve. Use this instead — it pulls the same code the tabs will install once the package is live:
bash
git clone https://github.com/jyswee/oddsockets-java-sdk
cd oddsockets-java-sdk && mvn install
xml
<dependency>
    <groupId>com.oddsockets</groupId>
    <artifactId>oddsockets-java-sdk</artifactId>
    <version>1.0.0</version>
</dependency>
gradle
implementation 'com.oddsockets:oddsockets-java-sdk:1.0.0'
scala
libraryDependencies += "com.oddsockets" % "oddsockets-java-sdk" % "1.0.0"

Quick Start

Basic Usage

java
import com.oddsockets.OddSockets;
import com.oddsockets.Channel;
import com.oddsockets.config.OddSocketsConfig;

public class BasicExample {
    public static void main(String[] args) {
        // Create configuration
        OddSocketsConfig config = new OddSocketsConfig.Builder()
            .apiKey("ak_live_1234567890abcdef")
            .build();

        // Initialize client
        OddSockets client = new OddSockets(config);

        // Get channel
        Channel channel = client.channel("my-channel");

        // Subscribe to messages
        channel.subscribe(message -> {
            System.out.println("Received: " + message.getData());
        });

        // Publish a message
        channel.publish("Hello, World!");
    }
}

Spring Boot Integration

java
@RestController
@RequestMapping("/api/chat")
public class ChatController {

    @Autowired
    private OddSockets oddSocketsClient;

    @PostMapping("/send")
    public ResponseEntity<String> sendMessage(@RequestBody ChatMessage message) {
        Channel channel = oddSocketsClient.channel("chat-room");
        
        try {
            channel.publish(message);
            return ResponseEntity.ok("Message sent successfully");
        } catch (Exception e) {
            return ResponseEntity.status(500).body("Failed to send message");
        }
    }
}

Async/CompletableFuture Usage

java
import java.util.concurrent.CompletableFuture;

public class AsyncExample {
    public void asyncPublish() {
        OddSockets client = new OddSockets(config);
        Channel channel = client.channel("async-channel");

        // Async publish
        CompletableFuture<PublishResult> future = channel.publishAsync("Hello Async!");
        
        future.thenAccept(result -> {
            System.out.println("Message published: " + result.getMessageId());
        }).exceptionally(throwable -> {
            System.err.println("Publish failed: " + throwable.getMessage());
            return null;
        });
    }
}

Configuration

Client Configuration

java
OddSocketsConfig config = new OddSocketsConfig.Builder()
    .apiKey("ak_live_1234567890abcdef")     // Required: Your OddSockets API key
    .userId("user-123")                     // Optional: User identifier
    .autoConnect(true)                      // Optional: Auto-connect on creation
    .reconnectAttempts(5)                   // Optional: Max reconnection attempts
    .heartbeatInterval(30000)               // Optional: Heartbeat interval (ms)
    .timeout(10000)                         // Optional: Connection timeout (ms)
    .build();

Channel Options

java
// Subscribe with options
SubscribeOptions subscribeOptions = new SubscribeOptions.Builder()
    .enablePresence(true)                   // Enable presence tracking
    .retainHistory(true)                    // Retain message history
    .filter("user.premium == true")         // Message filter expression
    .build();

channel.subscribe(callback, subscribeOptions);

// Publish with options
PublishOptions publishOptions = new PublishOptions.Builder()
    .ttl(3600)                              // Time to live (seconds)
    .metadata(Map.of("priority", "high"))   // Additional metadata
    .storeInHistory(true)                   // Store in message history
    .build();

channel.publish(message, publishOptions);

Examples

Explore comprehensive examples demonstrating the OddSockets Java SDK in production applications:

Enhanced Features

Beyond core pub/sub, OddSockets ships a Slack-like enhanced surface — reactions, typing indicators, threads, read receipts, presence/status, notifications, DMs, channel management, message editing and search. It lives on client.enhanced. The pattern is always the same: send an action with a client.enhanced.* method, then receive the paired broadcast with client.on("<event>", handler).

Typing & Reactions

java
// Receive-path: broadcasts from other users on the channel
client.on("user_typing",    data -> System.out.println("user is typing"));
client.on("reaction_added", data -> System.out.println("reaction added"));

// Send-path: enhanced actions over the live socket
client.enhanced.startTyping("alice", "room-42");

client.enhanced.addReaction("msg-1", "room-42", ":thumbsup:", "alice", "Alice");

Threads

java
client.on("thread_reply", data -> System.out.println("new thread reply"));

CompletableFuture<JsonObject> reply = client.enhanced.threadReply(
    "room-42", "msg-1", "Replying in the thread", "alice", "Alice");
System.out.println("thread reply: " + reply.get());

Enhanced surface

Each area exposes methods on client.enhanced; the worker broadcasts the paired events which you handle with client.on(...). Query methods (get*, search*) return a CompletableFuture<JsonObject> that completes with the worker response.

  • Typing — startTyping, stopTyping → user_typing, user_stopped_typing
  • Reactions — addReaction, removeReaction, getReactions → reaction_added, reaction_removed
  • Threads — threadReply, getThread, subscribeThread, followThread, markThreadRead → thread_reply, thread_subscribed, thread_followed, thread_read_updated
  • Read receipts — markRead, markAllRead, getUnreadCounts → user_read, unread_count_updated, all_marked_read
  • Messages — editMessage, deleteMessage, pinMessage, unpinMessage, getPinnedMessages, searchMessages → message_edited, message_deleted, message_pinned, message_unpinned
  • Presence & status — setStatus, setCustomStatus, setDND, getUserPresence → user_status_changed, custom_status_updated, dnd_status_changed
  • Channels — createChannel, updateChannel, archiveChannel, inviteToChannel, joinChannel, leaveChannel → channel_created, channel_updated, user_invited, user_joined_channel, user_left_channel
  • DMs — createDM, sendDM, getDMConversations → dm_created, dm_received
  • Notifications — subscribeNotifications, getNotifications, markNotificationRead, clearNotifications → notification, notification_read, notifications_cleared
  • Search — searchMessages, searchInChannel, searchByUser, filterMessages → (future results)

For any worker event not wrapped above, subscribe with the raw client.on("<event>", handler) API — all enhanced broadcasts are forwarded onto the client surface.

Challenges & Leaderboards

Challenges, leaderboards and achievements build on the same live socket. The send side lives on the enhanced surface (client.enhanced); request/query methods return a CompletableFuture<JsonObject> that resolves with the worker's reply, while progress and achievement calls are fire-and-forget. Inbound broadcasts arrive on the client event surface — subscribe with the client's normal client.on("<event>", handler).

Quick example

java
client.enhanced.createChallenge(Map.of(
    "challengeId", "weekly-sprint", "metric", "points", "ranked", true)).get();

client.enhanced.reportProgress(Map.of("challengeId", "weekly-sprint", "value", 120));

JsonObject board = client.enhanced.getStandings(
    Map.of("challengeId", "weekly-sprint", "limit", 10)).get();

client.enhanced.completeChallenge(
    Map.of("challengeId", "weekly-sprint", "outcome", "completed")).get();

Methods

All methods live on client.enhanced. Request/query methods return a CompletableFuture<JsonObject> that completes with the acked reply; the fire-and-forget methods return void.

  • createChallenge — create a challenge/leaderboard. Ack challenge_create_success.
  • reportProgress — fire-and-forget metric progress. (no ack)
  • completeChallenge — finalize with an outcome. Ack challenge_complete_success.
  • unlockAchievement — fire-and-forget; pass percentComplete (0–100). (no ack)
  • getStandings — request top-N + caller rank. Ack challenge_standings_success.
  • getAchievements — query achievement state. Ack achievement_state.
  • sendChallengeInvite — directed invite to another user. Ack challenge_invite_success.
  • replyChallengeInvite — accept/decline an invite. Ack challenge_reply_success.
  • cancelChallengeInvite — cancel a sent invite. Ack challenge_invite_cancel_success.
  • getChallengeInvites — list pending invites. Ack challenge_invites.

Completion outcomes

Pass one of these as the outcome to completeChallenge:

  • completed — win (rank 1)
  • failed — loss
  • tied — draw
  • conceded — resign / concede
  • expired — timed out

Progressive achievements

unlockAchievement always emits the wire event achievement_unlock; the worker is authoritative and derives the outbound broadcast from percentComplete: a value < 100 broadcasts achievement_progress (status in_progress), while >= 100 or an omitted value broadcasts achievement_unlock (status unlocked). You never emit achievement_progress yourself.

Inbound events

Subscribe with the client's normal client.on("<event>", handler).

  • Room broadcasts — challenge_progress, leaderboard_rank_change, challenge_complete, achievement_unlock, achievement_progress
  • Directed (per-user) — challenge_invited, challenge_reply_received, challenge_invite_cancelled

Usage Analytics

Pull your tenant's headline usage tiles — monthly active users, daily active users, total messages published, and error rate — straight from the SDK, without hand-rolling a REST call. getUsageStats() resolves the same four tiles the developer dashboard renders.

UsageStats stats = client.getUsageStats().get();

// Boxed Long/Double stay null when a tile is not live yet.
System.out.println("MAU:            " + tile(stats.getMau()));
System.out.println("DAU:            " + tile(stats.getDau()));
System.out.println("Total messages: " + tile(stats.getTotalMessages()));
System.out.println("Error rate:     " + tile(stats.getErrorRate()));

// Render an em-dash for a null tile, never a fabricated zero.
static String tile(Number v) { return v == null ? "\u2014" : v.toString(); }

The four tiles

  • mau — monthly active users for your owner scope
  • dau — daily active users
  • totalMessages — total messages published
  • errorRate — publish error rate, 0–1

Honesty rule — null, never a fake zero

Each tile is a boxed number or null. A null means that leg of the analytics pipeline is not live yet for your tenant — the SDK returns it verbatim and never coerces it to 0. Render an em-dash (—) for a null tile so you never show a fabricated zero.

Requires an API key

getUsageStats() reads your owner-scoped analytics, so it needs an apiKey. Keyless / token-only clients have no owner scope to query and will throw getUsageStats requires an apiKey.

Performance & Compatibility

Platform limits and plan ceilings, plus the versions and platforms this SDK supports:

32KB
Max message size
100M
Messages/mo (Scale)
5,000
Peak connections (Scale)
99.999%
Uptime SLA (Enterprise)

JVM Support

  • Java 11+ (LTS)
  • Kotlin 1.8+
  • Scala 2.13+
  • Android API 26+

Framework Support

  • Spring Boot 2.7+
  • Micronaut 3.0+
  • Quarkus 2.0+
  • Jakarta EE 9+

Framework Integrations

The OddSockets Java SDK works seamlessly with all modern Java frameworks. Here are examples showing how to integrate with popular frameworks:

Spring Boot

java
@Configuration
@EnableConfigurationProperties(OddSocketsProperties.class)
public class OddSocketsConfig {

    @Bean
    @ConditionalOnMissingBean
    public OddSockets oddSocketsClient(OddSocketsProperties properties) {
        OddSocketsConfig config = new OddSocketsConfig.Builder()
            .apiKey(properties.getApiKey())
            .userId(properties.getUserId())
            .autoConnect(properties.isAutoConnect())
            .build();
            
        return new OddSockets(config);
    }
}

@Service
public class ChatService {
    
    @Autowired
    private OddSockets oddSocketsClient;
    
    @EventListener
    public void handleUserMessage(UserMessageEvent event) {
        Channel channel = oddSocketsClient.channel("chat-" + event.getRoomId());
        channel.publish(event.getMessage());
    }
}

Android

java
public class ChatActivity extends AppCompatActivity {
    private OddSockets oddSocketsClient;
    private Channel chatChannel;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_chat);
        
        // Initialize OddSockets client
        OddSocketsConfig config = new OddSocketsConfig.Builder()
            .apiKey("ak_live_1234567890abcdef")
            .userId(getCurrentUserId())
            .build();
            
        oddSocketsClient = new OddSockets(config);
        chatChannel = oddSocketsClient.channel("mobile-chat");
        
        // Subscribe to messages
        chatChannel.subscribe(message -> {
            runOnUiThread(() -> {
                displayMessage(message);
            });
        });
    }
    
    private void sendMessage(String text) {
        ChatMessage message = new ChatMessage(getCurrentUserId(), text);
        chatChannel.publishAsync(message)
            .thenAccept(result -> {
                Log.d("Chat", "Message sent: " + result.getMessageId());
            })
            .exceptionally(throwable -> {
                Log.e("Chat", "Failed to send message", throwable);
                return null;
            });
    }
    
    @Override
    protected void onDestroy() {
        super.onDestroy();
        if (oddSocketsClient != null) {
            oddSocketsClient.disconnect();
        }
    }
}

Kotlin

kotlin
import com.oddsockets.OddSockets
import com.oddsockets.config.OddSocketsConfig
import kotlinx.coroutines.*

class ChatService {
    private val oddSocketsClient: OddSockets
    private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())
    
    init {
        val config = OddSocketsConfig.Builder()
            .apiKey("ak_live_1234567890abcdef")
            .userId("kotlin-user")
            .build()
            
        oddSocketsClient = OddSockets(config)
    }
    
    suspend fun subscribeToChannel(channelName: String) {
        val channel = oddSocketsClient.channel(channelName)
        
        channel.subscribe { message ->
            scope.launch {
                handleMessage(message)
            }
        }
    }
    
    suspend fun publishMessage(channelName: String, message: Any) {
        val channel = oddSocketsClient.channel(channelName)
        
        try {
            val result = channel.publishAsync(message).await()
            println("Message published: ${result.messageId}")
        } catch (e: Exception) {
            println("Failed to publish message: ${e.message}")
        }
    }
    
    private suspend fun handleMessage(message: Message) {
        // Process message on background thread
        withContext(Dispatchers.Default) {
            println("Received message: ${message.data}")
        }
    }
    
    fun cleanup() {
        scope.cancel()
        oddSocketsClient.disconnect()
    }
}

Micronaut

java
@Factory
public class OddSocketsFactory {
    
    @Singleton
    public OddSockets oddSocketsClient(@Property(name = "oddsockets.api-key") String apiKey,
                                       @Property(name = "oddsockets.user-id") Optional<String> userId) {
        OddSocketsConfig.Builder builder = new OddSocketsConfig.Builder()
            .apiKey(apiKey);
            
        userId.ifPresent(builder::userId);
        
        return new OddSockets(builder.build());
    }
}

@Controller("/api/chat")
public class ChatController {
    
    private final OddSockets oddSocketsClient;
    
    public ChatController(OddSockets oddSocketsClient) {
        this.oddSocketsClient = oddSocketsClient;
    }
    
    @Post("/send")
    public Single<HttpResponse<String>> sendMessage(@Body ChatMessage message) {
        Channel channel = oddSocketsClient.channel("micronaut-chat");
        
        return Single.fromFuture(channel.publishAsync(message))
            .map(result -> HttpResponse.ok("Message sent: " + result.getMessageId()))
            .onErrorReturn(throwable -> 
                HttpResponse.serverError("Failed to send message: " + throwable.getMessage()));
    }
}

Quarkus

java
@ApplicationScoped
public class OddSocketsProducer {
    
    @ConfigProperty(name = "oddsockets.api-key")
    String apiKey;
    
    @ConfigProperty(name = "oddsockets.user-id", defaultValue = "quarkus-user")
    String userId;
    
    @Produces
    @Singleton
    public OddSockets oddSocketsClient() {
        OddSocketsConfig config = new OddSocketsConfig.Builder()
            .apiKey(apiKey)
            .userId(userId)
            .build();
            
        return new OddSockets(config);
    }
}

@Path("/api/chat")
@ApplicationScoped
public class ChatResource {
    
    @Inject
    OddSockets oddSocketsClient;
    
    @POST
    @Path("/send")
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.APPLICATION_JSON)
    public Uni<Response> sendMessage(ChatMessage message) {
        Channel channel = oddSocketsClient.channel("quarkus-chat");
        
        return Uni.createFrom().completionStage(channel.publishAsync(message))
            .map(result -> Response.ok()
                .entity(Map.of("messageId", result.getMessageId()))
                .build())
            .onFailure().recoverWithItem(throwable ->
                Response.serverError()
                    .entity(Map.of("error", throwable.getMessage()))
                    .build());
    }
}