Deepen PracticeOrdered learning track

Identifying Seams in Legacy Java Systems

Learn Java Microservices Design and Architect - Part 078

Identifying seams in legacy Java systems: transaction seams, domain seams, API seams, UI seams, data seams, batch seams, dependency seams, observability seams, and extraction readiness.

15 min read2999 words
PrevNext
Lesson 78100 lesson track55–82 Deepen Practice
#java#microservices#legacy-code#modernization+6 more

Part 078 — Identifying Seams in Legacy Java Systems

1. Core Idea

A seam is a place where you can change, observe, intercept, replace, or redirect behavior without rewriting the entire system.

In legacy Java modernization, seams are more important than services.

A team that cannot identify seams will usually extract the wrong thing.

The weak approach:

Find classes.
Move classes into new service.
Expose REST API.

The strong approach:

Find behavior boundaries.
Find dependency boundaries.
Find transaction boundaries.
Find data authority boundaries.
Find routing boundaries.
Find test boundaries.
Create seams where none exist.
Extract only after the seam is safe.

A seam is not always a clean interface.

It can be:

  • an HTTP endpoint,
  • a controller method,
  • a package boundary,
  • a database table boundary,
  • a transaction boundary,
  • a batch job boundary,
  • a message topic,
  • a feature flag,
  • a facade,
  • an adapter,
  • a stored procedure boundary,
  • a UI route,
  • a scheduled workflow step,
  • a domain event,
  • a test harness,
  • a proxy route,
  • a module API.

The point is not elegance.

The point is controlled change.

2. Why Seam Discovery Comes Before Service Extraction

A microservice boundary is expensive.

It adds:

  • network calls,
  • independent deployment,
  • data ownership rules,
  • observability requirements,
  • failure handling,
  • security boundaries,
  • versioning,
  • operational ownership.

Before paying that cost, prove that the behavior can be separated.

Seam discovery answers:

Can this behavior be observed?
Can this behavior be tested?
Can this behavior be isolated?
Can this behavior be routed?
Can this behavior be replaced?
Can this behavior be rolled back?
Can this behavior own its data?

If the answer is no, extraction is premature.

3. The Seam Map

A legacy Java application can be mapped through multiple seam layers.

Do not look for only one seam.

A safe extraction often requires several seams aligned.

Example:

A route seam without a data seam creates data coupling.
A data seam without a test seam creates behavior risk.
A transaction seam without observability creates unknown outcome risk.
A module seam without ownership seam creates organizational conflict.

4. Seam Inventory Template

Use this table for each candidate capability.

Seam typeEvidenceRiskAction
API seam/cases/{id}/assessment endpointMany callersAdd contract tests
Application seamAssessmentService methodCalls decision package directlyIntroduce port
Transaction seamOne transaction updates case + assessment + auditCross-domain writeSplit audit via outbox
Data seamcase_assessment table mostly isolatedFK to caseReplace FK with reference ID boundary
Batch seamNightly reassessment job updates same rowsHidden writerMove job behind same port
Integration seamCalls external registryTimeout riskIntroduce client adapter
Observability seamLogs missing command IDDebug riskAdd structured log/trace
Test seamNo tests around scoring rulesRegression riskAdd characterization tests

A seam without evidence is just an assumption.

5. Source Types for Seam Discovery

Use both static and runtime evidence.

5.1 Static Evidence

Collect from:

  • package dependencies,
  • class dependencies,
  • method call graph,
  • Maven modules,
  • Spring bean graph,
  • JPA entity relationships,
  • SQL queries,
  • repository interfaces,
  • database schema,
  • stored procedures,
  • scheduled job definitions,
  • controller routes,
  • message listeners,
  • configuration files,
  • feature flags,
  • code ownership files,
  • Git commit history.

Static evidence tells you what can happen.

5.2 Runtime Evidence

Collect from:

  • access logs,
  • distributed traces,
  • SQL statement logs,
  • APM call graphs,
  • production traffic profiles,
  • message consumption metrics,
  • batch job history,
  • error logs,
  • slow query logs,
  • audit logs,
  • user journey analytics,
  • incident timelines.

Runtime evidence tells you what actually happens.

5.3 Human Evidence

Collect from:

  • domain experts,
  • support teams,
  • operations teams,
  • compliance teams,
  • DBAs,
  • product owners,
  • long-tenured engineers,
  • incident commanders,
  • customer support transcripts.

Human evidence tells you why behavior exists.

A top-tier modernization effort combines all three.

6. Seam Type 1 — UI Route Seam

A UI route seam exists when a user journey can be routed to different backend behavior.

Example:

/cases/{caseId}/assessment
/cases/{caseId}/evidence
/cases/{caseId}/decision

Useful for strangler migration because you can route one page/journey at a time.

Good signs:

  • route maps to clear user journey,
  • user journey has clear ownership,
  • data needed by route can be isolated or composed,
  • rollback can route back to legacy.

Bad signs:

  • route depends on huge shared session state,
  • UI screen mixes unrelated capabilities,
  • many hidden AJAX calls hit legacy endpoints,
  • server-side rendering and business logic are tangled.

Java legacy examples:

  • JSF backing bean with business logic,
  • Struts action class with direct DAO calls,
  • JSP reading session attributes mutated by services,
  • Spring MVC controller returning server-rendered template.

Refactoring move:

Extract view model builder behind a port.
Separate screen composition from business mutation.
Move command behavior behind explicit application service.

7. Seam Type 2 — HTTP/API Seam

An HTTP/API seam exists when external callers already interact through a stable endpoint.

Example:

@RestController
@RequestMapping("/api/case-assessments")
class CaseAssessmentController {

    private final CaseAssessmentUseCase useCase;

    @PostMapping("/{caseId}/submit")
    ResponseEntity<AssessmentResponse> submit(
            @PathVariable String caseId,
            @RequestBody SubmitAssessmentRequest request) {
        return ResponseEntity.accepted().body(useCase.submit(caseId, request));
    }
}

Good API seam:

  • endpoint expresses business intent,
  • request/response contract is stable,
  • caller identity is known,
  • validation boundary is clear,
  • error semantics are documented,
  • idempotency is possible,
  • route can be proxied.

Weak API seam:

  • endpoint exposes database entity shape,
  • caller depends on undocumented fields,
  • endpoint returns giant aggregate graph,
  • endpoint requires shared session,
  • endpoint performs many unrelated side effects,
  • no contract tests exist.

Extraction move:

Put a facade/proxy in front of API.
Add contract tests.
Route selected endpoint/cohort to new service.
Keep response semantics compatible during migration.

8. Seam Type 3 — Application Service Seam

An application service seam exists when a use case is already coordinated from one method/class.

Good example:

public final class SubmitAssessmentHandler {

    private final CaseAssessmentRepository assessments;
    private final CaseRepository cases;
    private final AuditEventPort auditEvents;
    private final Clock clock;

    @Transactional
    public SubmitAssessmentResult handle(SubmitAssessmentCommand command) {
        CaseRecord caseRecord = cases.getForUpdate(command.caseId());
        CaseAssessment assessment = assessments.getForUpdate(command.assessmentId());

        assessment.submit(command.officerId(), clock.instant());

        assessments.save(assessment);
        auditEvents.record(AuditEvent.assessmentSubmitted(command.caseId(), command.officerId()));

        return SubmitAssessmentResult.accepted(assessment.id());
    }
}

This is a useful seam because orchestration is explicit.

Bad example:

@PostMapping("/submit")
public String submit(HttpServletRequest request) {
    var conn = dataSource.getConnection();
    // validation, DB writes, external calls, audit, notification, and view rendering here
    return "assessmentSubmitted";
}

This has no seam.

Create one by extracting command handling.

8.1 Application Seam Refactoring

Step-by-step:

1. Identify one use case.
2. Create command object.
3. Move orchestration into handler/service.
4. Hide external calls behind ports.
5. Hide persistence behind repositories.
6. Add characterization tests.
7. Add structured logging around command lifecycle.
8. Make controller thin.

A thin controller is not the goal by itself.

It is a sign that a use-case seam exists.

9. Seam Type 4 — Domain Seam

A domain seam exists when a group of behavior protects a coherent set of business invariants.

Example invariant:

An enforcement decision cannot be finalized unless:
- case is in ASSESSMENT_COMPLETED state,
- required evidence snapshot exists,
- officer has decision authority,
- decision reason is recorded,
- audit event is emitted.

If these rules live together, you may have a domain seam.

If they are scattered across controllers, SQL triggers, and UI validation, you do not yet have one.

9.1 Domain Seam Discovery Questions

Ask:

  • Which commands change this domain state?
  • Which states exist?
  • Which transitions are legal?
  • Which invariants must never be violated?
  • Which data must be loaded together to check invariants?
  • Which language does the domain use?
  • Which team/business role owns the rules?
  • Which events are produced when state changes?
  • Which external systems should not leak into the domain language?

9.2 Domain Seam Diagram

A state machine is often a strong seam detector.

If a lifecycle can be named, owned, and guarded, it may be a service candidate.

10. Seam Type 5 — Transaction Seam

A transaction seam is where atomicity starts and ends.

In legacy Java, transaction seams are often hidden by:

  • @Transactional on broad service methods,
  • EJB container-managed transactions,
  • programmatic transaction templates,
  • Open Session in View,
  • stored procedure transactions,
  • batch chunk transactions,
  • database triggers.

10.1 Transaction Closure

For each candidate command, map:

Command
  -> tables read
  -> rows locked
  -> tables written
  -> triggers fired
  -> events emitted
  -> cache invalidated
  -> external calls made
  -> audit records written
  -> batch jobs affected

Example:

Extraction is risky when one transaction updates multiple future service-owned areas.

10.2 Creating a Transaction Seam

Options:

  • split command into local transaction + event,
  • move audit write to outbox-driven consumer,
  • replace cross-table update with projection,
  • introduce optimistic locking,
  • remove external call from transaction,
  • isolate batch update behind same application service,
  • create command status table for async flow.

Bad sign:

The new service cannot perform its command without a distributed transaction with the monolith.

That means the seam is not ready.

11. Seam Type 6 — Data Seam

A data seam exists when data authority can be separated.

Look for:

  • tables mostly written by one capability,
  • clear lifecycle ownership,
  • low FK entanglement,
  • few shared writes,
  • stable reference IDs,
  • clear read-only consumers,
  • natural event stream,
  • rebuildable read models.

Bad data seam signs:

  • table is written by many modules,
  • stored procedures mutate it indirectly,
  • reporting joins depend on it everywhere,
  • no one knows source of truth,
  • many nullable columns belong to different business concepts,
  • table represents multiple lifecycles.

11.1 Table Ownership Map

Create a matrix:

TableWritersReadersLifecycle ownerCandidate serviceRisk
caseintake, assessment, decision, batcheveryoneunclearnone yethigh
case_assessmentassessment moduledecision, reportingassessmentassessment-servicemedium
notification_lognotification modulesupport, reportingnotificationnotification-servicelow
audit_logmany modulescomplianceaudit platformaudit-service/read modelhigh

Data seams often reveal that entity names are misleading.

The case table may contain multiple lifecycles, not one clean aggregate.

11.2 Foreign Key Entanglement

Foreign keys are not bad inside one boundary.

They become migration constraints across boundaries.

Options:

  • keep FK inside same service-owned schema,
  • replace cross-boundary FK with reference ID,
  • enforce referential validity through command/API checks,
  • use snapshot for historical evidence,
  • create read model for composition,
  • keep monolith-owned table until boundary matures.

12. Seam Type 7 — Batch and Scheduler Seam

Legacy batch jobs are common hidden writers.

Examples:

Recalculate case risk score nightly.
Close expired tasks.
Escalate overdue cases.
Sync party data from external registry.
Generate regulatory report.
Archive old evidence.

If a batch job writes data for a candidate service, that service does not fully own the data yet.

12.1 Batch Seam Discovery

Inventory:

  • job name,
  • schedule,
  • owner,
  • input tables/files,
  • output tables/files,
  • transaction model,
  • retry behavior,
  • idempotency,
  • failure handling,
  • downstream impact,
  • manual rerun process.

12.2 Batch Refactoring Pattern

Before extraction:

Batch Job -> Direct SQL writes

After seam creation:

Batch Job -> Application Port -> Domain/Application Service -> Repository

Eventually:

Scheduler -> Message/Command -> Extracted Service Worker

This prevents direct data mutation outside service authority.

13. Seam Type 8 — External Integration Seam

External systems often contaminate domain logic.

Examples:

  • identity provider,
  • payment provider,
  • external registry,
  • document storage,
  • email/SMS provider,
  • government data exchange,
  • search engine,
  • reporting warehouse.

A clean integration seam hides protocol, vendor vocabulary, timeout behavior, and error mapping.

public interface PartyRegistryPort {
    PartyRegistrySnapshot lookup(PartyRegistryId id);
}

public final class HttpPartyRegistryAdapter implements PartyRegistryPort {
    private final WebClient webClient;

    @Override
    public PartyRegistrySnapshot lookup(PartyRegistryId id) {
        try {
            return webClient.get()
                .uri("/parties/{id}", id.value())
                .retrieve()
                .bodyToMono(ExternalPartyDto.class)
                .timeout(Duration.ofSeconds(2))
                .map(PartyRegistryTranslator::toDomainSnapshot)
                .block();
        } catch (WebClientResponseException.NotFound notFound) {
            throw new PartyNotFoundInRegistry(id);
        } catch (RuntimeException failure) {
            throw new PartyRegistryUnavailable(id, failure);
        }
    }
}

This seam lets you:

  • test domain without external system,
  • change provider,
  • add circuit breaker,
  • add cache,
  • translate error semantics,
  • extract service later.

14. Seam Type 9 — Event Seam

An event seam exists when behavior can be described as meaningful business facts.

Examples:

CaseSubmitted
AssessmentCompleted
DecisionFinalized
EvidenceAttached
NotificationDeliveryFailed
DeadlineExpired

An event seam is useful for:

  • decoupling side effects,
  • building read models,
  • feeding new services,
  • auditing lifecycle changes,
  • migrating consumers gradually.

Bad event seam signs:

  • event names are technical CRUD changes,
  • payload exposes internal table schema,
  • event producer does not own semantic meaning,
  • event has no ID/version/time/source,
  • consumers infer business meaning from optional fields,
  • no replay or dedupe strategy exists.

14.1 Event Extraction Pattern

Step 1: internal domain event.

public record AssessmentCompleted(
    AssessmentId assessmentId,
    CaseId caseId,
    OfficerId completedBy,
    Instant completedAt
) implements DomainEvent {}

Step 2: persist via outbox in same transaction.

@Transactional
public void completeAssessment(CompleteAssessmentCommand command) {
    var assessment = assessments.getForUpdate(command.assessmentId());
    assessment.complete(command.officerId(), clock.instant());

    assessments.save(assessment);
    outbox.append(IntegrationEvent.from(assessment.pullDomainEvents()));
}

Step 3: publish integration event.

{
  "event_id": "evt-2026-0001",
  "event_type": "case.assessment.completed.v1",
  "occurred_at": "2026-07-05T10:30:00Z",
  "producer": "case-monolith",
  "aggregate_id": "assessment-123",
  "payload": {
    "case_id": "CASE-2026-000177",
    "assessment_id": "ASM-123",
    "completed_by": "officer-42"
  }
}

Now new services can consume behavior without direct database access.

15. Seam Type 10 — Observability Seam

You cannot safely extract what you cannot observe.

An observability seam lets you see behavior before replacing it.

Add:

  • command ID,
  • correlation ID,
  • trace ID,
  • business entity ID,
  • lifecycle state,
  • dependency timing,
  • decision result,
  • error category,
  • route decision,
  • migration cohort.

Example structured log:

{
  "event": "assessment.completed",
  "case_id": "CASE-2026-000177",
  "assessment_id": "ASM-123",
  "actor_id": "officer-42",
  "previous_state": "SUBMITTED",
  "new_state": "COMPLETED",
  "duration_ms": 184,
  "trace_id": "4bf92f3577b34da6a3ce929d0e0e4736"
}

If a candidate flow cannot emit stable telemetry, migration risk is high.

16. Seam Type 11 — Test Seam

A test seam lets you verify behavior before changing it.

Legacy code often lacks tests.

Do not wait for perfect unit tests.

Start with characterization tests.

A characterization test captures current behavior, including awkward edge cases, so that migration can detect accidental behavior drift.

16.1 Characterization Test Strategy

1. Select high-value command/query.
2. Capture input fixtures from production-like examples.
3. Execute legacy path in controlled test environment.
4. Assert observable outputs and side effects.
5. Add edge cases discovered from incidents/support tickets.
6. Use same tests against new implementation.

Example:

@Test
void submittingAssessmentCreatesAuditRecordAndTransitionsState() {
    var fixture = CaseAssessmentFixture.submittedAssessment();
    database.insert(fixture);

    legacyAssessmentApi.submit(fixture.caseId(), officer("officer-42"));

    assertThat(database.assessment(fixture.assessmentId()).state())
        .isEqualTo("COMPLETED");

    assertThat(database.auditEventsFor(fixture.caseId()))
        .anyMatch(event -> event.type().equals("ASSESSMENT_COMPLETED"));
}

This is not beautiful.

It is useful.

17. Seam Type 12 — Ownership Seam

A technical seam without ownership is fragile.

Ask:

  • Which team owns the capability?
  • Which team handles incidents?
  • Which team approves schema changes?
  • Which team owns SLOs?
  • Which team owns regulatory evidence?
  • Which team owns runbook updates?
  • Which team deprecates old path?

A service boundary should align with an ownership seam.

If three teams must approve every change, independent deployability is not real.

18. Legacy Java Discovery Workflow

Use a repeatable workflow.

19. Practical Discovery Commands

These are examples, not universal truth.

19.1 Find Spring Controllers

grep -R "@RestController\|@Controller" src/main/java

19.2 Find Transaction Boundaries

grep -R "@Transactional" src/main/java

19.3 Find Scheduled Jobs

grep -R "@Scheduled\|Quartz\|JobLauncher\|SpringBatch" src/main/java src/main/resources

19.4 Find Direct SQL

grep -R "createNativeQuery\|JdbcTemplate\|prepareStatement\|SELECT \|UPDATE \|DELETE " src/main/java

19.5 Find Cross-Package Coupling

Use tools such as jdeps, ArchUnit, or build scans.

Example:

jdeps --recursive --multi-release 21 target/case-monolith.jar

19.6 Find Database Writers

SQL examples:

select
    table_name,
    trigger_name
from information_schema.triggers
where trigger_schema = 'public';
select
    routine_name,
    routine_type
from information_schema.routines
where routine_schema = 'public';

These commands do not replace thinking.

They reveal where to inspect.

20. Seam Readiness Scorecard

Score from 1 to 5.

DimensionQuestion
Entry point clarityDo we know how behavior starts?
Domain clarityDo we know the business capability/invariants?
Transaction clarityDo we know atomic write boundary?
Data ownership clarityDo we know source of truth?
Hidden writer inventoryDo we know jobs/triggers/scripts that mutate data?
Dependency isolationAre external systems behind ports/adapters?
TestabilityCan behavior be verified before/after migration?
ObservabilityCan we compare legacy and new behavior?
Routing abilityCan traffic be shifted gradually?
Rollback abilityCan we route back safely?
Ownership clarityDoes one team own the extracted capability?
Retirement pathDo we know how to remove old behavior?

Interpretation:

Average < 3: do not extract; create seams first.
Average 3-4: extract only low-risk/read-side/peripheral flows.
Average > 4: candidate is ready for controlled extraction.

21. Example: Assessment Capability Seam Discovery

Candidate:

Case Assessment

21.1 Entry Points

  • /cases/{id}/assessment UI page,
  • /api/cases/{id}/assessment/submit endpoint,
  • nightly reassessment job,
  • admin correction screen,
  • CSV import process.

21.2 Data

  • case_assessment,
  • case_assessment_answer,
  • case_risk_score,
  • case_summary,
  • audit_log,
  • workflow_task.

21.3 Hidden Writers

  • nightly risk recalculation,
  • DB trigger updates case_summary,
  • support script updates case_assessment.status,
  • workflow engine changes task state after assessment submission.

21.4 External Dependencies

  • party registry lookup,
  • evidence metadata service,
  • notification provider,
  • identity/role service.

21.5 Seam Risk

SeamStatusRisk
APIClear submit endpointmedium
DomainAssessment state machine exists informallymedium
TransactionUpdates assessment + case summary + audit + workflowhigh
DataAssessment tables separable; summary/audit sharedmedium/high
BatchNightly job writes assessment scorehigh
ObservabilityLogs weak; no command IDhigh
TestFew testshigh
OwnershipAssessment team existslow

Verdict:

Do not extract immediately.
First create application service seam, move batch behind same port, add outbox for audit/workflow side effects, add characterization tests, and add structured observability.

22. Refactoring Toward a Seam

Before:

@Service
public class CaseService {

    @Transactional
    public void submitAssessment(String caseId, Map<String, Object> answers) {
        CaseEntity caseEntity = caseRepository.findById(caseId).orElseThrow();
        AssessmentEntity assessment = caseEntity.getAssessment();

        assessment.setAnswers(answers);
        assessment.setStatus("COMPLETED");
        caseEntity.setStatus("ASSESSMENT_COMPLETED");

        auditRepository.save(new AuditEntity(caseId, "ASSESSMENT_COMPLETED"));
        notificationClient.send(caseEntity.getOwnerEmail(), "Assessment completed");
    }
}

Problems:

  • case and assessment lifecycle mixed,
  • external notification inside transaction,
  • audit storage coupled,
  • raw answer map leaks representation,
  • no domain state transition,
  • no idempotency,
  • no event seam.

After seam creation:

public record SubmitAssessmentCommand(
    CaseId caseId,
    AssessmentId assessmentId,
    OfficerId officerId,
    AssessmentAnswers answers,
    IdempotencyKey idempotencyKey
) {}

public final class SubmitAssessmentHandler {

    private final AssessmentRepository assessments;
    private final CaseStatusPort caseStatus;
    private final OutboxPort outbox;
    private final IdempotencyPort idempotency;
    private final Clock clock;

    @Transactional
    public SubmitAssessmentResult handle(SubmitAssessmentCommand command) {
        return idempotency.execute(command.idempotencyKey(), () -> {
            var assessment = assessments.getForUpdate(command.assessmentId());

            assessment.submit(command.officerId(), command.answers(), clock.instant());

            assessments.save(assessment);
            outbox.append(AssessmentCompletedEvent.from(assessment));

            return SubmitAssessmentResult.completed(assessment.id());
        });
    }
}

Now extraction is easier because:

  • command boundary is explicit,
  • domain transition is local,
  • side effects are event-driven,
  • idempotency is explicit,
  • external effects are not inside DB transaction,
  • tests can target handler/domain behavior.

23. Seams and Anti-Corruption Layers

Sometimes the seam should not expose legacy model directly.

Use an anti-corruption layer when:

  • legacy names differ from domain language,
  • legacy enum values are misleading,
  • legacy data shape is overloaded,
  • external system exposes vendor-specific meaning,
  • new service must not inherit old schema assumptions.

Example:

final class LegacyAssessmentTranslator {

    AssessmentSnapshot toSnapshot(LegacyAssessmentRow row) {
        return new AssessmentSnapshot(
            new AssessmentId(row.assessmentUuid()),
            new CaseId(row.caseReference()),
            translateStatus(row.statusCode()),
            translateRisk(row.riskBucket()),
            row.lastUpdatedAt()
        );
    }

    private AssessmentStatus translateStatus(String statusCode) {
        return switch (statusCode) {
            case "N" -> AssessmentStatus.DRAFT;
            case "S" -> AssessmentStatus.SUBMITTED;
            case "C" -> AssessmentStatus.COMPLETED;
            case "X" -> AssessmentStatus.CANCELLED;
            default -> throw new UnknownLegacyAssessmentStatus(statusCode);
        };
    }
}

A translator is not boilerplate.

It is a semantic firewall.

24. Seams and Feature Flags

A routing seam can be controlled by feature flags.

Example:

public AssessmentResponse submit(SubmitAssessmentRequest request) {
    if (migrationFlags.isEnabled("assessment-service-write", request.tenantId())) {
        return newAssessmentClient.submit(request);
    }

    return legacyAssessmentHandler.submit(request);
}

Use flags for:

  • tenant-based rollout,
  • user cohort rollout,
  • read vs write split,
  • fallback route,
  • shadow execution,
  • emergency disable.

Flag requirements:

  • owner,
  • purpose,
  • expiry,
  • metrics,
  • rollback behavior,
  • audit log for changes.

A migration flag without expiry becomes legacy configuration.

25. Seams and Rollback

A seam is not production-ready unless it supports rollback.

Rollback questions:

  • Can traffic be routed back?
  • Is data still compatible with old path?
  • Have new writes changed state old code cannot understand?
  • Are events duplicated if rollback occurs?
  • Can idempotency prevent duplicate side effects?
  • Can support team identify which path handled a request?

If rollback is impossible, you need stronger pre-cutover validation.

26. Common Seam Discovery Mistakes

26.1 Confusing Class Boundary with Business Boundary

A package named case does not mean it is a bounded context.

Check behavior, data, state, ownership.

26.2 Ignoring Batch Jobs

Request paths are not the whole system.

Batch jobs may be the real owner of important data transitions.

26.3 Trusting Database Schema Names

A table named customer may contain:

  • applicant,
  • suspect,
  • representative,
  • reporting party,
  • organization,
  • contact person.

Names drift.

Behavior reveals meaning.

26.4 Extracting Read Model as Source of Truth

A projection is not authority unless explicitly designed as authority.

26.5 Creating Interfaces Everywhere

Interfaces do not automatically create seams.

A useful seam changes the cost/risk of future change.

26.6 Forgetting Manual Operations

Support scripts, admin screens, and manual DB updates are part of the system.

If they mutate extracted data, they must be migrated or blocked.

27. Seam Creation Roadmap

A practical order:

1. Pick one business capability.
2. Map all entry points.
3. Add command/query naming.
4. Move orchestration into application service.
5. Put external dependencies behind ports.
6. Identify transaction closure.
7. Introduce outbox for side effects.
8. Inventory database writers.
9. Add characterization tests.
10. Add structured observability.
11. Add routing seam or branch-by-abstraction.
12. Run shadow/parallel comparison.
13. Extract only when rollback path is credible.

This roadmap may feel slower than moving code.

It is faster than recovering from a broken extraction.

28. Final Mental Model

Legacy modernization is not primarily about old code.

It is about hidden coupling.

Seams reveal coupling.

Good seams let you:

  • see behavior,
  • test behavior,
  • isolate behavior,
  • route behavior,
  • replace behavior,
  • rollback behavior,
  • eventually retire behavior.

The strongest modernization principle:

Never extract what you cannot first observe, test, and route.

A microservice extracted without seams is just legacy code with network latency.

29. References

  • Martin Fowler — Legacy Seam.
  • Michael Feathers — Working Effectively with Legacy Code.
  • Martin Fowler — Strangler Fig Application.
  • Microsoft Azure Architecture Center — Strangler Fig pattern.
  • AWS Prescriptive Guidance — Strangler fig pattern.
  • Martin Fowler — Branch by Abstraction.
  • Spring Framework documentation — transaction management and web controllers.
  • Kubernetes documentation — service routing and workload operations.
Lesson Recap

You just completed lesson 78 in deepen practice. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.