4

I am developing an event-sourced Electric Vehicle Charging Station Management System, which is connected to several Charging Stations. 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).

The commands I can issue to a Station aggregate are:

  • UnlockConnector, which emits StationConnectorUnlocked

  • StopConnectorEnergyFlow, which emits StationConnectorEnergyFlowStopped

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 coupled with these events, 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 finished.

I've added a Process Manager that listens to the events:

  • StationConnectorUnlocked(stationID, connectorID) -> SessionCreated(new uuid())

  • StationConnectorEnergyFlowStopped(stationID, connectorID) -> SessionFinished(id???)

for the first event, it's pretty straight-forward to create the session. However for the latter, it must know the sessionID of the ongoing session happening on the Connector(connectorID) of the Station(stationID), so it can update the session.

I can't simply implement a GetSessionByConnectorID function as I'm implementing an event-sourced system, and the only way I can get the event stream of a session, is by its ID, not by its ConnectorID(because I only know the session's ConnectorID when I hydrate it back), so I don't see how I could implement the GetSessionByConnectorID function.

So, the process manager has some internal in-memory state((stationID, connectorID) -> (sessionID)), to keep track of the sessionID. However, as it is in-memory, as soon as the process manager crashes, I've lost the association between the (stationID, connectorID) <-> (sessionID) and I can no longer respond properly to the ConnectorEnergyFlowStopped event.

session-flow

How should I handle that? Should I persist the Process Manager state? Should I persist it with Process Manager events(which would be awkward since, it does not correlate nicely with the Ubiquitous Language, i'm thinking SessionProcessManagerReceivedStationConnectorUnlockedEvent-awkward-level)

Edit - new thought

I thought about something else, which would be to remove the internal Process Manager state, and put the association of the (stationID, connectorID) <-> (sessionID), inside of the Station aggregate. That would, unfortunately, mean a higher coupling(the Station must know when a session is Created, and how to generate its ID), but I think it could be simpler(as the Station's events are persisted). So the Station would emit the session-related events, with the sessionID inside of them:

  • StationConnectorUnlocked(stationID, connectorID, sessionID) -> SessionCreated(sessionID)

  • StationConnectorEnergyFlowStopped(stationID, connectorID, sessionID) -> SessionFinished(sessionID)

However, it does seem a bit odd to mix the two things, even if it's just the ID of the session.

Henke
  • 364
  • 6
  • 13
  • A diagram would help. The event flow is hard to understand. I am 95% certain you don't need a process manager. Btw, process managers _always_ have persisted state. But I don't think it's relevant here. So, the answer to the question title is "yes" but your problem is the modelling problem, – Alexey Zimarev May 16 '20 at 15:35
  • As I mentioned on the other question, I don't know the proper way to solve this in ES and without ES I would just query the Session by ConnectorId. So, I've just had an idea: given the mapping from ConnectorId to SessionId is a relatively short need, why don't you simply store it in a distributed cache (Redis)? You can completely get rid of this process manager, simply implement the event handlers which use a sessionRepository.GetByConnectorId() and internally this repo can translate the the ConnectorId to SessionId using cache. This makes the solution cleaner and hides the ES technical needs – Francesc Castells May 16 '20 at 15:55
  • @AlexeyZimarev I've added a diagram to explain it more clearly – Henke May 16 '20 at 16:04
  • @FrancescCastells yeah, that makes sense, it's really just a simple mapping that I need. I've added a new train of thought but I think that leads to higher coupling. – Henke May 16 '20 at 16:06

3 Answers3

4

To me the process manager just gets in the way here. Aggregates aren't supposed to spawn from nowhere and I think you are overly concerned about Station knowing about Session.

Why not something expressive like session = station.unlockConnector(sessionId)? It's pretty common to use factory methods on ARs to create new related ARs, unless Session is an entirely seperate BC.

Furthermore, the lifecycle of Session seems closely related to Station. Can you afford eventual consistency when a connector gets closed? What if the session gets mutated at the same time the connector is closed?

plalx
  • 42,889
  • 6
  • 74
  • 90
  • 1
    It looks like a good approach. What's the need for passing the sessionId if unlockConnector will create a new session? Even, unlockConnector could decide to return the previous session if it wasn't terminated for example, not needing a new sessionId. – Francesc Castells May 17 '20 at 06:31
  • @FrancescCastells ID generation can usually be seen as an infrastructure detail that shouldn't be part of the ARs, unless they are for entities localized within the AR. Whether the ID is client-generated or server generated I always pass the IDs to ARs. Furthermore if you use GUIDs then the client can generate IDs which gives a few advantages. It relieves the server from returning the ID in the ack response. It allows you to move to async CQRS more easily. It also enables idempotency when handling commands. – plalx May 17 '20 at 11:19
  • 1
    That'd make more sense to me if the client (the station) sent an OpenSessionCommand, but here it sends UnlockConnectorCommand and it doesn't seem that the client needs to know about Sessions. If the Station AR is in charge of creating Sessions, it seems weird to me that an external element passes the Id, because it implies that it needs to know if the Station will create a new Session or not. An alternative would be that the Session is created with an empty Id and the infrastructure that persists it generates it. – Francesc Castells May 17 '20 at 16:28
  • Hard to reason about the domain when you dont know it. What kind of behaviors does sessions have? Waiting after persistance for IDs doesn't play very well with events. How do you publish events in the model when you don't yet have the ARs ID? – plalx May 17 '20 at 18:26
  • Of course. I have more context thanks to a previous question: https://stackoverflow.com/q/61529751/352826 – Francesc Castells May 17 '20 at 19:02
  • "How do you publish events in the model when you don't yet have the ARs ID?" Well, I don't get them from the DB. Most of the time I create GUIDs. I only proposed leaving it to the infrastructure because you said that you considered it an infrastructure concern. My only point was that if the Station was responsible for creating the Session AR, it should also be the Station to decide if it was a new Id or an existing one. Personally, I would simply make it create a new Session with a new GUID or return the previous session if there wasn't the need of creating a new one. – Francesc Castells May 17 '20 at 19:10
  • 1
    What's the point of unlocking a connector if not to create a session? How useful is it to know a connector is unlocked without knowing details about the client? Perhaps `Session` is not even an AR afterall. Perhaps it's merely a projection of connector events? Does `Session` protect any kind of invariants? – plalx May 17 '20 at 21:00
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/214075/discussion-between-francesc-castells-and-plalx). – Francesc Castells May 17 '20 at 21:09
  • I've only now seen the discussion, and both of you are correct in your assumptions. The motivation of the Session aggregate is almost exclusively to billing. There is in fact problems in signal strength and the Stations sometimes doesn't communicate what has happened when the connection is lost. Hopefully the "important" messages(those which start/end a session) are persisted, and sent when the connection is restablished. The book of record is in fact the Station, and that is a bit awkward to deal, as some commands coming from the Station are not validated(?) – Henke Jun 01 '20 at 15:49
  • 2
    I've gone for the solution that the Station aggregate knows the SessionID of a given connector, and it sends that SessionID in its ConnectorUnlocked and ConnectorEnergyFlowStopped events – Henke Jun 01 '20 at 15:51
2

I can see quite a few ways of solving this but I am not familiar with your domain, so just throwing ideas here:

  • Use the process manager. Process managers handle long-running processes and need to persist the state _by definitions. Message-driven process managers usually don't need to even run between messages, so they process events, issue commands and shut down until the next message. Or finalize. That's, as I understand, was the initial question.

  • Issue session UUID from the station, for each session. Then, the station would know the session ID and there's no need for process managers.

  • Use the query model. Project the session started event with all the information you need. Query it when the flow of energy stops to find out an ongoing session for a given station and connector, you might not even need the user id. Project sessionClosed event and delete the read model.

If possible, I'd go with the second one. The last one would be second in my list and the process manager would be the last resort due to the associated complexity.

Alexey Zimarev
  • 17,944
  • 2
  • 55
  • 83
  • 1
    I think the second option would be the simplest, and most straight-forward. My only worry is that it makes the Station and Session Aggregate more coupled, since the Station must know when a Session is created. I might be overthinking, like I do, but it also seems a minimal coupling. What do you think? – Henke May 16 '20 at 19:51
0

I think the problem should be taken in the perspective of vending machine and hope below may answer to design your aggregate. https://sourcemaking.com/design_patterns/state

Biju
  • 45
  • 1
  • 9