Step 1.

Define rich, type‑safe domain model

Describe commands, events, and state of entities using Protobuf.

// Create a new task.
message CreateTask {
    TaskId id = 1; // assumed `required`
    string name = 2 [(required) = true];
    string description = 3;
}
// A new task has been created.
message TaskCreated {
    TaskId task = 1 [(required) = true];
    string name = 2 [(required) = true];
    string description = 3;
    spine.core.UserId owner = 4 [(required) = true];
}
// A task which can be assigned to a user.
message Task {
    option (entity).kind = AGGREGATE;
    TaskId id = 1; // assumed `required`
    string name = 2 [(required) = true];
    string description = 3;
    spine.core.UserId owner = 4 [(required) = true];
    spine.core.UserId assignee = 5;
}
Hide code

Step 2.

Generate the data types code for all tiers and runtimes

Generate code

Step 3.

Add business logic in a straight and testable way

Focus on business logic rather than “plumbing”. A Command will be delivered to only one Aggregate. Projections will get all Events they need. ProcessManagers will cover more complex scenarios. Storage, message delivery, and other environment matters are isolated from the main code.

final class TaskAggregate extends Aggregate<TaskId, Task, Task.Builder> {

    @Assign
    TaskCreated handle(CreateTask cmd, CommandContext ctx) {
        return TaskCreated.newBuilder()
                    .setTask(cmd.getId())
                    .setName(cmd.getName())
                    .setDescription(cmd.getDescription())
                    .setOwner(ctx.getActorContext().getActor())
                    .vBuild(); // validate the event
    }

    @Apply
    private void event(TaskCreated e) {
        builder().setName(e.getName())
                 .setDescription(e.getDescription())
                 .setOwner(e.getOwner());
    }
}
@DisplayName("Handling `CreateTask` command should")
public class TaskCreationTest extends ContextAwareTest {

    private TaskId task;
    private String name;
    private String description;
    ...
    @BeforeEach
    void postCommand() {
        CreateTask cmd = generateCommand();
        context().receivesCommand(cmd);
    }

    @Test
    @DisplayName("generate `TaskCreated` event")
    void eventGenerated() {
        TaskCreated expected = expectedEvent();
        context().assertEvent(TaskCreated.class)
                 .comparingExpectedFieldsOnly()
                 .isEqualTo(expected);
    }

    @Test
    @DisplayName("create a `Task`")
    void aggregateCreation() {
        Task expected = expectedAggregateState();
        context().assertEntityWithState(task, Task.class)
                 .hasStateThat()
                 .comparingExpectedFieldsOnly()
                 .isEqualTo(expected);
    }
}
Hide code

Step 4.

Easily deploy to Google Cloud or a custom environment

In-memory and JDBC-based storage implementations allow to implement and test the core logic quickly. Adopt your application to selected deployment environment(s) with a few lines of code.

static void configureEnvironment() {
    StorageFactory rdbms = JdbcStorageFactory.newBuilder()
        .setDataSource(dataSource())
        .build();
    StorageFactory datastore = DatastoreStorageFactory.newBuilder()
        .setDatastore(datastoreService())
        .build();

    ServerEnvironment
        .when(Production.class)
        .use(datastore);
    ServerEnvironment
        .when(Tests.class)
        .use(rdbms); // use RDBMS instead of default In-Memory storage for tests.
}
./gradlew build deploy
Hide code

Why Spine?

Code Generation

The code is automatically generated for all the languages of your project, as you update the model. Forget about missed hashCode() or equals().

Model Extensibility

With Protobuf support, a model can be extended preserving backward and future compatibility with client- and server nodes of your application.

Multitenancy Support

Transforming a single-tenant application into a multi-tenant one requires a few lines of code. You don't have to introduce tenantId parameters for all the calls.

Open for Business Changes

Add and remove fields while keeping binary compatibility with older code; handle new opportunities with oneof, natively provided by Protobuf. Build new Projections based on the whole event history of the system.

Clear API

Concepts from the DDD books, such as Aggregate, Projection, ProcessManager, Repository are right in the code. Ever guessed how to cook a BoundedContext? Guess no more!

Built-in Validation

Constraints defined in a business model are automatically checked for commands, events, and entity states. Learn more.

Multiple Storage and Deployment Platforms

The framework promotes development of storage- and platform-agnostic code. You can start with JDBC and later switch to Google Cloud Platform Datastore simply changing a few lines of code.

Permissive Apache License

Use freely in closed-source projects. You are also welcome to contribute to improving our framework.

Promoted Immutability

All the data types are immutable, which makes it easy to cache and share. Mutations are performed only in response to incoming messages using clearly defined cycles.

Type Safety

Should you need CustomerId or WorkEstimate value, you get it within seconds, for multiple languages. You can also have binary storage format and automatic JSON support.

Develop faster, with better results

With CQRS and Event-Driven Architecture you can separate development workflows. More experienced team members can concentrate on the core domain and the write-side tasks while the read–side and UI are created by the rest of the team.

Want to learn more?

Get started by learning concepts and exploring example applications.