diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index c9cac7a48116769bcc6169e1fa1f22ee35281cd0..2ab223c051e303877ce167ad9d09596f159b977f 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -8,6 +8,7 @@ ksp = "2.0.21-1.0.28"
 ktor = "3.0.1"
 kotlinx-serialization = "1.7.3"
 cache4k = "0.13.0"
+statelyConcurrentCollections = "2.1.0"
 slf4j = "2.0.16"
 
 
@@ -19,6 +20,7 @@ arrow = "1.2.4"
 # Main Koin version - for core, android deps
 koin = "4.0.0"
 koinTest = "4.0.0"
+gradleBuildConfigPlugin = "5.5.1"
 # Koin version for Compose multiplatform
 koinComposeMultiplatform = "4.0.0"
 mosaic = "0.13.0"
@@ -42,6 +44,7 @@ jline = "3.27.1"
 appdirs = "1.2.0"
 kstore = "0.9.1"
 kmpIo = "0.1.5"
+kotlinSemver = "2.0.0"
 
 kotlinx-dataframe = "0.13.1"
 klaxon = "5.6"
@@ -74,6 +77,7 @@ ktor-client-java = { group = "io.ktor", name = "ktor-client-java", version.ref =
 ktor-client-android = { group = "io.ktor", name = "ktor-client-android", version.ref = "ktor" }
 ktor-test = { group = "io.ktor", name = "ktor-client-mock", version.ref = "ktor" }
 cache4k = { group = "io.github.reactivecircus.cache4k", name = "cache4k", version.ref = "cache4k" }
+statelyConcurrentCollections = { group = "co.touchlab", name = "stately-concurrent-collections", version.ref = "statelyConcurrentCollections" }
 slf4j = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" }
 
 kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
@@ -93,6 +97,7 @@ appdirs = { module = "ca.gosyer:kotlin-multiplatform-appdirs", version.ref = "ap
 kstore = { module = "io.github.xxfast:kstore", version.ref = "kstore" }
 kstore-file = { module = "io.github.xxfast:kstore-file", version.ref = "kstore" }
 kmpIo = { module = "io.github.skolson:kmp-io", version.ref = "kmpIo" }
+kotlinSemver = { module = "io.github.z4kn4fein:semver", version.ref = "kotlinSemver" }
 
 sandwich = { module = "com.github.skydoves:sandwich", version.ref = "sandwich" }
 sandwich-ktor = { module = "com.github.skydoves:sandwich-ktor", version.ref = "sandwich" }
@@ -183,6 +188,7 @@ ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
 ktorfit = { id = "de.jensklingenberg.ktorfit", version.ref = "ktorfit" }
 kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
 kotestMultiplatform = { id = "io.kotest.multiplatform", version.ref = "kotest" }
+buildConfig = { id = "com.github.gmazzo.buildconfig", version.ref = "gradleBuildConfigPlugin" }
 dokka = { id = "org.jetbrains.dokka", version.ref = "dokka" }
 spotless = { id = "com.diffplug.spotless", version.ref = "spotless" }
 gitSemVer = { id = "org.danilopianini.git-sensitive-semantic-versioning", version.ref = "gitSemVer" }
diff --git a/temerity/build.gradle.kts b/temerity/build.gradle.kts
index f98fbe7efe856748e5d37d83b0c0852cdaa019eb..4c08df2c332ec0323f42134d95020f8ede82371c 100644
--- a/temerity/build.gradle.kts
+++ b/temerity/build.gradle.kts
@@ -26,6 +26,7 @@ plugins {
     alias(libs.plugins.ktorfit)
     alias(libs.plugins.kotlinxSerialization)
     alias(libs.plugins.kotestMultiplatform)
+    alias(libs.plugins.buildConfig)
 
     id("convention.formattingLib")
     id("convention.version")
@@ -43,6 +44,10 @@ buildscript {
     }
 }
 
+buildConfig {
+    buildConfigField("LIB_VERSION", provider { "${project.version}" })
+}
+
 gitSemVer {
     commitNameBasedUpdateStrategy(ConventionalCommit::semanticVersionUpdate)
 }
@@ -80,7 +85,8 @@ kotlin {
                 implementation(libs.ktorfit.lib)
                 implementation(libs.kotlinx.coroutines.core)
                 implementation(libs.kermit)
-                implementation(libs.cache4k)
+                // TODO: Re-add cache4k when updated for Kotlin 2.0.21
+                implementation(libs.statelyConcurrentCollections)
                 implementation(libs.kmpIo)
             }
         }
@@ -118,6 +124,8 @@ kotlin {
 
                 implementation(libs.koin.test)
                 implementation(libs.koin.test.junit5)
+
+                implementation(libs.kotlinSemver)
             }
         }
     }
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt
index a7372e974a21539852a64daba54998d237b4f674..cf6d4610dc67f584889f91479dc9f9c1383d33a9 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemClientConfig.kt
@@ -37,6 +37,7 @@ import kotlin.time.Duration
  * @property useWebTimeout Specifies whether to use a timeout  other than default for web requests
  * @property webTimeout Specifies the timeout [Duration] to use when making web requests
  * @property cacheTimeout Specifies the timeout [Duration] to use when storing cache entries. Affects keep-alive time for cached instance data like user role type fields.
+ * @property threadCount Specifies the number of threads to use for each client instance. Defaults to 2 at a minimum
  */
 @TemDsl
 public class TemClientConfig {
@@ -54,6 +55,8 @@ public class TemClientConfig {
   // Default cache timeout is set to 15 minutes if this is left unspecified; @see [edu.ucsc.its.temerity.core.Temerity] init block
   public var cacheTimeout: Duration? = null
 
+  public var threadCount: Int? = null
+
   /**
    * Configures the HttpClient with the provided block.
    * @param () -> HttpClient The block to configure the HttpClient.
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt
index c0ab3d5dec0fcc35641200802be56d3ff6e6409a..692c59d386ceb8af23cc77cff7d4d917786de4d1 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/TemerityApi.kt
@@ -225,10 +225,16 @@ public interface TemerityApi {
 
   public suspend fun refreshCachedUserRoles(): List<String>
 
-  public suspend fun getCachedUserRoles(refresh: Boolean): List<String>
+  /**
+   * Fetches the user roles from the platform cached locally on the client.
+   * @param refresh Whether to fetch the roles if not present locally.
+   */
+  public suspend fun getCachedUserRoles(refresh: Boolean = true): List<String>
+
+  public fun version(): String
 }
 
-// Objects used by Temerity implementations, and possibly library consumers
+// Objects used by library consumers and re-implementations of the Temerity API spec
 
 public val AUDIT_LOG_REQUEST_DATE_FORMAT: DateTimeFormat<LocalDate> = LocalDate.Format {
   dayOfMonth()
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt
index 593167c9a00b87287c2f2ab4aef324e778bbbd5b..d14b8880bac8848e840bd382a0f549224fa55a93 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/Util.kt
@@ -17,7 +17,7 @@
  */
 package edu.ucsc.its.temerity
 
-import edu.ucsc.its.temerity.core.TemerityLibrary.applicationScope
+import edu.ucsc.its.temerity.core.Temerity
 import kotlinx.coroutines.CoroutineScope
 import kotlinx.coroutines.Job
 import kotlinx.coroutines.SupervisorJob
@@ -37,8 +37,7 @@ internal fun currentDate(): LocalDate = Clock.System.todayIn(currentTz())
 
 internal fun currentTime(): LocalTime = thisInstant().toLocalDateTime(currentTz()).time
 
-internal fun createJobScope(coroutineContext: CoroutineContext = applicationScope, allowIndependentFailure: Boolean = false): CoroutineScope = if (allowIndependentFailure) {
-  CoroutineScope(SupervisorJob() + coroutineContext)
-} else {
-  CoroutineScope(Job() + coroutineContext)
+internal fun Temerity.createJobScope(coroutineContext: CoroutineContext = clientCoroutineScope, allowIndependentFailure: Boolean = false): CoroutineScope {
+  val parentJob = if (allowIndependentFailure) SupervisorJob() else Job()
+  return CoroutineScope(coroutineContext + parentJob)
 }
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/DispatcherFactory.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/DispatcherFactory.kt
new file mode 100644
index 0000000000000000000000000000000000000000..1d775c60a7364db210b3a3844d0ae7eb4d5c4926
--- /dev/null
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/DispatcherFactory.kt
@@ -0,0 +1,47 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+package edu.ucsc.its.temerity.core
+
+import edu.ucsc.its.temerity.core.Temerity.Companion.DEFAULT_MINIMUM_THREAD_COUNT
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlin.coroutines.CoroutineContext
+
+internal object DispatcherFactory {
+
+  internal fun createDispatcher(threadCount: Int): CoroutineContext = SupervisorJob() + Dispatchers.IO.limitedParallelism(threadCount)
+
+  private fun availableThreads() = Runtime.getRuntime().availableProcessors().minus(1)
+
+  internal fun calculateMaxThreads(defaultMinimumThreadCount: Int) = availableThreads().coerceAtLeast(defaultMinimumThreadCount)
+
+  internal fun setThreadCount(maximumThreadCount: Int? = null): Int = when (maximumThreadCount) {
+    null -> calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT)
+    else -> {
+      if (maximumThreadCount > availableThreads()) {
+        // TODO: Log warning that the configured thread count is higher than the number of available threads
+        calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT)
+      }
+      if (maximumThreadCount < DEFAULT_MINIMUM_THREAD_COUNT) {
+        // TODO: Log warning that the configured thread count is lower than the default minimum thread pool size
+        calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT)
+      }
+      maxOf(maximumThreadCount, calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT))
+    }
+  }
+}
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/HttpClientFactory.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/HttpClientFactory.kt
index 3adbd44ebfb7fd1108319b03781c16ee998e0722..920110d43bd9c8b441ae4f7cd877502089e27413 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/HttpClientFactory.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/HttpClientFactory.kt
@@ -18,7 +18,7 @@
 package edu.ucsc.its.temerity.core
 
 import edu.ucsc.its.temerity.TemClientConfig
-import edu.ucsc.its.temerity.core.TemerityLibrary.koin
+import edu.ucsc.its.temerity.core.Temerity.Companion.DEFAULT_WEB_TIMEOUT
 import io.ktor.client.HttpClient
 import io.ktor.client.HttpClientConfig
 import io.ktor.client.engine.HttpClientEngine
@@ -28,6 +28,7 @@ import io.ktor.client.plugins.logging.LogLevel
 import io.ktor.client.plugins.logging.Logging
 import io.ktor.client.request.header
 import io.ktor.http.URLProtocol
+import org.koin.core.context.GlobalContext.get
 import kotlin.time.DurationUnit
 import co.touchlab.kermit.Logger as KermitLogger
 import io.ktor.client.plugins.logging.Logger as KtorLogger
@@ -43,7 +44,6 @@ internal object HttpClientFactory {
    * It configures the client to log all requests and responses if enableNetworkLogs is true. TODO: read this as a build setting
    * It also sets a default request header with the provided authToken.
    * @param httpClientEngine The HttpClientEngine to use for the HttpClient.
-   * @param timeoutDuration The duration in milliseconds to wait for a response before timing out.
    * @param config A block specifying the following options:
    *  - Whether to log all requests and responses.
    *  - Service URL to make requests to.
@@ -69,7 +69,7 @@ internal object HttpClientFactory {
       if (config.optDebugEnabled) {
         if (config.httpClientLoggingBlock == null) {
           install(Logging) {
-            val kermit = koin.get<KermitLogger>()
+            val kermit = get().get<KermitLogger>()
             logger = object : KtorLogger {
               override fun log(message: String) {
                 kermit.d(message)
@@ -90,7 +90,7 @@ internal object HttpClientFactory {
       expectSuccess = config.expectSuccess
 
       if (config.useWebTimeout) {
-        val defaultWebTimeout = TemerityLibrary.DEFAULT_WEB_TIMEOUT.toLong(DurationUnit.MILLISECONDS)
+        val defaultWebTimeout = DEFAULT_WEB_TIMEOUT.toLong(DurationUnit.MILLISECONDS)
         val configuredWebTimeout = config.webTimeout?.toLong(DurationUnit.MILLISECONDS)
         install(HttpTimeout) {
           connectTimeoutMillis = configuredWebTimeout ?: defaultWebTimeout
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Library.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Library.kt
deleted file mode 100644
index 57b9f8b6e6bd547712c318da778c44804e1f253e..0000000000000000000000000000000000000000
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Library.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-/*
- *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
- *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
- *
- *     This library is free software; you can redistribute it and/or
- *     modify it under the terms of the GNU Lesser General Public
- *     License as published by the Free Software Foundation; version 2.1 of the License.
- *
- *     This library is distributed in the hope that it will be useful,
- *     but WITHOUT ANY WARRANTY; without even the implied warranty of
- *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
- *     Lesser General Public License for more details.
- *
- *     You should have received a copy of the GNU Lesser General Public
- *     License along with this library; if not, write to the Free Software
- *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
- */
-package edu.ucsc.its.temerity.core
-
-import edu.ucsc.its.temerity.TemClientConfig
-import edu.ucsc.its.temerity.di.libModule
-import edu.ucsc.its.temerity.di.loggerModule
-import edu.ucsc.its.temerity.platformModule
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-import org.koin.core.Koin
-import org.koin.dsl.koinApplication
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.minutes
-
-/**
- * TemerityLibrary is the main entry point for the Temerity library.
- *
- * This object initializes the Koin dependency injection framework and returns a library or builder instance
- *
- */
-public object TemerityLibrary {
-  public val DEFAULT_WEB_TIMEOUT: Duration = 2.minutes
-  internal val applicationScope = SupervisorJob() + Dispatchers.IO.limitedParallelism(2)
-
-  private val koinApp = koinApplication {
-    modules(
-      loggerModule(),
-      platformModule(),
-      libModule,
-    )
-  }
-  internal val koin: Koin = koinApp.koin
-}
-
-/**
- * Create a Temerity instance using Kotlin-DSL.
- * Adapted from the tmdb-api project: https://github.com/MoviebaseApp/tmdb-kotlin/raw/refs/heads/main/tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/Tmdb4.kt
- */
-@TemDsl
-public fun Temerity(block: TemClientConfig.() -> Unit): Temerity {
-  val config = TemClientConfig().apply(block)
-  return Temerity(config)
-}
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.kt
new file mode 100644
index 0000000000000000000000000000000000000000..ef8b67b094f090c42f5bcab14435443912d32a6e
--- /dev/null
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/LoggerFactory.kt
@@ -0,0 +1,35 @@
+/*
+ *     Designed and developed in 2022-2024 by William Walker (wnwalker@ucsc.edu)
+ *     Copyright 2022-2024 The Regents of the University of California. All rights reserved.
+ *
+ *     This library is free software; you can redistribute it and/or
+ *     modify it under the terms of the GNU Lesser General Public
+ *     License as published by the Free Software Foundation; version 2.1 of the License.
+ *
+ *     This library is distributed in the hope that it will be useful,
+ *     but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *     Lesser General Public License for more details.
+ *
+ *     You should have received a copy of the GNU Lesser General Public
+ *     License along with this library; if not, write to the Free Software
+ *     Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
+ */
+package edu.ucsc.its.temerity.core
+
+import co.touchlab.kermit.Logger
+import co.touchlab.kermit.NoTagFormatter
+import co.touchlab.kermit.Severity
+import co.touchlab.kermit.loggerConfigInit
+import co.touchlab.kermit.platformLogWriter
+
+internal object LoggerFactory {
+
+  internal fun createLogger(tag: String?): Logger = Logger(
+    config = loggerConfigInit(
+      platformLogWriter(NoTagFormatter),
+      minSeverity = Severity.Debug,
+    ),
+    tag = tag ?: "TemerityLib",
+  )
+}
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt
index 896c0ad21a9ee2768b1214459305d7c4a57de4d2..60fec02979202e2fccfc13dceb168b893e7a11c5 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/core/Temerity.kt
@@ -17,6 +17,7 @@
  */
 package edu.ucsc.its.temerity.core
 
+import co.touchlab.stately.collections.ConcurrentMutableMap
 import com.skydoves.sandwich.ApiResponse
 import com.skydoves.sandwich.StatusCode
 import com.skydoves.sandwich.getOrThrow
@@ -24,12 +25,14 @@ import com.skydoves.sandwich.ktor.executeApiResponse
 import com.skydoves.sandwich.ktor.statusCode
 import com.skydoves.sandwich.onSuccess
 import edu.ucsc.its.temerity.AuditLogSortOrder
+import edu.ucsc.its.temerity.BuildConfig
 import edu.ucsc.its.temerity.TemClientConfig
 import edu.ucsc.its.temerity.TemerityApi
 import edu.ucsc.its.temerity.api.PlatformApi
 import edu.ucsc.its.temerity.core.JsonFactory.buildJson
-import edu.ucsc.its.temerity.core.TemerityLibrary.koin
 import edu.ucsc.its.temerity.createJobScope
+import edu.ucsc.its.temerity.di.libModule
+import edu.ucsc.its.temerity.di.loggerModule
 import edu.ucsc.its.temerity.extensions.applyAuditLogFormat
 import edu.ucsc.its.temerity.extensions.applyScheduledSessionDateFormat
 import edu.ucsc.its.temerity.model.AuditLogEntry
@@ -44,8 +47,8 @@ import edu.ucsc.its.temerity.model.User
 import edu.ucsc.its.temerity.model.UserGroup
 import edu.ucsc.its.temerity.model.UserRecordingSession
 import edu.ucsc.its.temerity.model.UserUpdate
+import edu.ucsc.its.temerity.platformModule
 import edu.ucsc.its.temerity.sortByCreationDate
-import io.github.reactivecircus.cache4k.Cache
 import io.ktor.client.statement.HttpResponse
 import io.ktor.util.cio.toByteArray
 import io.ktor.utils.io.ByteReadChannel
@@ -59,8 +62,13 @@ import kotlinx.datetime.minus
 import kotlinx.serialization.SerializationException
 import kotlinx.serialization.encodeToString
 import kotlinx.serialization.json.Json
+import org.koin.core.Koin
+import org.koin.core.KoinApplication
 import org.koin.core.context.GlobalContext.get
 import org.koin.core.parameter.parametersOf
+import org.koin.dsl.koinApplication
+import kotlin.coroutines.CoroutineContext
+import kotlin.time.Duration
 import kotlin.time.Duration.Companion.minutes
 import co.touchlab.kermit.Logger as KermitLogger
 
@@ -70,15 +78,29 @@ import co.touchlab.kermit.Logger as KermitLogger
 public class Temerity internal constructor(
   private val config: TemClientConfig,
 ) : TemerityApi {
-
   /**
    * Constructor for Temerity
    */
   internal constructor(temApiToken: String) : this(TemClientConfig.withToken(temApiToken))
 
+  public companion object {
+    public val DEFAULT_WEB_TIMEOUT: Duration = 2.minutes
+
+    // TODO: Use this as cache4k expiration time
+    public val DEFAULT_CACHE_EXPIRATION: Duration = 15.minutes
+    internal const val DEFAULT_MINIMUM_THREAD_COUNT: Int = 2
+  }
+
+  override fun version(): String = BuildConfig.LIB_VERSION
+
   private val json: Json = buildJson()
-  private val platformApi: PlatformApi
-  private val cachedUserRoleList: Cache<Int, String>
+
+  @Suppress("MemberVisibilityCanBePrivate")
+  internal val temerityKoinApp: KoinApplication
+  internal val koin: Koin
+  private var platformApi: PlatformApi
+  private var cachedUserRoleList: ConcurrentMutableMap<Int, String>
+  internal var clientCoroutineScope: CoroutineContext
 
   init {
     check(!config.serviceToken.isNullOrBlank()) {
@@ -89,13 +111,26 @@ public class Temerity internal constructor(
       "Service url must be provided. Set it using TemClientConfig.serviceUrl(url)"
     }
 
+    temerityKoinApp = koinApplication {
+      modules(
+        loggerModule(),
+        platformModule(),
+        libModule,
+      )
+    }
+    koin = temerityKoinApp.koin
+    clientCoroutineScope = koin.get<CoroutineContext> {
+      when (val optThreadCount = config.threadCount) {
+        null -> parametersOf(DispatcherFactory.calculateMaxThreads(DEFAULT_MINIMUM_THREAD_COUNT))
+        else -> parametersOf(DispatcherFactory.setThreadCount(optThreadCount))
+      }
+    }
+
     platformApi = koin.get<PlatformApi> {
       parametersOf(config)
     }
 
-    cachedUserRoleList = Cache.Builder<Int, String>()
-      .expireAfterWrite(config.cacheTimeout ?: 15.minutes)
-      .build()
+    cachedUserRoleList = ConcurrentMutableMap()
   }
 
   /**
@@ -164,8 +199,9 @@ public class Temerity internal constructor(
     val returnedUsersResponse = platformApi.getUsers().executeApiResponse<String>().getOrThrow()
     val returnedUserList = json.decodeFromString<List<User>>(returnedUsersResponse)
     val roleTypes = returnedUserList.map { it.userType }.distinct()
+    cachedUserRoleList.clear()
     roleTypes.forEach {
-      cachedUserRoleList.put(it.hashCode(), it)
+      cachedUserRoleList[it.hashCode()] = it
     }
     return roleTypes
   }
@@ -174,7 +210,7 @@ public class Temerity internal constructor(
     if (refresh) {
       return refreshCachedUserRoles()
     }
-    val returnedUserRoles = cachedUserRoleList.asMap().values.toList()
+    val returnedUserRoles = cachedUserRoleList.values.toList()
     return returnedUserRoles.ifEmpty {
       refreshCachedUserRoles()
     }
@@ -403,3 +439,19 @@ public class Temerity internal constructor(
   public override suspend fun getStorageAnalyticsReport(groupId: Long): ByteArray =
     platformApi.getStorageAnalyticsReport(groupId).executeApiResponse<ByteReadChannel>().getOrThrow().toByteArray(limit = 2000000000) // Limit file downloads to 2 GB
 }
+
+/**
+ * Create a Temerity instance using Kotlin-DSL.
+ * Adapted from the tmdb-api project: https://github.com/MoviebaseApp/tmdb-kotlin/raw/refs/heads/main/tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/Tmdb4.kt
+ */
+@TemDsl
+public fun Temerity(block: TemClientConfig.() -> Unit): Temerity {
+  val config = TemClientConfig().apply(block)
+  return Temerity(config)
+}
+
+/**
+ * Create a Temerity Client Configuration instance using Kotlin-DSL.
+ */
+@TemDsl
+public fun TemClientConfig(block: TemClientConfig.() -> Unit): TemClientConfig = TemClientConfig().apply(block)
diff --git a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/Koin.kt b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/Koin.kt
index 92400c898f7ac651fe51ceb7556436fece7e64e2..8d053379682b8c0d2f03eba0dc3b3c9ca5cf595c 100644
--- a/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/Koin.kt
+++ b/temerity/src/commonMain/kotlin/edu/ucsc/its/temerity/di/Koin.kt
@@ -17,23 +17,17 @@
  */
 package edu.ucsc.its.temerity.di
 
-import co.touchlab.kermit.NoTagFormatter
-import co.touchlab.kermit.Severity
-import co.touchlab.kermit.loggerConfigInit
-import co.touchlab.kermit.platformLogWriter
 import com.skydoves.sandwich.ktorfit.ApiResponseConverterFactory
 import de.jensklingenberg.ktorfit.ktorfit
 import edu.ucsc.its.temerity.TemClientConfig
 import edu.ucsc.its.temerity.api.PlatformApi
 import edu.ucsc.its.temerity.api.createPlatformApi
+import edu.ucsc.its.temerity.core.DispatcherFactory.createDispatcher
 import edu.ucsc.its.temerity.core.HttpClientFactory.buildHttpClient
-import edu.ucsc.its.temerity.core.TemerityLibrary.koin
+import edu.ucsc.its.temerity.core.LoggerFactory.createLogger
 import io.ktor.client.HttpClient
-import org.koin.core.Koin
-import org.koin.core.component.KoinComponent
 import org.koin.core.parameter.parametersOf
 import org.koin.dsl.module
-import co.touchlab.kermit.Logger as KermitLogger
 
 internal fun loggerModule() = module {
   factory<co.touchlab.kermit.Logger> { tag ->
@@ -61,16 +55,7 @@ internal val libModule = module {
     }
     ktorfit.createPlatformApi()
   }
-}
-
-private fun createLogger(tag: String?): KermitLogger = KermitLogger(
-  config = loggerConfigInit(
-    platformLogWriter(NoTagFormatter),
-    minSeverity = Severity.Debug,
-  ),
-  tag = tag ?: "TemerityLib",
-)
-
-internal abstract class TemerityKoinComponent : KoinComponent {
-  override fun getKoin(): Koin = koin
+  single { (threadCount: Int) ->
+    createDispatcher(threadCount)
+  }
 }
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt
index 565875c2a00f42a90a583552d0ab15828d8fb796..df9b3ff7a6937b24947f031df55ea9abd0f28fae 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/DevUtilityTests.kt
@@ -22,9 +22,6 @@ import edu.ucsc.its.temerity.core.Temerity
 import edu.ucsc.its.temerity.di.loggerModule
 import io.kotest.core.extensions.Extension
 import io.kotest.core.spec.style.FunSpec
-import kotlinx.datetime.Clock
-import kotlinx.datetime.TimeZone
-import kotlinx.datetime.todayIn
 import org.dotenv.vault.dotenvVault
 import org.koin.core.parameter.parametersOf
 import org.koin.test.KoinTest
@@ -44,7 +41,6 @@ class DevUtilityTests :
     val dotenv = dotenvVault()
 
     lateinit var testTemerity: Temerity
-    val today = Clock.System.todayIn(TimeZone.currentSystemDefault())
 
     beforeTest {
       testTemerity = Temerity {
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt
index 0f6deec359cb1b16f0d1208c285fdb22af50d7b6..098fb938d396ebffb480cc5b031e4ee369ba3b53 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/ProdUtilityTests.kt
@@ -22,9 +22,6 @@ import edu.ucsc.its.temerity.core.Temerity
 import edu.ucsc.its.temerity.di.loggerModule
 import io.kotest.core.extensions.Extension
 import io.kotest.core.spec.style.FunSpec
-import kotlinx.datetime.Clock
-import kotlinx.datetime.TimeZone
-import kotlinx.datetime.todayIn
 import org.dotenv.vault.dotenvVault
 import org.koin.core.parameter.parametersOf
 import org.koin.test.KoinTest
@@ -44,7 +41,6 @@ class ProdUtilityTests :
     val dotenv = dotenvVault()
 
     lateinit var testTemerity: Temerity
-    val today = Clock.System.todayIn(TimeZone.currentSystemDefault())
 
     beforeTest {
       testTemerity = Temerity {
diff --git a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt
index 3ba2506dab26aff2d4818a16c9a7971953896a6e..86610e3637007d1b16091aaa1a256278ef39e36b 100644
--- a/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt
+++ b/temerity/src/jvmTest/kotlin/edu/ucsc/its/temerity/test/TemerityDevTest.kt
@@ -23,8 +23,12 @@ import com.skydoves.sandwich.ktor.getStatusCode
 import edu.ucsc.its.temerity.AuditLogSortOrder.NEW_FIRST
 import edu.ucsc.its.temerity.core.Temerity
 import edu.ucsc.its.temerity.currentDate
+import edu.ucsc.its.temerity.di.loggerModule
 import edu.ucsc.its.temerity.model.EventType.NEW_LOG_IN
 import edu.ucsc.its.temerity.model.NewUser
+import io.github.z4kn4fein.semver.Version
+import io.github.z4kn4fein.semver.toVersion
+import io.kotest.core.extensions.Extension
 import io.kotest.core.spec.style.FunSpec
 import io.kotest.matchers.file.shouldNotBeEmpty
 import io.kotest.matchers.shouldBe
@@ -34,19 +38,26 @@ import kotlinx.datetime.DateTimeUnit
 import kotlinx.datetime.TimeZone
 import kotlinx.datetime.minus
 import kotlinx.datetime.plus
-import kotlinx.datetime.toLocalDateTime
 import kotlinx.datetime.todayIn
 import org.dotenv.vault.dotenvVault
 import org.jetbrains.kotlinx.dataframe.api.toDataFrame
 import org.jetbrains.kotlinx.dataframe.io.writeCSV
+import org.koin.core.component.inject
+import org.koin.core.parameter.parametersOf
+import org.koin.test.KoinTest
 import java.io.File
 import kotlin.uuid.ExperimentalUuidApi
 import kotlin.uuid.Uuid
 
 @OptIn(ExperimentalUuidApi::class)
 class TemerityDevTest :
-  FunSpec({
+  FunSpec(),
+  KoinTest {
 
+  override fun extensions(): List<Extension> = listOf(koinExtension(loggerModule()))
+  private val kermit: Logger by inject { parametersOf("TemerityDevTest") }
+
+  init {
     val dotenv = dotenvVault()
 
     lateinit var testTemerity: Temerity
@@ -57,9 +68,17 @@ class TemerityDevTest :
       }
     }
 
+    test("Temerity client returns a correctly-formatted version String") {
+      val returnedVersion = testTemerity.version().toVersion()
+      kermit.d { "Returned client version: $returnedVersion" }
+      assert(testTemerity.version().isNotEmpty())
+      assert(returnedVersion < Version(0, 1, 0))
+      assert(returnedVersion.isPreRelease)
+    }
+
     test("Get all audit log entries of type \"New Login\" from the past month") {
       runBlocking {
-        val currentDate = Clock.System.todayIn(TimeZone.currentSystemDefault())
+        val currentDate = currentDate()
         val pastDate = currentDate.minus(1, DateTimeUnit.MONTH)
 
         val returnedAuditLogEntries = testTemerity.getAuditLogEntries(
@@ -133,13 +152,6 @@ class TemerityDevTest :
       }
     }
 
-    test("test datetime tostring formatting") {
-      runBlocking {
-        val time = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
-        println("Current time: $time")
-      }
-    }
-
     test("Get all audit log entries of type \"New Login\" from the past day") {
       runBlocking {
         val testEventType = NEW_LOG_IN
@@ -188,4 +200,5 @@ class TemerityDevTest :
         Logger.d(response.toString())
       }
     }
-  })
+  }
+}