Copilot Instructions for Java Enterprise Services
Java service conventions for Copilot: immutable-by-default classes, Optional only as a return type, JUnit 5 with AssertJ, and exceptions that carry context instead of being swallowed.
Use when: Use in a Java 17+ backend service when you want Copilot to produce modern, immutable, well-tested Java instead of setter-heavy beans and bare try/catch blocks.
The skill file
GitHub Copilot · .github/copilot-instructions.md.github/copilot-instructions.md2948 chars
# Java Service Instructions
## Immutability
- Default every field to `private final`; introduce mutability only with a comment explaining why.
- Data carriers are `record`s. Do not generate JavaBean classes with getters/setters for new value types — records or builders only.
- Collections exposed from any class are unmodifiable: return `List.copyOf(...)` or wrap with `Collections.unmodifiableList(...)`; never expose the internal list.
- No setters on entities for fields that are part of the entity's identity or invariants — use intention-revealing methods (`order.markShipped(clock.instant())`).
- Static mutable state is forbidden, including "caches" implemented as static `HashMap`s. Use an injected cache abstraction.
## Optional usage
- `Optional` is a return type only. Never use it for fields, method parameters, or collection elements.
- Never call `.get()` without `.isPresent()` — and prefer `orElseThrow(() -> new SpecificException(...))`, `map`, or `ifPresent` over presence checks entirely.
- Do not return `Optional<Collection<T>>` — return an empty collection.
- Repository finders for a single row return `Optional<T>`; service methods that require the row to exist resolve it immediately with `orElseThrow` and a domain exception.
## Error handling
- Never catch `Exception` or `Throwable` except at the outermost boundary (request filter, message-consumer loop), and there, log with full stack trace and context fields.
- Empty catch blocks are forbidden. If an exception is genuinely ignorable, log it at DEBUG with a comment saying why.
- Wrap-and-rethrow must preserve the cause: `throw new PaymentFailedException("charge " + chargeId, e)` — never drop `e`.
- Domain exceptions extend a small sealed hierarchy under `exception/`; do not invent a new top-level exception per class.
- Log or rethrow, never both — double-logging the same failure at multiple layers is a bug.
## JUnit 5 patterns
- JUnit 5 (`@Test` from `org.junit.jupiter`) with AssertJ assertions (`assertThat(...)`). Hamcrest and JUnit 4 are forbidden.
- Test method names are sentences in camelCase: `refundFailsWhenOrderAlreadyRefunded()`. No `test` prefix.
- Use `@ParameterizedTest` with `@CsvSource`/`@MethodSource` for input matrices instead of duplicated tests.
- Exception assertions use `assertThatThrownBy(...).isInstanceOf(...).hasMessageContaining(...)` — never try/catch/fail in tests.
- Inject a fixed `Clock` everywhere time is read; tests must never call `Instant.now()` or sleep.
- Mock with Mockito only at I/O boundaries; construct real objects for everything in the same module.
## General
- Streams over loops when the operation is a clean map/filter/collect; loops when there is branching, early exit, or checked exceptions involved.
- No Lombok in new code; records and explicit constructors cover the need.
- Run `./mvnw verify` (Checkstyle + tests) before suggesting any commit; never commit with `@Disabled` tests added.
Install
drop into your repoSave this to your project or home directory so GitHub Copilot can load it.
path./.github/copilot-instructions.md
Discussion
What people are saying
Forks
0 forks of this skillLoading forks…