BMX Hastar is a lightweight Java utility library for structured exception handling and standardized error/action messaging in Spring Boot applications. It provides a shared vocabulary — consistent exception types, integer operation codes, and human-readable message constants — that multiple services in an ecosystem can depend on to ensure uniform error handling behavior.
- Group ID:
com.bloomscorp - Artifact ID:
bmx-hastar - Current Version:
0.0.3 - Java: 21+
- License: MIT
- Published to: Maven Central
- Why BMX Hastar?
- Installation
- Package Structure
- API Reference
- Usage Guide
- Design Notes
- Dependencies
- License
In a multi-service Spring Boot ecosystem, each service tends to re-invent its own exception classes and error messages independently. This leads to inconsistent error responses, duplicated string literals scattered across codebases, and no shared contract between services about what a given error code means.
BMX Hastar solves this by providing:
- Three purpose-built exception types that cover the most common failure categories: generic system errors, authorization failures, and missing-user scenarios.
- Two integer code registries (
ActionCodefor CRUD outcomes,ErrorCodefor system/domain errors) with a decode method on each that maps codes to ready-made messages. - One centralized message store (
Message) with ~30 pre-written, consistent user-facing strings — so the wording of error messages is decided once, not per-service.
The library has no Spring dependency of its own. It is pure Java, so it can be included in any Spring Boot service without pulling in a framework conflict.
<dependency>
<groupId>com.bloomscorp</groupId>
<artifactId>bmx-hastar</artifactId>
<version>0.0.3</version>
</dependency>implementation("com.bloomscorp:bmx-hastar:0.0.3")implementation 'com.bloomscorp:bmx-hastar:0.0.3'com.bloomscorp.hastar
│
├── CarrierException.java # Generic wrapper exception with an origin and error code
├── AuthorizationException.java # JWT / Spring Security authorization failure exception
├── NoUserFoundException.java # Exception for failed username lookups
│
├── code/
│ ├── ActionCode.java # Integer codes for CRUD outcomes + message resolver
│ └── ErrorCode.java # Integer codes for system/domain errors + message decoder
│
└── support/
├── Message.java # Centralized human-readable message string constants
└── Constant.java # Primitive library-wide fallback constants
Package: com.bloomscorp.hastar
Extends: RuntimeException
A general-purpose wrapper exception that carries an original Exception alongside a structured ErrorCode integer. It is the primary exception type for propagating unexpected system-level failures (encoding errors, malformed payloads, forged requests, etc.) up through service layers to a global handler.
Fields:
| Field | Type | Description |
|---|---|---|
origin |
Exception |
The original exception that triggered this carrier. Must not be null. |
errorCode |
int |
A category code from ErrorCode identifying the type of failure. |
Constructor:
new CarrierException(Exception origin, int errorCode)Lombok generates @AllArgsConstructor and @Getter — both fields are constructor-injectable and readable via getters.
Package: com.bloomscorp.hastar
Extends: RuntimeException
Represents a specific failure during the JWT authentication or Spring Security authorization process. Carries one of six error codes (AECx01–AECx06) and resolves the appropriate human-readable message via an overridden getMessage().
Error Code Constants:
| Constant | Value | Meaning |
|---|---|---|
AECx01 |
901 |
Some credentials were missing from the authentication request. |
AECx02 |
902 |
The bearer token was absent from the request header. |
AECx03 |
903 |
The username could not be resolved from the authentication request. |
AECx04 |
904 |
The security context already holds a non-null authentication object. |
AECx05 |
905 |
The provided JWT token is invalid or could not be verified. |
AECx06 |
906 |
Reserved for future authorization failure scenarios. |
Fields:
| Field | Type | Description |
|---|---|---|
errorCode |
int |
Must be one of AECx01–AECx06. |
Constructor:
new AuthorizationException(int errorCode)Methods:
| Method | Return | Description |
|---|---|---|
getMessage() |
String |
Returns the human-readable message for the given error code. Falls back to a generic authorization exception message for unrecognized codes. Never returns null. |
Package: com.bloomscorp.hastar
Extends: RuntimeException
Thrown when a username lookup (typically inside a UserDetailsService implementation or repository layer) finds no associated account. The failed lookup key is preserved in the username field for logging and error reporting.
Fields:
| Field | Type | Description |
|---|---|---|
username |
String |
The username that had no matching account. Must not be null. |
Constructor:
new NoUserFoundException(String username)Package: com.bloomscorp.hastar.code
A non-instantiable utility class defining integer codes for CRUD operation outcomes, with a static message() resolver. By convention: positive codes indicate success, 0 means no action, and negative codes indicate failure.
Constants:
| Constant | Value | Meaning |
|---|---|---|
NO_ACTION |
0 |
No database action was attempted or required. |
INSERT_SUCCESS |
1 |
Insert operation succeeded. |
UPDATE_SUCCESS |
2 |
Update operation succeeded. |
DELETE_SUCCESS |
3 |
Delete operation succeeded. |
INSERT_FAILURE |
-1 |
Insert operation failed. |
NOT_UNIQUE |
-2 |
A uniqueness constraint violation prevented the operation. |
INCORRECT_INFORMATION |
-3 |
The request contained factually incorrect information. |
UNAUTHORIZED_ACCESS |
-4 |
The operation was rejected due to insufficient authorization. |
UPDATE_FAILURE |
-5 |
Update operation failed. |
PARTIAL_UPDATE_FAILURE |
-6 |
Some records in a batch update failed while others succeeded. |
DELETE_FAILURE |
-7 |
Delete operation failed. |
Methods:
| Method | Signature | Description |
|---|---|---|
message |
static String message(int code) |
Resolves an action code to its human-readable message. Returns an empty string ("") for unrecognized codes. Never returns null. |
Package: com.bloomscorp.hastar.code
A non-instantiable utility class defining negative integer codes for system-level and domain-level error categories, with a static decode() resolver. All codes are negative to distinguish them from ActionCode success states.
Constants:
| Constant | Value | Meaning |
|---|---|---|
GENERIC |
-1 |
Generic fallback for unclassified failures. |
EMAIL_ENCODER |
-2 |
Failure during email address encoding or decoding. |
PASSWORD_ENCODER |
-3 |
Failure during password encoding or decoding. |
MALFORMED_PAYLOAD |
-4 |
The request payload is structurally malformed. |
FORGED_REQUEST |
-5 |
The request appears to have been forged or tampered with. |
AUTHORIZATION_DENIED |
-6 |
Authorization for the request was explicitly denied. |
INVALID_REQUEST |
-7 |
The request itself is considered invalid. |
INVALID_INFORMATION |
-8 |
The request contains semantically incorrect information. |
Note:
MALFORMED_PAYLOADandFORGED_REQUESTintentionally decode to the same message (Message.MALFORMED_INFORMATION). From a consumer's perspective, both represent integrity violations, and leaking the distinction could expose internal security logic.
Methods:
| Method | Signature | Description |
|---|---|---|
decode |
static String decode(int code) |
Decodes an error code into its human-readable message. Falls back to Message.GENERIC_ERROR for unrecognized codes. Never returns null. |
Package: com.bloomscorp.hastar.support
A non-instantiable utility class holding all human-readable message string constants used across the library and intended for use in consuming applications. Grouped by domain below for clarity.
Authentication & Credentials:
| Constant | Value |
|---|---|
IO_FAILURE |
"An I/O failure was encountered." |
SERVLET_FAILURE |
"A servlet failed while trying to process your request." |
EXPIRED_CREDENTIALS |
"The credentials used have expired." |
ILLEGAL_ARGUMENTS |
"Illegal credentials were used to authenticate." |
BAD_CREDENTIALS |
"The credentials provided were incorrect." |
ACCOUNT_DISABLED |
"The associated account is disabled and not accessible." |
AUTH_INFO_UNAVAILABLE |
"No associated account was found. Bad credentials, maybe." |
INTERNAL_AUTHENTICATION_SERVICE_FAILURE |
"Our system failed to authenticate the credentials." |
MALFORMED_CREDENTIALS |
"The credentials have been tampered with." |
USERNAME_NOT_FOUND |
"The username does not have any associated account." |
USER_ACCOUNT_LOCKED |
"The user account may be locked. Contact systems administrator." |
Request Validation:
| Constant | Value |
|---|---|
INCOMPLETE_INFORMATION |
"Complete information was not provided." |
MALFORMED_INFORMATION |
"The information provided is malformed and cannot be accepted." |
INVALID_REQUEST |
"This request is invalid." |
INCORRECT_INFORMATION |
"The request contains incorrect information." |
NOT_UNIQUE |
"The request is not unique." |
AUTHORIZATION_DENIED |
"Authorization has been denied." |
System Errors:
| Constant | Value |
|---|---|
UNHANDLED_EXCEPTION |
"The system detected an unknown error." |
EXCEPTION_READING_REQUEST |
"An exception occurred while trying to read the request dump contents." |
GENERIC_ERROR |
"An error occurred." |
EMAIL_ENCODER_ERROR |
"An error occurred while trying to encrypt or decrypt an email address" |
PASSWORD_ENCODER_ERROR |
"An error occurred while trying to encrypt or decrypt a password" |
CRUD Operation Outcomes:
| Constant | Value |
|---|---|
INSERT_SUCCESS |
"The request is successfully added to database." |
UPDATE_SUCCESS |
"The request is successfully updated to database." |
DELETE_SUCCESS |
"The request is successfully deleted from database." |
INSERT_FAILURE |
"The request failed to add to database." |
UPDATE_FAILURE |
"The request failed to update the database." |
PARTIAL_UPDATE_FAILURE |
"Some instances failed to update the database while others were successful." |
DELETE_FAILURE |
"The request failed to delete from database." |
Package: com.bloomscorp.hastar.support
A non-instantiable utility class holding primitive library-wide sentinel values.
| Constant | Value | Usage |
|---|---|---|
BLANK_STRING_VALUE |
"" |
Default fallback returned by ActionCode.message() when an unrecognized code is passed. |
Use ActionCode constants as return values from your service or repository layer to represent the outcome of a database operation. Pair with ActionCode.message() to get the corresponding string for the API response.
// In a service class
public int saveUser(UserDto dto) {
try {
userRepository.save(toEntity(dto));
return ActionCode.INSERT_SUCCESS;
} catch (DataIntegrityViolationException e) {
return ActionCode.NOT_UNIQUE;
} catch (Exception e) {
return ActionCode.INSERT_FAILURE;
}
}
// In a controller or response builder
int result = userService.saveUser(dto);
ApiResponse response = ApiResponse.builder()
.code(result)
.message(ActionCode.message(result))
.build();Use CarrierException to wrap low-level exceptions at the boundary where they are caught, attaching an ErrorCode so the global handler can interpret them without inspecting the original exception type.
// In a service class
public String encodeUserEmail(String email) {
try {
return emailEncoder.encode(email);
} catch (Exception e) {
throw new CarrierException(e, ErrorCode.EMAIL_ENCODER);
}
}
// In a @ControllerAdvice
@ExceptionHandler(CarrierException.class)
public ResponseEntity<ApiResponse> handleCarrierException(CarrierException ex) {
log.error("System error [code={}]: {}", ex.getErrorCode(), ex.getOrigin().getMessage());
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ApiResponse.error(ErrorCode.decode(ex.getErrorCode())));
}Throw AuthorizationException from a JWT filter when a specific precondition is not met. The getMessage() override handles translating the code to a message automatically.
// In a JWT authentication filter
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new AuthorizationException(AuthorizationException.AECx02);
}
String token = authHeader.substring(7);
if (!jwtService.isValid(token)) {
throw new AuthorizationException(AuthorizationException.AECx05);
}
String username = jwtService.extractUsername(token);
if (username == null) {
throw new AuthorizationException(AuthorizationException.AECx03);
}
// proceed...
chain.doFilter(request, response);
}
// In a @ControllerAdvice or AuthenticationEntryPoint
@ExceptionHandler(AuthorizationException.class)
public ResponseEntity<ApiResponse> handleAuthorizationException(AuthorizationException ex) {
// getMessage() resolves automatically from the error code
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(ApiResponse.error(ex.getMessage()));
}Throw NoUserFoundException from a UserDetailsService implementation when no account matches the supplied username. The username is captured for logging.
// In a UserDetailsService implementation
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByUsername(username)
.map(UserPrincipal::from)
.orElseThrow(() -> new NoUserFoundException(username));
}
// In a @ControllerAdvice
@ExceptionHandler(NoUserFoundException.class)
public ResponseEntity<ApiResponse> handleNoUserFound(NoUserFoundException ex) {
log.warn("Login attempt for unknown username: {}", ex.getUsername());
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(ApiResponse.error(Message.USERNAME_NOT_FOUND));
}A complete @ControllerAdvice that handles all three exception types from this library in one place:
@RestControllerAdvice
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(CarrierException.class)
public ResponseEntity<ApiResponse> onCarrierException(CarrierException ex) {
log.error("CarrierException [code={}]", ex.getErrorCode(), ex.getOrigin());
return ResponseEntity
.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(new ApiResponse(ex.getErrorCode(), ErrorCode.decode(ex.getErrorCode())));
}
@ExceptionHandler(AuthorizationException.class)
public ResponseEntity<ApiResponse> onAuthorizationException(AuthorizationException ex) {
log.warn("AuthorizationException [code={}]: {}", ex.getErrorCode(), ex.getMessage());
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(new ApiResponse(ex.getErrorCode(), ex.getMessage()));
}
@ExceptionHandler(NoUserFoundException.class)
public ResponseEntity<ApiResponse> onNoUserFound(NoUserFoundException ex) {
log.warn("No account found for username: {}", ex.getUsername());
return ResponseEntity
.status(HttpStatus.UNAUTHORIZED)
.body(new ApiResponse(ActionCode.UNAUTHORIZED_ACCESS, Message.USERNAME_NOT_FOUND));
}
}Message constants can also be used directly anywhere in a consuming application — in log statements, response builders, or validation utilities — to maintain wording consistency across services.
// In a validation utility
if (dto.getEmail() == null || dto.getPassword() == null) {
return ApiResponse.failure(ActionCode.INCORRECT_INFORMATION, Message.INCOMPLETE_INFORMATION);
}
// In a Spring Security AuthenticationFailureHandler
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws IOException {
String msg;
if (exception instanceof BadCredentialsException) {
msg = Message.BAD_CREDENTIALS;
} else if (exception instanceof DisabledException) {
msg = Message.ACCOUNT_DISABLED;
} else if (exception instanceof LockedException) {
msg = Message.USER_ACCOUNT_LOCKED;
} else {
msg = Message.INTERNAL_AUTHENTICATION_SERVICE_FAILURE;
}
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write(msg);
}No Spring dependency. The library is intentionally pure Java. It has no compile-time dependency on spring-boot, spring-security, or spring-web. This avoids version conflicts and makes the library consumable by any Java 21 project.
Non-instantiable utility classes. Message, Constant, ActionCode, and ErrorCode all have private constructors. They are pure static namespaces — they hold no state and are not designed to be extended or injected.
Code sign convention. ActionCode uses a deliberate sign convention: > 0 is success, 0 is no-op, < 0 is failure. ErrorCode is always negative. This means you can safely combine both in a single integer field in an API response DTO without collision, as long as you document which class the code belongs to.
MALFORMED_PAYLOAD and FORGED_REQUEST share a message. This is intentional. Distinguishing between a malformed payload and a forged/tampered request in a user-visible message would expose internal security logic. Both decode to Message.MALFORMED_INFORMATION for the consumer.
Lombok usage. CarrierException, AuthorizationException, and NoUserFoundException use Lombok's @Getter and @AllArgsConstructor. If your project does not have Lombok configured, ensure the annotation processor is set up in your build tool.
| Dependency | Version | Scope |
|---|---|---|
org.projectlombok:lombok |
1.18.36 |
Optional (annotation processor) |
org.jetbrains:annotations |
26.0.1 |
Compile |
BMX Hastar is released under the MIT License.
Copyright © 2023 Bloomscorp