Practical step-by-step guide for building Application Layer microservices
This guide provides hands-on examples and detailed instructions for using lib-common-application to build production-ready microservices.
- 📦 Getting Started
- Add Dependency
- Configure Properties
- Project Structure
- 🛠️ Basic Implementation
- Step 1: Implement ContextResolver
- Step 2: Implement ConfigResolver
- Step 3: Implement AuthorizationService
- Step 4: Create Application Service
- Step 5: Create REST Controller
- Step 6: Configure Security
- 🎨 Advanced Patterns
- Multi-Step Workflows
- Compensating Transactions
- Feature Flags
- Circuit Breakers
- Event Publishing
- 🧪 Testing
- Unit Testing
- Integration Testing
- Security Testing
- Mock Strategies
- 🔍 Troubleshooting
- Common Issues
- Debug Tips
- Performance Tuning
- FAQ
Add the library to your pom.xml:
<dependency>
<groupId>org.fireflyframework</groupId>
<artifactId>lib-common-application</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>In your application.yml:
firefly:
application:
security:
enabled: true
use-security-center: true
default-roles: []
context:
cache-enabled: true
cache-ttl: 300 # seconds
cache-max-size: 1000
config:
cache-enabled: true
cache-ttl: 600 # secondsCreate a context resolver for your microservice:
package com.mycompany.myservice.application.resolver;
import org.fireflyframework.application.context.AppContext;
import org.fireflyframework.application.resolver.AbstractContextResolver;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.UUID;
/**
* Custom context resolver that extracts context from JWT tokens.
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class JwtContextResolver extends AbstractContextResolver {
@Override
public Mono<UUID> resolvePartyId(ServerWebExchange exchange) {
return ReactiveSecurityContextHolder.getContext()
.map(securityContext -> securityContext.getAuthentication())
.filter(auth -> auth.getPrincipal() instanceof Jwt)
.map(auth -> (Jwt) auth.getPrincipal())
.map(jwt -> jwt.getClaimAsString("partyId"))
.map(UUID::fromString)
.switchIfEmpty(extractUUID(exchange, "partyId", "X-Party-Id"));
}
@Override
public Mono<UUID> resolveTenantId(ServerWebExchange exchange) {
return ReactiveSecurityContextHolder.getContext()
.map(securityContext -> securityContext.getAuthentication())
.filter(auth -> auth.getPrincipal() instanceof Jwt)
.map(auth -> (Jwt) auth.getPrincipal())
.map(jwt -> jwt.getClaimAsString("tenantId"))
.map(UUID::fromString)
.switchIfEmpty(extractUUID(exchange, "tenantId", "X-Tenant-Id"));
}
@Override
public Mono<UUID> resolveContractId(ServerWebExchange exchange) {
// First try path variable, then header
return extractUUIDFromPath(exchange, "contractId")
.switchIfEmpty(extractUUID(exchange, "contractId", "X-Contract-Id"));
}
@Override
public Mono<UUID> resolveProductId(ServerWebExchange exchange) {
// First try path variable, then header
return extractUUIDFromPath(exchange, "productId")
.switchIfEmpty(extractUUID(exchange, "productId", "X-Product-Id"));
}
@Override
public boolean supports(ServerWebExchange exchange) {
// This resolver supports requests with JWT authentication
return exchange.getRequest().getHeaders().containsKey("Authorization");
}
@Override
public int getPriority() {
return 100; // Higher priority than default
}
}Create a config resolver that integrates with your config management service:
package com.mycompany.myservice.application.resolver;
import org.fireflyframework.application.context.AppConfig;
import org.fireflyframework.application.resolver.AbstractConfigResolver;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* Custom config resolver that fetches tenant configuration.
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class TenantConfigResolver extends AbstractConfigResolver {
// TODO: Inject config-mgmt SDK client
// private final ConfigManagementClient configClient;
@Override
protected Mono<AppConfig> fetchConfigFromPlatform(UUID tenantId) {
// TODO: Implement using common-platform-config-mgmt-sdk
/*
return configClient.getTenantConfig(tenantId)
.map(response -> AppConfig.builder()
.tenantId(response.getTenantId())
.tenantName(response.getName())
.providers(convertProviders(response.getProviders()))
.featureFlags(response.getFeatureFlags())
.settings(response.getSettings())
.environment(response.getEnvironment())
.active(response.isActive())
.build());
*/
log.info("Fetching configuration for tenant: {}", tenantId);
// Temporary implementation for development
Map<String, AppConfig.ProviderConfig> providers = new HashMap<>();
providers.put("PAYMENT_GATEWAY", AppConfig.ProviderConfig.builder()
.providerType("PAYMENT_GATEWAY")
.implementation("StripeProvider")
.enabled(true)
.properties(Map.of(
"apiKey", "sk_test_...",
"webhookSecret", "whsec_..."
))
.build());
return Mono.just(AppConfig.builder()
.tenantId(tenantId)
.tenantName("Default Tenant")
.providers(providers)
.featureFlags(Map.of("NEW_TRANSFER_FLOW", true))
.settings(Map.of("currency", "USD"))
.environment("development")
.active(true)
.build());
}
}Create an authorization service:
package com.mycompany.myservice.application.security;
import org.fireflyframework.application.security.AbstractSecurityAuthorizationService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* Custom authorization service with SecurityCenter integration.
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class CustomAuthorizationService extends AbstractSecurityAuthorizationService {
// TODO: Inject SecurityCenter client when available
// private final SecurityCenterClient securityCenterClient;
// The base class provides default implementation
// Override authorizeWithSecurityCenter() when SecurityCenter is ready
}Create an application service that extends AbstractApplicationService:
package com.mycompany.myservice.application.service;
import org.fireflyframework.application.context.ApplicationExecutionContext;
import org.fireflyframework.application.resolver.ConfigResolver;
import org.fireflyframework.application.resolver.ContextResolver;
import org.fireflyframework.application.security.SecurityAuthorizationService;
import org.fireflyframework.application.service.AbstractApplicationService;
import com.mycompany.myservice.domain.model.Transfer;
import com.mycompany.myservice.domain.service.AccountDomainService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* Application service for account operations.
* Orchestrates business processes and manages context.
*/
@Service
@Slf4j
public class AccountApplicationService extends AbstractApplicationService {
private final AccountDomainService accountDomainService;
public AccountApplicationService(
ContextResolver contextResolver,
ConfigResolver configResolver,
SecurityAuthorizationService authorizationService,
AccountDomainService accountDomainService
) {
super(contextResolver, configResolver, authorizationService);
this.accountDomainService = accountDomainService;
}
/**
* Transfer funds between accounts.
* This method orchestrates the entire transfer process including:
* - Context resolution
* - Security validation
* - Business logic execution
*
* @param exchange the server web exchange
* @param request the transfer request
* @return Mono of Transfer result
*/
public Mono<Transfer> transferFunds(ServerWebExchange exchange, TransferRequest request) {
return resolveExecutionContext(exchange)
// Validate that we have contract and product context
.flatMap(context -> validateContext(context, true, true))
// Check permissions
.flatMap(context -> requirePermission(context, "TRANSFER_FUNDS")
.thenReturn(context))
// Check feature flag
.flatMap(context -> isFeatureEnabled(context, "NEW_TRANSFER_FLOW")
.flatMap(enabled -> enabled
? executeNewTransferFlow(context, request)
: executeLegacyTransferFlow(context, request)));
}
private Mono<Transfer> executeNewTransferFlow(
ApplicationExecutionContext context,
TransferRequest request
) {
log.info("Executing new transfer flow for party: {}", context.getPartyId());
// Get payment provider configuration
return getProviderConfig(context, "PAYMENT_GATEWAY")
.flatMap(providerConfig -> {
log.debug("Using payment provider: {}", providerConfig.getImplementation());
// Orchestrate the transfer
return accountDomainService.transfer(
context.getContext(),
request.getFromAccount(),
request.getToAccount(),
request.getAmount()
);
});
}
private Mono<Transfer> executeLegacyTransferFlow(
ApplicationExecutionContext context,
TransferRequest request
) {
log.info("Executing legacy transfer flow for party: {}", context.getPartyId());
return accountDomainService.legacyTransfer(
context.getContext(),
request.getFromAccount(),
request.getToAccount(),
request.getAmount()
);
}
}Create a controller that uses the @Secure annotation:
package com.mycompany.myservice.presentation.controller;
import org.fireflyframework.application.security.annotation.Secure;
import com.mycompany.myservice.application.service.AccountApplicationService;
import com.mycompany.myservice.domain.model.Transfer;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import jakarta.validation.Valid;
/**
* REST controller for account operations.
*/
@RestController
@RequestMapping("/api/v1/accounts")
@RequiredArgsConstructor
@Slf4j
@Secure(roles = {"ACCOUNT_HOLDER", "ACCOUNT_ADMIN"})
public class AccountController {
private final AccountApplicationService applicationService;
/**
* Transfer funds between accounts.
* Requires ACCOUNT_HOLDER role and TRANSFER_FUNDS permission.
*/
@PostMapping("/{accountId}/transfer")
@Secure(
roles = "ACCOUNT_HOLDER",
permissions = "TRANSFER_FUNDS",
description = "Transfer funds between accounts"
)
public Mono<Transfer> transferFunds(
@PathVariable UUID accountId,
@Valid @RequestBody TransferRequest request,
ServerWebExchange exchange
) {
log.info("Transfer request received for account: {}", accountId);
return applicationService.transferFunds(exchange, request);
}
/**
* Get account balance.
* Requires any account-related role.
*/
@GetMapping("/{accountId}/balance")
@Secure(
roles = {"ACCOUNT_HOLDER", "ACCOUNT_VIEWER"},
requireAllRoles = false // Any of the roles is sufficient
)
public Mono<BalanceResponse> getBalance(
@PathVariable UUID accountId,
ServerWebExchange exchange
) {
return applicationService.getBalance(exchange, accountId);
}
/**
* Public endpoint that doesn't require authentication.
*/
@GetMapping("/public/rates")
@Secure(allowAnonymous = true)
public Mono<RatesResponse> getExchangeRates() {
return applicationService.getPublicRates();
}
}Register security rules programmatically:
package com.mycompany.myservice.config;
import org.fireflyframework.application.security.EndpointSecurityRegistry;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import java.util.Set;
@Component
public class SecurityConfiguration {
private final EndpointSecurityRegistry registry;
public SecurityConfiguration(EndpointSecurityRegistry registry) {
this.registry = registry;
}
@EventListener(ApplicationReadyEvent.class)
public void registerSecurityRules() {
// Register endpoint security
registry.registerEndpoint(
"/api/v1/admin/users",
"POST",
EndpointSecurityRegistry.EndpointSecurity.builder()
.roles(Set.of("ADMIN", "USER_MANAGER"))
.requireAllRoles(false)
.build()
);
registry.registerEndpoint(
"/api/v1/accounts/{id}/close",
"DELETE",
EndpointSecurityRegistry.EndpointSecurity.builder()
.roles(Set.of("ACCOUNT_OWNER"))
.permissions(Set.of("CLOSE_ACCOUNT"))
.requireAllRoles(true)
.requireAllPermissions(true)
.build()
);
}
}Override enrichment logic for special cases:
@Component
public class EnrichedContextResolver extends AbstractContextResolver {
@Override
protected Mono<AppContext> enrichContext(
AppContext basicContext,
ServerWebExchange exchange,
AppMetadata metadata
) {
// Call parent enrichment first
return super.enrichContext(basicContext, exchange, metadata)
.flatMap(context -> {
// Add custom enrichment
Map<String, Object> attributes = new HashMap<>();
attributes.put("ipAddress", metadata.getClientIp());
attributes.put("userAgent", metadata.getUserAgent());
return Mono.just(context.toBuilder()
.attributes(attributes)
.build());
});
}
}Orchestrate complex multi-step processes:
@Service
public class LoanApplicationService extends AbstractApplicationService {
private final CreditCheckService creditCheckService;
private final DocumentService documentService;
private final ApprovalService approvalService;
public Mono<LoanApplication> processLoanApplication(
ServerWebExchange exchange,
LoanApplicationRequest request
) {
return resolveExecutionContext(exchange)
// Step 1: Validate context
.flatMap(ctx -> validateContext(ctx, true, true))
// Step 2: Check credit
.flatMap(ctx -> creditCheckService.performCreditCheck(ctx, request)
.map(creditResult -> new ProcessState(ctx, request, creditResult)))
// Step 3: Verify documents
.flatMap(state -> documentService.verifyDocuments(state.context, state.request)
.map(docResult -> state.withDocumentResult(docResult)))
// Step 4: Get approval
.flatMap(state -> approvalService.getApproval(state.context, state)
.map(approval -> state.withApproval(approval)))
// Step 5: Create loan if approved
.flatMap(state -> state.approval.isApproved()
? createLoan(state)
: Mono.error(new LoanRejectedException(state.approval.getReason())));
}
@lombok.Value
@lombok.With
private static class ProcessState {
ApplicationExecutionContext context;
LoanApplicationRequest request;
CreditCheckResult creditResult;
DocumentVerificationResult documentResult;
ApprovalResult approval;
ProcessState(ApplicationExecutionContext context,
LoanApplicationRequest request,
CreditCheckResult creditResult) {
this.context = context;
this.request = request;
this.creditResult = creditResult;
this.documentResult = null;
this.approval = null;
}
}
}Handle failures with compensating transactions:
public Mono<TransferResult> transferWithCompensation(
ApplicationExecutionContext context,
TransferRequest request
) {
return accountDomainService.debit(context.getContext(), request.getFromAccount(), request.getAmount())
.flatMap(debitResult ->
accountDomainService.credit(context.getContext(), request.getToAccount(), request.getAmount())
.onErrorResume(creditError -> {
// Compensate: reverse the debit
log.error("Credit failed, compensating debit", creditError);
return accountDomainService.credit(
context.getContext(),
request.getFromAccount(),
request.getAmount()
).then(Mono.error(creditError));
})
);
}@ExtendWith(MockitoExtension.class)
class JwtContextResolverTest {
@Mock
private ServerWebExchange exchange;
@Mock
private ServerHttpRequest request;
@InjectMocks
private JwtContextResolver resolver;
@Test
void shouldResolvePartyIdFromJwt() {
// Given
UUID expectedPartyId = UUID.randomUUID();
Jwt jwt = createJwtWithPartyId(expectedPartyId);
when(exchange.getRequest()).thenReturn(request);
mockSecurityContext(jwt);
// When
StepVerifier.create(resolver.resolvePartyId(exchange))
// Then
.expectNext(expectedPartyId)
.verifyComplete();
}
}@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureWebTestClient
class AccountApplicationServiceIntegrationTest {
@Autowired
private WebTestClient webClient;
@Test
@WithMockJwtAuth(partyId = "123e4567-e89b-12d3-a456-426614174000", roles = {"ACCOUNT_HOLDER"})
void shouldTransferFunds() {
TransferRequest request = new TransferRequest(
fromAccount,
toAccount,
BigDecimal.valueOf(100.00)
);
webClient.post()
.uri("/api/v1/accounts/{id}/transfer", accountId)
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(request)
.exchange()
.expectStatus().isOk()
.expectBody(Transfer.class)
.value(transfer -> {
assertThat(transfer.getAmount()).isEqualTo(request.getAmount());
assertThat(transfer.getStatus()).isEqualTo(TransferStatus.COMPLETED);
});
}
}Problem: ApplicationExecutionContext cannot be resolved.
Solution:
- Verify that your
ContextResolveris properly registered as a Spring bean - Check that the
supports()method returns true for your requests - Verify authentication headers/tokens are present
- Enable debug logging:
logging.level.org.fireflyframework.application=DEBUG
Problem: Receiving 403 Forbidden despite having correct roles.
Solution:
- Verify roles are being resolved correctly in
resolveRoles() - Check that permissions are being resolved in
resolvePermissions() - Ensure
@Secureannotation is being processed (AOP is enabled) - Check security configuration in application.yml
Problem: AppConfig is empty or missing provider configurations.
Solution:
- Verify
ConfigResolveris properly implemented - Check connection to config-mgmt service
- Verify cache settings in application.yml
- Call
refreshConfig()to force reload
Problem: @Secure annotation is ignored.
Solution:
- Verify
@EnableAspectJAutoProxyis present - Check that
SecurityAspectis registered as a bean - Ensure methods are being called from outside the class (AOP proxy)
- Verify AspectJ dependencies are on classpath
Enable verbose logging:
logging:
level:
org.fireflyframework.application: DEBUG
org.fireflyframework.application.aop: TRACE
org.fireflyframework.application.resolver: DEBUG
org.fireflyframework.application.security: DEBUGUse Actuator for introspection:
management:
endpoints:
web:
exposure:
include: health,info,metrics,beans,mappingsMonitor metrics:
@Component
public class ApplicationMetrics {
private final MeterRegistry meterRegistry;
public void recordContextResolution(String resolver, long duration) {
meterRegistry.timer("app.context.resolution",
"resolver", resolver)
.record(Duration.ofMillis(duration));
}
}- Review ARCHITECTURE.md for architectural patterns
- Read API_REFERENCE.md for detailed API documentation
- Check EXAMPLES.md for more code examples
- See MIGRATION_GUIDE.md for upgrading existing applications