Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ linter:

# **Error Prevention**

# Enforce non-nullable types where possible.
always_require_non_null_named_parameters: true

# **Documentation**
# Require documentation for public members.
public_member_api_docs: false
Expand Down
7 changes: 5 additions & 2 deletions packages/bounded/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Strongly-typed identity primitives (`Identity` interface, `TypedIdentity` class)
- Value object support with structural equality (`ValueObject` mixin)
- Entity mixin with identity-based equality
- Aggregate root mixin with domain event collection
- Aggregate root base class for domain model entry points
- Domain event interface (`DomainEvent`)
- Event collection methods on aggregates (record, retrieve, clear)
- Pure Dart package with no infrastructure dependencies

### Removed

- Event collection from aggregate roots (no longer stores events internally)
49 changes: 0 additions & 49 deletions packages/bounded/lib/src/aggregate_root.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import 'domain_event.dart';
import 'entity.dart';
import 'identity.dart';

Expand All @@ -9,12 +8,6 @@ import 'identity.dart';
/// aggregate root is responsible for ensuring all invariants within the
/// aggregate are maintained.
///
/// Aggregate roots can record domain events during state transitions.
/// These events represent facts that have occurred within the domain
/// and can be published or persisted by application layer code.
///
/// An aggregate root is an [Entity] with event collection capabilities.
///
/// Example:
/// ```dart
/// class Order extends AggregateRoot<OrderId> {
Expand All @@ -28,7 +21,6 @@ import 'identity.dart';
/// throw StateError('Order can only be placed when pending');
/// }
/// status = OrderStatus.placed;
/// recordEvent(OrderPlaced(id, DateTime.now()));
/// }
/// }
/// ```
Expand All @@ -40,45 +32,4 @@ abstract class AggregateRoot<ID extends Identity> with Entity<ID> {

@override
ID get id => _id;

final List<DomainEvent> _events = [];

/// Records a domain event that occurred during a state transition.
///
/// Events are stored internally and can be retrieved via [events]
/// for publishing or persistence by application layer code.
void recordEvent(DomainEvent event) {
_events.add(event);
}

/// Returns a read-only view of domain events recorded by this aggregate.
///
/// This list contains events that have been recorded but not yet cleared.
List<DomainEvent> get events => List.unmodifiable(_events);

/// Clears all recorded domain events.
///
/// This should be called after events have been published or persisted
/// to prevent them from being processed multiple times.
void clearEvents() {
_events.clear();
}

/// Returns all recorded domain events and clears them in a single operation.
///
/// This is a convenience for the common application-layer flow:
/// 1) perform a domain operation
/// 2) publish/persist the resulting events
/// 3) clear the pending event buffer
///
/// Events are returned in the order they were recorded.
List<DomainEvent> pullEvents() {
if (_events.isEmpty) {
return const <DomainEvent>[];
}

final drained = List<DomainEvent>.unmodifiable(_events);
_events.clear();
return drained;
}
}
35 changes: 0 additions & 35 deletions packages/bounded/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,39 +12,4 @@ dev_dependencies:
test: ^1.25.0
lints: ^6.0.0

# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec

# The following section is specific to Flutter packages.
flutter:

# To add assets to your package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/to/asset-from-package
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images

# To add custom fonts to your package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/to/font-from-package
66 changes: 5 additions & 61 deletions packages/bounded/test/aggregate_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,93 +37,37 @@ class Order extends AggregateRoot<OrderId> {
throw StateError('Order can only be placed when pending');
}
status = OrderStatus.placed;
recordEvent(OrderPlaced(id, at));
}

void ship() {
if (status != OrderStatus.placed) {
throw StateError('Order can only be shipped when placed');
}
status = OrderStatus.shipped;
recordEvent(OrderShipped(id));
}
}

void main() {
group('AggregateRoot', () {
test('aggregate can record domain events during transitions', () {
test('aggregate can perform state transitions', () {
final order = Order(const OrderId('order-123'));
final placedAt = DateTime(2026, 1, 21);

order.place(placedAt);

expect(order.events, hasLength(1));
expect(order.events.first, isA<OrderPlaced>());
final event = order.events.first as OrderPlaced;
expect(event.orderId, equals(order.id));
expect(event.placedAt, equals(placedAt));
expect(order.status, equals(OrderStatus.placed));
});

test('aggregate records multiple events', () {
test('aggregate can perform multiple transitions', () {
final order = Order(const OrderId('order-123'));

order.place(DateTime.now());
order.ship();

expect(order.events, hasLength(2));
expect(order.events[0], isA<OrderPlaced>());
expect(order.events[1], isA<OrderShipped>());
expect(order.status, equals(OrderStatus.shipped));
});

test('events property returns read-only view', () {
final order = Order(const OrderId('order-123'));
order.place(DateTime.now());

final events = order.events;
expect(() => (events as List).add(OrderShipped(order.id)), throwsUnsupportedError);
});

test('clearEvents removes all recorded events', () {
final order = Order(const OrderId('order-123'));
order.place(DateTime.now());

expect(order.events, hasLength(1));

order.clearEvents();

expect(order.events, isEmpty);
});

test('pullEvents drains events in order and clears', () {
final order = Order(const OrderId('order-123'));

order.place(DateTime(2026, 1, 21));
order.ship();

final drained = order.pullEvents();

expect(drained, hasLength(2));
expect(drained[0], isA<OrderPlaced>());
expect(drained[1], isA<OrderShipped>());
expect(order.events, isEmpty);
});

test('pullEvents returns empty list when no events are recorded', () {
final order = Order(const OrderId('order-123'));

final drained = order.pullEvents();

expect(drained, isEmpty);
expect(order.events, isEmpty);
});

test('aggregate starts with no events', () {
final order = Order(const OrderId('order-123'));

expect(order.events, isEmpty);
});

test('aggregate enforces invariants without infrastructure', () {
test('aggregate enforces invariants', () {
final order = Order(const OrderId('order-123'));

// Cannot ship without placing first
Expand Down
16 changes: 8 additions & 8 deletions packages/bounded_lints/example/pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -195,18 +195,18 @@ packages:
dependency: transitive
description:
name: json_annotation
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
sha256: "805fa86df56383000f640384b282ce0cb8431f1a7a2396de92fb66186d8c57df"
url: "https://pub.dev"
source: hosted
version: "4.9.0"
version: "4.10.0"
lints:
dependency: "direct dev"
description:
name: lints
sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
version: "6.1.0"
logging:
dependency: transitive
description:
Expand All @@ -227,10 +227,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
sha256: "9f29b9bcc8ee287b1a31e0d01be0eae99a930dbffdaecf04b3f3d82a969f296f"
url: "https://pub.dev"
source: hosted
version: "1.18.0"
version: "1.18.1"
package_config:
dependency: transitive
description:
Expand Down Expand Up @@ -275,10 +275,10 @@ packages:
dependency: transitive
description:
name: source_span
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.1"
version: "1.10.2"
stack_trace:
dependency: transitive
description:
Expand Down