A simple example of server application in Spine to get you started.
-
Install JDK 8 or higher.
-
Clone the source code:
git clone git@github.com:spine-examples/server-quickstart.git
-
Run
./gradlew clean build
(orgradlew.bat clean build
on Windows) in the project root folder.
The project consists of three modules.
Defines the Ubiquitous Language of the application in Protobuf.
The model/src/main/proto
directory contains the Protobuf definitions of the domain model:
Task
is an aggregate state type; as any entity type, it is marked with the(entity)
option;TaskCreated
inevents.proto
is an event of theTaskAggregate
;CreateTask
incommands.proto
is a command handled by theTaskAggregate
;- the model may also contain other message types, e.g. identifiers (see
identifiers.proto
), value types, etc.
-
Describes the business rules for Spine entities, such as Aggregates, in Java. See the
TaskAggregate
which handles theCreateTask
command and applies the producedTaskCreated
event. -
Plugs the
model
into the infrastructure:- configures the storage;
- creates a
BoundedContext
and registers repositories; - exposes the
BoundedContext
instance to the outer world through a set of gRPC services, provided by the framework.
See io.spine.tasks.server.ServerApp
for implementation.
Run ServerApp.main()
to start the server.
Interacts with the gRPC services, exposed by the server
module:
- sends commands via
CommandService
stub; - sends queries via
QueryService
stub.
See io.spine.tasks.client.ClientApp
for implementation.
Run ClientApp.main()
to start the client and see it connecting to the server.
-
Experiment with the model. Create a new command type in
commands.proto
import "spine/time/time.proto"; import "spine/time_options.proto"; // ... message AssignDueDate { TaskId task = 1; spine.time.LocalDate due_date = 2 [(validate) = true, (required) = true, (when).in = FUTURE]; }
Remember to import
LocalDate
viaimport "spine/time/time.proto";
. This type is provided by the Spine Time library. You don't have to perform any additional steps to use it in your domain. -
Create a new event type in
events.proto
:import "spine/time/time.proto"; // ... message DueDateAssigned { TaskId task = 1; spine.time.LocalDate due_date = 2 [(validate) = true, (required) = true]; }
-
Adjust the aggregate state:
import "spine/time/time.proto"; // ... message Task { option (entity) = {kind: AGGREGATE visibility: FULL}; // An ID of the task. TaskId id = 1; // A title of the task. string title = 2 [(required) = true]; // The date and time by which this task should be completed. spine.time.LocalDate due_date = 3 [(validate) = true, (required) = false]; }
Make sure to run a Gradle build after the changing the Protobuf definitions:
./gradlew clean build
or for Windows:
gradlew.bat clean build
-
Handle the
AssignDueDate
command in theTaskAggregate
:@Assign DueDateAssigned handle(AssignDueDate command) { return DueDateAssigned .newBuilder() .setTask(command.getTask()) .setDueDate(command.getDueDate()) .vBuild(); }
-
Apply the emitted event:
@Apply private void on(DueDateAssigned event) { builder().setDueDate(event.getDueDate()); }
-
In
ClientApp
, extend themain()
method. Post another command:AssignDueDate dueDateCommand = AssignDueDate .newBuilder() .setTask(taskId) .setDueDate(LocalDates.of(2038, JANUARY, 19)) .vBuild(); commandService.post(requestFactory.command() .create(dueDateCommand));
and log the updated state:
QueryResponse updatedStateResponse = queryService.read(taskQuery); info("The second response received: %s", Stringifiers.toString(updatedStateResponse));
-
Restart the server. Run the client and make sure that the due date is set to the task.