Persistence

Persistence guide

The library traces commands and fires events — but it does not include a persistence layer. You provide the entity, the repository, and the event listener that connects them.

The persistence chain

Diagram

1. Entity

Create a JPA or Micronaut Data entity that implements CommandLogData. The static factory method from(params, request, response) is the conventional way to construct it:

@Entity
@Table(name = "command_log")
@Data
public class MyCommandLogEntity implements CommandLogData {

    @Id @GeneratedValue(strategy = IDENTITY)
    private Long id;

    private String tenantId;
    private String userId;
    @NonNull private UUID cmdUuid;
    private String cmdSourceRef;
    private String clientRef;

    @Enumerated(STRING) private CommandImportance importance;
    @Enumerated(STRING) private CommandRetention retention;
    @Enumerated(STRING) private CommandState state;

    private String cmdClass;
    @Enumerated(STRING) private HttpMethod httpMethod;
    @Enumerated(STRING) private HttpStatus httpStatus;
    private Integer httpStatusCode;

    private String problemCode;
    @JdbcTypeCode(SqlTypes.JSON) private Problem problem;
    @JdbcTypeCode(SqlTypes.JSON) private Map<String, Object> cmdBody;

    @CreationTimestamp private Instant createdAt;
    private Instant startedAt;
    private Instant endedAt;
    private Long durationInMillis;
    private Boolean dryRun;

    @JdbcTypeCode(SqlTypes.JSON) private Map<String, Object> attributes;

    public static MyCommandLogEntity from(CommandTracingParams params,
                                          CommandRequest req,
                                          CommandResponse res) {

        var entity = new MyCommandLogEntity();
        entity.setTenantId(req.tenantId());
        entity.setUserId(req.userId());
        entity.setCmdUuid(req.command().getCmdUuid());
        entity.setCmdSourceRef(req.command().getCmdSourceRef());
        entity.setClientRef(req.clientRef());
        entity.setImportance(params.importance());
        entity.setRetention(CommandRetention.fromImportance(params.importance()));
        entity.setState(res.decideState());
        entity.setCmdClass(req.cmdClass());
        entity.setHttpMethod(req.httpMethod());
        entity.setHttpStatus(res.httpStatus());
        entity.setHttpStatusCode(res.httpStatusCode());
        entity.setProblemCode(res.problemCode());
        entity.setProblem(res.problem());
        entity.setCmdBody(req.cmdBody());
        entity.setStartedAt(req.startedAt());
        entity.setEndedAt(res.endedAt());
        entity.setDurationInMillis(
            Duration.between(req.startedAt(), res.endedAt()).toMillis());
        entity.setDryRun(req.dryRun());
        entity.setAttributes(req.attributes());
        return entity;
    }
}

2. Publisher

The CommandTracingEventPublisher enriches the minimal request with tenant ID, user ID, and optionally the command body, then fires an async event:

@Singleton
public class MyCommandTracingEventPublisher implements CommandTracingEventPublisher {

    private final ApplicationEventPublisher<CommandTracingEvent> eventPublisher;

    @Override
    public void publishEvent(CommandTracingParams params,
                             CommandRequest minimal,
                             CommandResponse cmdResponse) {

        // resolveTenantId and resolveUserId are your application-specific methods
        // that extract identity from the command or the security context.
        var cmdRequest = CommandRequest.of(
            minimal,
            resolveTenantId(minimal.command()),
            resolveUserId(),
            params.options().contains(ExcludeCmdBody)
                ? null
                : cmdToMap(minimal.command())
        );

        eventPublisher.publishEventAsync(
            new CommandTracingEvent(
                MyCommandLogEntity.from(params, cmdRequest, cmdResponse),
                params
            )
        );
    }
}

Publishing asynchronously ensures the HTTP response is not delayed by persistence.

3. Event listener

A Micronaut @EventListener picks up the event and saves the entity:

@Singleton
public class MyCommandLogEventListener {

    private final MyCommandLogRepository repository;

    @EventListener
    @Async
    public void onCommandTraced(CommandTracingEvent event) {

        if (event.data() instanceof MyCommandLogEntity entity) {
            repository.save(entity);
        }
    }
}

4. Repository

A standard Micronaut Data repository:

@Repository
public interface MyCommandLogRepository
    extends GenericRepository<MyCommandLogEntity, Long> {

    MyCommandLogEntity save(MyCommandLogEntity entity);
}