|
| 1 | +--- |
| 2 | +title: Domain Coupling |
| 3 | +sidebar_position: 5 |
| 4 | +--- |
| 5 | + |
| 6 | +# Domain Coupling and Historical Immutability |
| 7 | + |
| 8 | +## Why this matters |
| 9 | + |
| 10 | +Here's something that might surprise you: Changes that ran successfully in the past can break your build today. This happens when Changes depend on domain classes that evolve over time. Let's understand why this matters and how to keep your Changes stable. |
| 11 | + |
| 12 | +## The coupling problem |
| 13 | + |
| 14 | +Changes in Flamingock are meant to be **historically immutable** - they represent past changes that have been applied and audited. Their code should remain untouched over time to ensure: |
| 15 | + |
| 16 | +- **Repeatability**: The same Change produces the same result |
| 17 | +- **Auditability**: Historical changes can be verified |
| 18 | +- **Reliability**: Past Changes continue to work in new environments |
| 19 | + |
| 20 | +However, when a Change depends on a domain class and that class evolves (fields removed, renamed, or restructured), your older Changes will no longer compile or run correctly. |
| 21 | + |
| 22 | +### A practical example |
| 23 | + |
| 24 | +Consider a PostgreSQL database with a `customers` table. Initially, your domain model includes: |
| 25 | + |
| 26 | +```java |
| 27 | +public class Customer { |
| 28 | + private Long id; |
| 29 | + private String firstName; |
| 30 | + private String middleName; // Will be removed later |
| 31 | + private String lastName; |
| 32 | + private String email; |
| 33 | + // getters/setters... |
| 34 | +} |
| 35 | +``` |
| 36 | + |
| 37 | +You create a Change that uses this domain class: |
| 38 | + |
| 39 | +```java |
| 40 | +@Change(id = "add-premium-customers", order = "0001", author = "team") |
| 41 | +public class _0001_AddPremiumCustomers { |
| 42 | + |
| 43 | + @Apply |
| 44 | + public void apply(CustomerRepository repository) { |
| 45 | + Customer customer = new Customer(); |
| 46 | + customer.setFirstName("John"); |
| 47 | + customer.setMiddleName("William"); // Uses the field |
| 48 | + customer.setLastName("Smith"); |
| 49 | + customer .setEmail( "[email protected]"); |
| 50 | + repository.save(customer); |
| 51 | + } |
| 52 | +} |
| 53 | +``` |
| 54 | + |
| 55 | +Six months later, your team decides `middleName` is unnecessary and removes it from the `Customer` class. Now: |
| 56 | + |
| 57 | +- ✅ Your application works fine with the updated model |
| 58 | +- ❌ The Change `_0001_AddPremiumCustomers` no longer compiles |
| 59 | +- ❌ You can't run Flamingock in new environments |
| 60 | +- ❌ CI/CD pipelines break |
| 61 | + |
| 62 | +This breaks the principle of historical immutability and undermines Flamingock's reliability. |
| 63 | + |
| 64 | +## The solution: Generic structures |
| 65 | + |
| 66 | +To ensure stability, avoid injecting domain classes or anything tightly coupled to your evolving business model. Instead, use schema-free or generic structures. |
| 67 | + |
| 68 | +Here's how the same Change looks using generic structures: |
| 69 | + |
| 70 | +```java |
| 71 | +@Change(id = "add-premium-customers", order = "0001", author = "team") |
| 72 | +public class _0001_AddPremiumCustomers { |
| 73 | + |
| 74 | + @Apply |
| 75 | + public void apply(RestTemplate restTemplate) { |
| 76 | + // Using a Map instead of the Customer domain class |
| 77 | + Map<String, Object> customerData = new HashMap<>(); |
| 78 | + customerData.put("firstName", "John"); |
| 79 | + customerData.put("middleName", "William"); |
| 80 | + customerData.put("lastName", "Smith"); |
| 81 | + customerData .put( "email", "[email protected]"); |
| 82 | + customerData.put("status", "PREMIUM"); |
| 83 | + |
| 84 | + // Send to customer service API |
| 85 | + restTemplate.postForObject( |
| 86 | + "/api/customers", |
| 87 | + customerData, |
| 88 | + Map.class |
| 89 | + ); |
| 90 | + } |
| 91 | + |
| 92 | + @Rollback |
| 93 | + public void rollback(RestTemplate restTemplate) { |
| 94 | + // Remove the customer using email as identifier |
| 95 | + restTemplate .delete( "/api/customers/[email protected]"); |
| 96 | + } |
| 97 | +} |
| 98 | +``` |
| 99 | + |
| 100 | +This Change remains stable even if the `Customer` domain class evolves or the `middleName` field is removed. The Map structure is decoupled from your domain model. |
| 101 | + |
| 102 | +## When you need reusable logic |
| 103 | + |
| 104 | +If you have complex logic that needs to be shared across Changes, consider these approaches: |
| 105 | + |
| 106 | +### Utility classes for Changes |
| 107 | + |
| 108 | +Create utilities specifically for your Changes that are isolated from your domain: |
| 109 | + |
| 110 | +```java |
| 111 | +public class ChangeUtils { |
| 112 | + public static Map<String, Object> createCustomerData( |
| 113 | + String firstName, String lastName, String email) { |
| 114 | + return Map.of( |
| 115 | + "firstName", firstName, |
| 116 | + "lastName", lastName, |
| 117 | + "email", email, |
| 118 | + "createdAt", Instant.now().toString() |
| 119 | + ); |
| 120 | + } |
| 121 | +} |
| 122 | +``` |
| 123 | + |
| 124 | +### SQL files or scripts |
| 125 | + |
| 126 | +For complex SQL operations, consider external scripts: |
| 127 | + |
| 128 | +```java |
| 129 | +@Apply |
| 130 | +public void apply(JdbcTemplate jdbc) throws IOException { |
| 131 | + String sql = Files.readString( |
| 132 | + Paths.get("changes/sql/001_create_premium_customers.sql") |
| 133 | + ); |
| 134 | + jdbc.execute(sql); |
| 135 | +} |
| 136 | +``` |
| 137 | + |
| 138 | +## Best practices summary |
| 139 | + |
| 140 | +1. **Treat Changes as historical artifacts** - They are versioned records of the past, not part of your live business logic |
| 141 | + |
| 142 | +2. **Use generic structures** - Maps, Documents, ResultSets, or direct queries instead of domain objects |
| 143 | + |
| 144 | +3. **Keep Changes self-contained** - Minimize dependencies on external classes that might change |
| 145 | + |
| 146 | +4. **Test with evolution in mind** - Ensure your Changes compile and run even as your domain evolves |
| 147 | + |
| 148 | +5. **Document data structures** - When using generic structures, add comments explaining the expected schema |
| 149 | + |
| 150 | +## The balance |
| 151 | + |
| 152 | +We're not suggesting you should never use any classes in your Changes. The key is understanding the trade-off: |
| 153 | + |
| 154 | +- **Domain classes**: Type safety now, brittleness over time |
| 155 | +- **Generic structures**: Less type safety, long-term stability |
| 156 | + |
| 157 | +Choose based on your context, but be aware of the implications. For most production systems where Changes need to remain stable for years, generic structures are the safer choice. |
| 158 | + |
| 159 | +## Next steps |
| 160 | + |
| 161 | +- Review existing Changes for domain coupling |
| 162 | +- Establish team conventions for Change implementations |
| 163 | +- Consider using [Templates](../templates/templates-introduction.md) for standardized, decoupled change patterns |
0 commit comments