Tutorial: add your first module

Goal: a CRUD module with an entity, repository, service and controller that is auto-configured by the starter.

1. Copy the golden path

cd strata/examples
cp -r strata-module-example my-domain

The example is a complete reactor module; rename the artifact/package and add it to strata/examples/pom.xml (and to strata/pom.xml module list for the product reactor). Keep the api/biz split:

my-domain/
  my-domain-api/   entity, DTO, service port
  my-domain-biz/   controller, app service, repository, mapper
    src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports

2. Entity and repository

@Entity
@Table(name = "biz_note")
public class Note {
    @Id
    private Long id;          // snowflake ids via IdGenerator, never auto-increment
    private String title;
    private String body;
    private Long createdAt;
    // getters/setters
}

public interface NoteRepository extends JpaRepository<Note, Long> {}

Add the entity/repository package to your module's auto-configuration (or place them under a package that the starter already scans if you are extending the system domain):

@AutoConfiguration
@EntityScan(basePackages = "dev.strata.mydomain.api.entity")
@EnableJpaRepositories(basePackages = "dev.strata.mydomain.biz.repository")
public class MyDomainAutoConfiguration {}

3. Service and controller

@Service
public class NoteAppService {
    private final NoteRepository repository;

    public NoteAppService(NoteRepository repository) { this.repository = repository; }

    @Transactional
    public Note create(String title, String body) {
        Note note = new Note();
        note.setId(IdGenerator.nextId());
        note.setTitle(title);
        note.setBody(body);
        return repository.save(note);
    }

    @Transactional(readOnly = true)
    public List<Note> list() { return repository.findAll(); }
}

@RestController
@RequestMapping("/api/v1/notes")
public class NoteController {
    private final NoteAppService service;

    public NoteController(NoteAppService service) { this.service = service; }

    @GetMapping
    @PreAuthorize("@permissions.has('notes:note:list')")
    public Result<List<NoteDto>> list() {
        return Result.ok(service.list().stream().map(NoteDto::from).toList());
    }

    @PostMapping
    @PreAuthorize("@permissions.has('notes:note:create')")
    public Result<NoteDto> create(@RequestBody CreateNoteRequest request) {
        return Result.ok(NoteDto.from(service.create(request.title(), request.body())));
    }
}

4. Migration

Add a Flyway migration after the framework ones — the framework already ships V1__strata_framework.sql and V2__idempotency_record.sql in classpath:db/strata-framework, and Flyway versions are global across locations, so start at V3 (never edit released migrations):

-- src/main/resources/db/migration/V3__create_biz_note.sql
create table biz_note (
  id bigint primary key,
  title varchar(128) not null,
  body text,
  created_at bigint not null
);

5. Test it

@SpringBootTest(webEnvironment = RANDOM_PORT)
class NoteApiTest {
    @Autowired TestRestTemplate rest;
    // POST /api/v1/notes with a JWT, assert 200 and the returned id
}

Every module in this repository runs mvn verify with tests (JDK 25 is the build toolchain; the compiled code targets Java 21); add at least one controller-level test.

6. Permissions

Register the authority string in the RBAC seed (or through the admin UI): notes:note:list, notes:note:create, notes:note:update, notes:note:delete. The naming convention is module:entity:action.