domain-driven-designdddd

How to manage two coupled aggregates in DDD?


I am developing Electric Vehicle Charging Station Management System, which is connected to several Charging Stations, and I am in an impasse. In this domain, I've come up with an aggregate for the Charging Station, which includes the internal state of the Charging Station(whether it is connected, the internal state of its Connectors).

It has a UnlockConnector method, for which, to accurately respect its name(and not be anemic) it sends the request to the respective Charging Station, as it is central to my domain to know of the Charging Station's connectivity, and to send requests to the physical Charging Station:

type Connector struct {
  Status string
}

type ChargingStation struct {
  Connected bool
  Connectors []Connector
  URL string
  ...
}

func (station *ChargingStation) UnlockConnector(connectorID int, stationClient service.StationClient) error {
  if !station.Connected {
    return errors.New("charging station is not connected")
  }
  connector := station.Connectors[connectorID]
  if connector.Status != "Available" {
    return errors.New("connector is not available to be unlocked")
  }
  err := stationClient.SendUnlockConnectorRequest(station.URL, connectorID)
  if err != nil {
    return errors.New("charging station rejected the request")
  }
  station.On(event.StationConnectorUnlocked{
    ConnectorID: connectorID,
    Timestamp: time.Now(),
  })
  return nil
}

And I've come up with another aggregate which represents the Charging Session, the interaction between a User and a Charging Station's Connector. The creation of a Charging Session is entirely coupled with the Connector's state, i.e. if the Connector has been unlocked by a user, a session was created, if the Connector's energy flow has stopped, the Charging Session has ended.

However coupled they both are, the Charging Session entity doesn't seem to belong to the Charging Station Aggregate, as it doesn't respond to the primary question: when a Station is deleted, should its Charging Sessions be deleted as well?) also, I when I would pay for the energy consumed in this session, it wouldn't have anything to do with the Station Aggregate Context.

I thought of creating a SessionCreator domain service that makes sure that when a Charging Station's Connector is unlocked, a Charging Session is to be created as well:

type SessionCreator interface {
  CreateSession(station *station.Station, connectorID int, sessionID int) error
}

type sessionCreator struct {
  stationClient StationClient
}

type (svc sessionCreator) CreateSession(station *station.Station, connectorID int, userID string, sessionID int) error {
  err := station.UnlockConnector(connectorID, svc.stationClient)
  if err != nil {
    return err
  }
  session.Create(sessionID, station.ID, connectorID, userID)
  return nil
}

However it just feels a little odd, and doesn't quite satisfy the other invariants(when a connector's energy flow stops, it must end the session), I though as well as making a Event Listener that listens to the StationConnectorUnlocked event, but I just don't know which would be the ideal way.


Solution

  • From what I can understand from the problem, I think that your aggregates Charging Station and Charging Session are correct. You say they are both coupled, but it seems to me that Charging Station doesn't need to know about the existence of the Session, and the Session doesn't really need to know anything about the internals of the Station, so the coupling to me is low.

    Regarding your question about how to create and modify the Session when things happen on the Station, the solution seems clear to me based on your wording:

    if the Connector has been unlocked by a user, a session was created, if the Connector's energy flow has stopped, the Charging Session has ended.

    The highlighted words are business Events. You could draw it like:

    And the way I'd implement this is with a pub/sub system, so aggregates publish events after a state change and subscribers trigger use cases, which might execute an operation on an aggregate, which will likely end up publishing an event.

    The distributed nature of this approach makes it a bit more difficult to follow what is going on on the whole system as Unlocking the Connector and creating the Session don't happen in a single place. They will likely happen in two different parts of the system. The advantage is that over time, stakeholders can come up with many things that need to happen when the Connector is unlocked and you'll only have to keep adding subscribers to that existing event.