CARP Core Framework

CARP Core is a software framework to help developers build research platforms to run studies involving distributed data collection.
It provides modules to define, deploy, and monitor research studies, and to collect data from multiple devices at multiple locations.
It is the result of a collaboration between iMotions and the Copenhagen Center for Health Technology (CACHET).
Both used CARP Core to implement their respective research platforms: the iMotions Mobile Research Platform (discontinued) and the Copenhagen Research Platform (CARP).
Following domain-driven design, this project contains all domain models and application services for all CARP subsystems (depicted below), not having any dependencies on concrete infrastructure.
As such, this project defines an open standard for distributed data collection, available for Kotlin, the Java runtime, and JavaScript, which others can build upon to create their own infrastructure.
Two key design goals differentiate this project from similar projects:
- Modularity: Whether you want to set up your own entire infrastructure, customize part of it but integrate with externally hosted CARP webservices, or simply create an app which collects data locally on a smartphone, you can 'pick and choose' the subsystems you are interested in and how to deploy them.
A , and hosting can be arranged upon request.
Table of Contents
Architecture

Each of the subsystems expose application service interfaces with corresponding integration events.
Synchronous communication between subsystems happens via dependency injected application service interfaces,
which implementing infrastructures are expected to implement as remote procedure calls (RPCs).
Asynchronous communication between subsystems happens via an event bus,
which implementing infrastructures are expected to implement using a message queue which guarantees order for all IntegrationEvent's sharing the same aggregateId.
Not all subsystems are implemented or complete yet.
Currently, this project contains a stable version of the protocols, studies, deployments, and data subsystems.
The client subsystem is still considered alpha and expected to change in the future.
The resources and analysis subsystem are envisioned later additions.
Infrastructure helpers
Even though this library does not contain dependencies on concrete infrastructure, it does provide building blocks which greatly facilitate hosting the application services defined in this library as a distributed service and consuming them.
You are not required to use these, but they remove boilerplate code you would otherwise have to write.
Serialization
To facilitate easy exchange of requests across the different subsystems, all objects that are passed through application services are serializable to JSON using built-in serializers.
This works for both the Java runtime and JavaScript, which is achieved by relying on the kotlinx.serialization library and compiler plugin.
In fact, kotlinx.serialization also supports other formats, such as ProtoBuf and CBOR, but we have not tested those extensively.
In addition, domain objects which need to be persisted (aggregate roots) implement the snapshot pattern.
All snapshots are fully serializable to JSON, making it straightforward to store them in a document store.
But, if you prefer to use a relational database instead, you can call consumeEvents() to get all the modifications since the object was last stored.
Lastly, custom serializers to the default ones generated by kotlinx.serialization are provided for extendable types used in study protocols (e.g., DeviceConfiguration).
These 'magic' serializers support deserializing extending types which are unknown at runtime, allowing you to access the base properties seamlessly.
Using the built-in serializers thus allows you to handle incoming requests and persistence of extending types you do not have available at compile time.
They are used by default in all objects that need to be serialized for data transfer or snapshot storage.
It is therefore recommended to use built-in serializers to store and transfer any objects containing study protocol information to get this type of extensibility for free.
More detailed information on how this works can be found in .
Request objects
To help implement remote procedure calls (RPCs), each application service has matching polymorphic serializable 'request objects'.
For example, the "deployments" subsystem has a sealed class DeploymentServiceRequest and each subclass represents a request to DeploymentService.
Using these objects, all requests to a single application service can be handled by one endpoint using type checking.
We recommend using a when expression so that the compiler can verify whether you have handled all requests.
In addition, each request object can be executed by passing a matching application service to invokeOn.
This allows a centralized implementation for any incoming request object to an application service.
However, in practice you might want to perform additional actions depending on specific requests, e.g., authorization which is currently not part of core.
Application service versioning
When using the default serializers for the provided request objects and integration events, you can get backwards compatible application services for free.
Each new CARP version will come with the necessary application service migration functionality for new minor API versions.
Clients that are on the same major version as the backend will be able to use new hosted minor versions of the API.
Each application service has a corresponding ApplicationServiceApiMigrator.
To get support for backwards compatible application services, you need to wire a call to migrateRequest into your infrastructure endpoints.
MigratedRequest.invokeOn can be used to execute the migrated request on the application service.
Authorization
Currently, this library does not contain support for authorization.
Authorization needs to be implemented by concrete infrastructure.
However, CARP is designed with claim-based authorization in mind, and the documentation of application services in each of the subsystems describes a recommended implementation.
In a future release we might pass authorization as a dependent service to application services.
Stub classes
Stub classes are available for the abstract domain objects defined in the common subsystem.
These can be used to write unit tests in which you are not interested in testing the behavior of specific device configurations, trigger configurations, etc., but rather how they are referenced from within a study protocol or deployment.
In addition, String manipulation functions are available to convert type names of protocol domain objects within a JSON string to 'unknown' type names. This supports testing deserialization of domain objects unknown at runtime, e.g., as defined in an application-specific client. See the section on serialization for more details.
Usage
This is a multiplatform Kotlin library which targets both the Java Runtime Environment (JRE) and JavaScript (JS).
Since this project does not contain any infrastructure, you need to include dependencies to the subsystems you want to implement infrastructure for and implement all application services, e.g. as a web service. We recommend reading the Kotlin documentation to see how to consume multiplatform libraries.
As this project progresses, we intend to include native targets as well, starting with iOS.
The releases are published to Maven. In case you want to use SNAPSHOT versions, use the following repository:
maven { url "https://central.sonatype.com/repository/maven-snapshots/" }
Example
The following shows how the subystems interact to create a study protocol, instantiate it as a study, and deploy it to a client.
carp.protocols: Example study protocol definition to collect GPS and step count on a smartphone which can be serialized to JSON:
val ownerId = UUID.randomUUID()
val protocol = StudyProtocol( ownerId, "Track patient movement" )
phone = Smartphone.create( )
{
defaultSamplingConfiguration {
geolocation { batteryNormal { granularity = Granularity.Balanced } }
}
}
protocol.addPrimaryDevice( phone )
sensors = Smartphone.Sensors
trackMovement = Smartphone.Tasks.BACKGROUND.create( ) {
measures = listOf( sensors.GEOLOCATION.measure(), sensors.STEP_COUNT.measure() )
description =
}
protocol.addTaskControl( phone.atStartOfStudy().start( trackMovement, phone ) )
json: String = JSON.encodeToString( protocol.getSnapshot() )
carp.studies: Example creation of a study based on a study protocol, and adding and deploying a single participant:
carp.deployments: Most calls to this subsystem are abstracted away by the 'studies' and 'clients' subsystems, so you wouldn't call its endpoints directly. Example code which is called when a study is created and accessed by a client:
carp.data: Calls to this subsystem are abstracted away by the 'deployments' subsystem and are planned to be abstracted away by the 'clients' subsystem.
Example code which is called once a deployment is running and data is subsequently uploaded by the client.
val dataStreamService: DataStreamService = createDataStreamEndpoint()
val studyDeploymentId: UUID = getStudyDeploymentId()
device =
geolocation = DataStreamsConfiguration.ExpectedDataStream( device, CarpDataTypes.GEOLOCATION.type )
stepCount = DataStreamsConfiguration.ExpectedDataStream( device, CarpDataTypes.STEP_COUNT.type )
configuration = DataStreamsConfiguration( studyDeploymentId, setOf( geolocation, stepCount ) )
dataStreamService.openDataStreams( configuration )
geolocationData = MutableDataStreamSequence<Geolocation>(
dataStream = dataStreamId<Geolocation>( studyDeploymentId, device ),
firstSequenceId = ,
triggerIds = listOf( )
)
uploadData: DataStreamBatch = MutableDataStreamBatch().apply {
appendSequence( geolocationData )
}
dataStreamService.appendToDataStreams( studyDeploymentId, uploadData )
carp.client: Example initialization of a smartphone client for the participant that got invited to the study in the 'studies' code sample above:
Development
In case you want to contribute, please follow our contribution guidelines.
We recommend using IntelliJ IDEA 2026 (community edition is fine), as this is the development environment we use and is therefore fully tested.
- Open the project folder in IntelliJ IDEA.
- Make sure Google Chrome is installed; JS unit tests are run on headless Chrome.
- In case you want to run TypeScript declaration tests (
verifyTsDeclarations), install node.
- To build/test/publish, click "Edit Configurations" to add configurations for the included Gradle tasks, or run them from the Gradle tool window.
Gradle tasks
For carp.core-kotlin:
Release management
Semantic versioning is used for releases.
Backwards compatibility is assessed from the perspective of clients using an implementation of the framework,
as opposed to developers using the framework to implement an infrastructure.
In other words, versioning is based on the exposed API (application namespaces), but not the domain used to implement infrastructures (domain namespaces).
Breaking changes between minor versions can occur in domain objects, including the need to do database migrations.
Module versions are configured in the main build.gradle in ext.globalVersion and ext.clientsVersion.
Workflows:
- Each push to
develop triggers a snapshot release of the currently configured version.
- Each push to
master triggers a release to Maven using the currently configured version.
Releases require a couple of manual steps:
Development checklists
When changes are made to CARP Core, various parts in the codebase sometimes need to be updated accordingly.
Generally speaking, failing tests will guide you as an attempt was made to catch omissions through automated tests.
But, recommended workflows for common new features/changes are documented in development checklists.